Merge branch 'master' into production
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ profile.stats
|
|||||||
|
|
||||||
pillar/web/static/assets/css/*.css
|
pillar/web/static/assets/css/*.css
|
||||||
pillar/web/static/assets/js/*.min.js
|
pillar/web/static/assets/js/*.min.js
|
||||||
|
pillar/web/static/assets/js/vendor/video.min.js
|
||||||
pillar/web/static/storage/
|
pillar/web/static/storage/
|
||||||
pillar/web/static/uploads/
|
pillar/web/static/uploads/
|
||||||
pillar/web/templates/
|
pillar/web/templates/
|
||||||
|
60
gulpfile.js
60
gulpfile.js
@@ -12,15 +12,16 @@ var pug = require('gulp-pug');
|
|||||||
var rename = require('gulp-rename');
|
var rename = require('gulp-rename');
|
||||||
var sass = require('gulp-sass');
|
var sass = require('gulp-sass');
|
||||||
var sourcemaps = require('gulp-sourcemaps');
|
var sourcemaps = require('gulp-sourcemaps');
|
||||||
var uglify = require('gulp-uglify');
|
var uglify = require('gulp-uglify-es').default;
|
||||||
|
|
||||||
var enabled = {
|
var enabled = {
|
||||||
uglify: argv.production,
|
uglify: argv.production,
|
||||||
maps: argv.production,
|
maps: !argv.production,
|
||||||
failCheck: !argv.production,
|
failCheck: !argv.production,
|
||||||
prettyPug: !argv.production,
|
prettyPug: !argv.production,
|
||||||
cachify: !argv.production,
|
cachify: !argv.production,
|
||||||
cleanup: argv.production,
|
cleanup: argv.production,
|
||||||
|
chmod: argv.production,
|
||||||
};
|
};
|
||||||
|
|
||||||
var destination = {
|
var destination = {
|
||||||
@@ -29,6 +30,11 @@ var destination = {
|
|||||||
js: 'pillar/web/static/assets/js',
|
js: 'pillar/web/static/assets/js',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var source = {
|
||||||
|
bootstrap: 'node_modules/bootstrap/',
|
||||||
|
jquery: 'node_modules/jquery/',
|
||||||
|
popper: 'node_modules/popper.js/'
|
||||||
|
}
|
||||||
|
|
||||||
/* CSS */
|
/* CSS */
|
||||||
gulp.task('styles', function() {
|
gulp.task('styles', function() {
|
||||||
@@ -67,36 +73,52 @@ gulp.task('scripts', function() {
|
|||||||
.pipe(gulpif(enabled.uglify, uglify()))
|
.pipe(gulpif(enabled.uglify, uglify()))
|
||||||
.pipe(rename({suffix: '.min'}))
|
.pipe(rename({suffix: '.min'}))
|
||||||
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
|
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
|
||||||
.pipe(chmod(644))
|
.pipe(gulpif(enabled.chmod, chmod(644)))
|
||||||
.pipe(gulp.dest(destination.js))
|
.pipe(gulp.dest(destination.js))
|
||||||
.pipe(gulpif(argv.livereload, livereload()));
|
.pipe(gulpif(argv.livereload, livereload()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* Collection of scripts in src/scripts/tutti/ to merge into tutti.min.js */
|
/* Collection of scripts in src/scripts/tutti/ to merge into tutti.min.js
|
||||||
/* Since it's always loaded, it's only for functions that we want site-wide */
|
* Since it's always loaded, it's only for functions that we want site-wide.
|
||||||
|
* It also includes jQuery and Bootstrap (and its dependency popper), since
|
||||||
|
* the site doesn't work without it anyway.*/
|
||||||
gulp.task('scripts_concat_tutti', function() {
|
gulp.task('scripts_concat_tutti', function() {
|
||||||
gulp.src('src/scripts/tutti/**/*.js')
|
|
||||||
|
toUglify = [
|
||||||
|
source.jquery + 'dist/jquery.min.js',
|
||||||
|
source.popper + 'dist/umd/popper.min.js',
|
||||||
|
source.bootstrap + 'js/dist/index.js',
|
||||||
|
source.bootstrap + 'js/dist/util.js',
|
||||||
|
source.bootstrap + 'js/dist/alert.js',
|
||||||
|
source.bootstrap + 'js/dist/collapse.js',
|
||||||
|
source.bootstrap + 'js/dist/dropdown.js',
|
||||||
|
source.bootstrap + 'js/dist/tooltip.js',
|
||||||
|
'src/scripts/tutti/**/*.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
gulp.src(toUglify)
|
||||||
.pipe(gulpif(enabled.failCheck, plumber()))
|
.pipe(gulpif(enabled.failCheck, plumber()))
|
||||||
.pipe(gulpif(enabled.maps, sourcemaps.init()))
|
.pipe(gulpif(enabled.maps, sourcemaps.init()))
|
||||||
.pipe(concat("tutti.min.js"))
|
.pipe(concat("tutti.min.js"))
|
||||||
.pipe(gulpif(enabled.uglify, uglify()))
|
.pipe(gulpif(enabled.uglify, uglify()))
|
||||||
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
|
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
|
||||||
.pipe(chmod(644))
|
.pipe(gulpif(enabled.chmod, chmod(644)))
|
||||||
.pipe(gulp.dest(destination.js))
|
.pipe(gulp.dest(destination.js))
|
||||||
.pipe(gulpif(argv.livereload, livereload()));
|
.pipe(gulpif(argv.livereload, livereload()));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('scripts_concat_markdown', function() {
|
|
||||||
gulp.src('src/scripts/markdown/**/*.js')
|
/* Simply move these vendor scripts from node_modules. */
|
||||||
.pipe(gulpif(enabled.failCheck, plumber()))
|
gulp.task('scripts_move_vendor', function(done) {
|
||||||
.pipe(gulpif(enabled.maps, sourcemaps.init()))
|
|
||||||
.pipe(concat("markdown.min.js"))
|
let toMove = [
|
||||||
.pipe(gulpif(enabled.uglify, uglify()))
|
'node_modules/video.js/dist/video.min.js',
|
||||||
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
|
];
|
||||||
.pipe(chmod(644))
|
|
||||||
.pipe(gulp.dest(destination.js))
|
gulp.src(toMove)
|
||||||
.pipe(gulpif(argv.livereload, livereload()));
|
.pipe(gulp.dest(destination.js + '/vendor/'));
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -111,9 +133,9 @@ gulp.task('watch',function() {
|
|||||||
gulp.watch('src/templates/**/*.pug',['templates']);
|
gulp.watch('src/templates/**/*.pug',['templates']);
|
||||||
gulp.watch('src/scripts/*.js',['scripts']);
|
gulp.watch('src/scripts/*.js',['scripts']);
|
||||||
gulp.watch('src/scripts/tutti/**/*.js',['scripts_concat_tutti']);
|
gulp.watch('src/scripts/tutti/**/*.js',['scripts_concat_tutti']);
|
||||||
gulp.watch('src/scripts/markdown/**/*.js',['scripts_concat_markdown']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Erases all generated files in output directories.
|
// Erases all generated files in output directories.
|
||||||
gulp.task('cleanup', function() {
|
gulp.task('cleanup', function() {
|
||||||
var paths = [];
|
var paths = [];
|
||||||
@@ -136,5 +158,5 @@ gulp.task('default', tasks.concat([
|
|||||||
'templates',
|
'templates',
|
||||||
'scripts',
|
'scripts',
|
||||||
'scripts_concat_tutti',
|
'scripts_concat_tutti',
|
||||||
'scripts_concat_markdown',
|
'scripts_move_vendor',
|
||||||
]));
|
]));
|
||||||
|
3597
package-lock.json
generated
3597
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,26 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "pillar",
|
"name": "pillar",
|
||||||
"license": "GPL-2.0+",
|
"license": "GPL-2.0+",
|
||||||
"author": "Blender Institute",
|
"author": "Blender Institute",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/armadillica/pillar.git"
|
"url": "git://git.blender.org/pillar.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp": "~3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-autoprefixer": "~2.3.1",
|
"gulp-autoprefixer": "^6.0.0",
|
||||||
"gulp-cached": "~1.1.0",
|
"gulp-cached": "^1.1.1",
|
||||||
"gulp-chmod": "~1.3.0",
|
"gulp-chmod": "^2.0.0",
|
||||||
"gulp-concat": "~2.6.0",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-if": "^2.0.1",
|
"gulp-if": "^2.0.2",
|
||||||
"gulp-git": "~2.4.2",
|
"gulp-git": "^2.8.0",
|
||||||
"gulp-livereload": "~3.8.1",
|
"gulp-livereload": "^4.0.0",
|
||||||
"gulp-plumber": "~1.1.0",
|
"gulp-plumber": "^1.2.0",
|
||||||
"gulp-pug": "~3.2.0",
|
"gulp-pug": "^4.0.1",
|
||||||
"gulp-rename": "~1.2.2",
|
"gulp-rename": "^1.4.0",
|
||||||
"gulp-sass": "~2.3.1",
|
"gulp-sass": "^4.0.1",
|
||||||
"gulp-sourcemaps": "~1.6.0",
|
"gulp-sourcemaps": "^2.6.4",
|
||||||
"gulp-uglify": "~1.5.3",
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
}
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^4.1.3",
|
||||||
|
"jquery": "^3.3.1",
|
||||||
|
"popper.js": "^1.14.4",
|
||||||
|
"video.js": "^7.2.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -140,8 +140,6 @@ class PillarServer(BlinkerCompatibleEve):
|
|||||||
|
|
||||||
self.org_manager = pillar.api.organizations.OrgManager()
|
self.org_manager = pillar.api.organizations.OrgManager()
|
||||||
|
|
||||||
self.before_first_request(self.setup_db_indices)
|
|
||||||
|
|
||||||
# Make CSRF protection available to the application. By default it is
|
# Make CSRF protection available to the application. By default it is
|
||||||
# disabled on all endpoints. More info at WTF_CSRF_CHECK_DEFAULT in config.py
|
# disabled on all endpoints. More info at WTF_CSRF_CHECK_DEFAULT in config.py
|
||||||
self.csrf = CSRFProtect(self)
|
self.csrf = CSRFProtect(self)
|
||||||
@@ -479,10 +477,11 @@ class PillarServer(BlinkerCompatibleEve):
|
|||||||
|
|
||||||
# Pillar-defined Celery task modules:
|
# Pillar-defined Celery task modules:
|
||||||
celery_task_modules = [
|
celery_task_modules = [
|
||||||
'pillar.celery.tasks',
|
'pillar.celery.badges',
|
||||||
'pillar.celery.search_index_tasks',
|
|
||||||
'pillar.celery.file_link_tasks',
|
|
||||||
'pillar.celery.email_tasks',
|
'pillar.celery.email_tasks',
|
||||||
|
'pillar.celery.file_link_tasks',
|
||||||
|
'pillar.celery.search_index_tasks',
|
||||||
|
'pillar.celery.tasks',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Allow Pillar extensions from defining their own Celery tasks.
|
# Allow Pillar extensions from defining their own Celery tasks.
|
||||||
@@ -704,6 +703,8 @@ class PillarServer(BlinkerCompatibleEve):
|
|||||||
def finish_startup(self):
|
def finish_startup(self):
|
||||||
self.log.info('Using MongoDB database %r', self.config['MONGO_DBNAME'])
|
self.log.info('Using MongoDB database %r', self.config['MONGO_DBNAME'])
|
||||||
|
|
||||||
|
with self.app_context():
|
||||||
|
self.setup_db_indices()
|
||||||
self._config_celery()
|
self._config_celery()
|
||||||
|
|
||||||
api.setup_app(self)
|
api.setup_app(self)
|
||||||
@@ -760,6 +761,8 @@ class PillarServer(BlinkerCompatibleEve):
|
|||||||
coll.create_index([('properties.status', pymongo.ASCENDING),
|
coll.create_index([('properties.status', pymongo.ASCENDING),
|
||||||
('node_type', pymongo.ASCENDING),
|
('node_type', pymongo.ASCENDING),
|
||||||
('_created', pymongo.DESCENDING)])
|
('_created', pymongo.DESCENDING)])
|
||||||
|
# Used for asset tags
|
||||||
|
coll.create_index([('properties.tags', pymongo.ASCENDING)])
|
||||||
|
|
||||||
coll = db['projects']
|
coll = db['projects']
|
||||||
# This index is used for statistics, and for fetching public projects.
|
# This index is used for statistics, and for fetching public projects.
|
||||||
|
@@ -220,7 +220,7 @@ def fetch_blenderid_user() -> dict:
|
|||||||
|
|
||||||
my_log = log.getChild('fetch_blenderid_user')
|
my_log = log.getChild('fetch_blenderid_user')
|
||||||
|
|
||||||
bid_url = '%s/api/user' % current_app.config['BLENDER_ID_ENDPOINT']
|
bid_url = urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'api/user')
|
||||||
my_log.debug('Fetching user info from %s', bid_url)
|
my_log.debug('Fetching user info from %s', bid_url)
|
||||||
|
|
||||||
credentials = current_app.config['OAUTH_CREDENTIALS']['blender-id']
|
credentials = current_app.config['OAUTH_CREDENTIALS']['blender-id']
|
||||||
|
@@ -123,6 +123,43 @@ users_schema = {
|
|||||||
'allow_unknown': True,
|
'allow_unknown': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# Node-specific information for this user.
|
||||||
|
'nodes': {
|
||||||
|
'type': 'dict',
|
||||||
|
'schema': {
|
||||||
|
# Per watched video info about where the user left off, both in time and in percent.
|
||||||
|
'view_progress': {
|
||||||
|
'type': 'dict',
|
||||||
|
# Keyed by Node ID of the video asset. MongoDB doesn't support using
|
||||||
|
# ObjectIds as key, so we cast them to string instead.
|
||||||
|
'keyschema': {'type': 'string'},
|
||||||
|
'valueschema': {
|
||||||
|
'type': 'dict',
|
||||||
|
'schema': {
|
||||||
|
'progress_in_sec': {'type': 'float', 'min': 0},
|
||||||
|
'progress_in_percent': {'type': 'integer', 'min': 0, 'max': 100},
|
||||||
|
|
||||||
|
# When the progress was last updated, so we can limit this history to
|
||||||
|
# the last-watched N videos if we want, or show stuff in chrono order.
|
||||||
|
'last_watched': {'type': 'datetime'},
|
||||||
|
|
||||||
|
# True means progress_in_percent = 100, for easy querying
|
||||||
|
'done': {'type': 'boolean', 'default': False},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'badges': {
|
||||||
|
'type': 'dict',
|
||||||
|
'schema': {
|
||||||
|
'html': {'type': 'string'}, # HTML fetched from Blender ID.
|
||||||
|
'expires': {'type': 'datetime'}, # When we should fetch it again.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
# Properties defined by extensions. Extensions should use their name (see the
|
# Properties defined by extensions. Extensions should use their name (see the
|
||||||
# PillarExtension.name property) as the key, and are free to use whatever they want as value,
|
# PillarExtension.name property) as the key, and are free to use whatever they want as value,
|
||||||
# but we suggest a dict for future extendability.
|
# but we suggest a dict for future extendability.
|
||||||
@@ -339,11 +376,11 @@ tokens_schema = {
|
|||||||
},
|
},
|
||||||
'token': {
|
'token': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'required': False,
|
'required': True,
|
||||||
},
|
},
|
||||||
'token_hashed': {
|
'token_hashed': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'required': True,
|
'required': False,
|
||||||
},
|
},
|
||||||
'expire_time': {
|
'expire_time': {
|
||||||
'type': 'datetime',
|
'type': 'datetime',
|
||||||
@@ -362,6 +399,13 @@ tokens_schema = {
|
|||||||
'type': 'string',
|
'type': 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# OAuth scopes granted to this token.
|
||||||
|
'oauth_scopes': {
|
||||||
|
'type': 'list',
|
||||||
|
'default': [],
|
||||||
|
'schema': {'type': 'string'},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
files_schema = {
|
files_schema = {
|
||||||
|
@@ -94,17 +94,10 @@ def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
|
|||||||
|
|
||||||
# Use 'xy' as altargs to prevent + and / characters from appearing.
|
# Use 'xy' as altargs to prevent + and / characters from appearing.
|
||||||
# We never have to b64decode the string anyway.
|
# We never have to b64decode the string anyway.
|
||||||
token_bytes = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
|
token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
|
||||||
token = token_bytes.decode('ascii')
|
|
||||||
|
|
||||||
token_expiry = utcnow() + datetime.timedelta(days=days)
|
token_expiry = utcnow() + datetime.timedelta(days=days)
|
||||||
token_data = store_token(user_id, token, token_expiry)
|
return store_token(user_id, token.decode('ascii'), token_expiry)
|
||||||
|
|
||||||
# Include the token in the returned document so that it can be stored client-side,
|
|
||||||
# in configuration, etc.
|
|
||||||
token_data['token'] = token
|
|
||||||
|
|
||||||
return token_data
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:
|
def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:
|
||||||
|
@@ -1,17 +1,11 @@
|
|||||||
import base64
|
import base64
|
||||||
import functools
|
|
||||||
import logging
|
import logging
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
import pymongo.errors
|
import pymongo.errors
|
||||||
import werkzeug.exceptions as wz_exceptions
|
import werkzeug.exceptions as wz_exceptions
|
||||||
from bson import ObjectId
|
|
||||||
from flask import current_app, Blueprint, request
|
from flask import current_app, Blueprint, request
|
||||||
|
|
||||||
import pillar.markdown
|
from pillar.api.nodes import eve_hooks
|
||||||
from pillar.api.activities import activity_subscribe, activity_object_add
|
|
||||||
from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES
|
|
||||||
from pillar.api.file_storage_backends.gcs import update_file_name
|
|
||||||
from pillar.api.utils import str2id, jsonify
|
from pillar.api.utils import str2id, jsonify
|
||||||
from pillar.api.utils.authorization import check_permissions, require_login
|
from pillar.api.utils.authorization import check_permissions, require_login
|
||||||
|
|
||||||
@@ -20,40 +14,6 @@ blueprint = Blueprint('nodes_api', __name__)
|
|||||||
ROLES_FOR_SHARING = {'subscriber', 'demo'}
|
ROLES_FOR_SHARING = {'subscriber', 'demo'}
|
||||||
|
|
||||||
|
|
||||||
def only_for_node_type_decorator(*required_node_type_names):
|
|
||||||
"""Returns a decorator that checks its first argument's node type.
|
|
||||||
|
|
||||||
If the node type is not of the required node type, returns None,
|
|
||||||
otherwise calls the wrapped function.
|
|
||||||
|
|
||||||
>>> deco = only_for_node_type_decorator('comment')
|
|
||||||
>>> @deco
|
|
||||||
... def handle_comment(node): pass
|
|
||||||
|
|
||||||
>>> deco = only_for_node_type_decorator('comment', 'post')
|
|
||||||
>>> @deco
|
|
||||||
... def handle_comment_or_post(node): pass
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Convert to a set for efficient 'x in required_node_type_names' queries.
|
|
||||||
required_node_type_names = set(required_node_type_names)
|
|
||||||
|
|
||||||
def only_for_node_type(wrapped):
|
|
||||||
@functools.wraps(wrapped)
|
|
||||||
def wrapper(node, *args, **kwargs):
|
|
||||||
if node.get('node_type') not in required_node_type_names:
|
|
||||||
return
|
|
||||||
|
|
||||||
return wrapped(node, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
only_for_node_type.__doc__ = "Decorator, immediately returns when " \
|
|
||||||
"the first argument is not of type %s." % required_node_type_names
|
|
||||||
return only_for_node_type
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/<node_id>/share', methods=['GET', 'POST'])
|
@blueprint.route('/<node_id>/share', methods=['GET', 'POST'])
|
||||||
@require_login(require_roles=ROLES_FOR_SHARING)
|
@require_login(require_roles=ROLES_FOR_SHARING)
|
||||||
def share_node(node_id):
|
def share_node(node_id):
|
||||||
@@ -86,7 +46,68 @@ def share_node(node_id):
|
|||||||
else:
|
else:
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
return jsonify(short_link_info(short_code), status=status)
|
return jsonify(eve_hooks.short_link_info(short_code), status=status)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/tagged/')
|
||||||
|
@blueprint.route('/tagged/<tag>')
|
||||||
|
def tagged(tag=''):
|
||||||
|
"""Return all tagged nodes of public projects as JSON."""
|
||||||
|
from pillar.auth import current_user
|
||||||
|
|
||||||
|
# We explicitly register the tagless endpoint to raise a 404, otherwise the PATCH
|
||||||
|
# handler on /api/nodes/<node_id> will return a 405 Method Not Allowed.
|
||||||
|
if not tag:
|
||||||
|
raise wz_exceptions.NotFound()
|
||||||
|
|
||||||
|
# Build the (cached) list of tagged nodes
|
||||||
|
agg_list = _tagged(tag)
|
||||||
|
|
||||||
|
# If the user is anonymous, no more information is needed and we return
|
||||||
|
if current_user.is_anonymous:
|
||||||
|
return jsonify(agg_list)
|
||||||
|
|
||||||
|
# If the user is authenticated, attach view_progress for video assets
|
||||||
|
view_progress = current_user.nodes['view_progress']
|
||||||
|
for node in agg_list:
|
||||||
|
node_id = str(node['_id'])
|
||||||
|
# View progress should be added only for nodes of type 'asset' and
|
||||||
|
# with content_type 'video', only if the video was already in the watched
|
||||||
|
# list for the current user.
|
||||||
|
if node_id in view_progress:
|
||||||
|
node['view_progress'] = view_progress[node_id]
|
||||||
|
|
||||||
|
return jsonify(agg_list)
|
||||||
|
|
||||||
|
|
||||||
|
def _tagged(tag: str):
|
||||||
|
"""Fetch all public nodes with the given tag.
|
||||||
|
|
||||||
|
This function is cached, see setup_app().
|
||||||
|
"""
|
||||||
|
nodes_coll = current_app.db('nodes')
|
||||||
|
agg = nodes_coll.aggregate([
|
||||||
|
{'$match': {'properties.tags': tag,
|
||||||
|
'_deleted': {'$ne': True}}},
|
||||||
|
|
||||||
|
# Only get nodes from public projects. This is done after matching the
|
||||||
|
# tagged nodes, because most likely nobody else will be able to tag
|
||||||
|
# nodes anyway.
|
||||||
|
{'$lookup': {
|
||||||
|
'from': 'projects',
|
||||||
|
'localField': 'project',
|
||||||
|
'foreignField': '_id',
|
||||||
|
'as': '_project',
|
||||||
|
}},
|
||||||
|
{'$match': {'_project.is_private': False}},
|
||||||
|
|
||||||
|
# Don't return the entire project for each node.
|
||||||
|
{'$project': {'_project': False}},
|
||||||
|
|
||||||
|
{'$sort': {'_created': -1}}
|
||||||
|
])
|
||||||
|
|
||||||
|
return list(agg)
|
||||||
|
|
||||||
|
|
||||||
def generate_and_store_short_code(node):
|
def generate_and_store_short_code(node):
|
||||||
@@ -164,307 +185,35 @@ def create_short_code(node) -> str:
|
|||||||
return short_code
|
return short_code
|
||||||
|
|
||||||
|
|
||||||
def short_link_info(short_code):
|
|
||||||
"""Returns the short link info in a dict."""
|
|
||||||
|
|
||||||
short_link = urllib.parse.urljoin(
|
|
||||||
current_app.config['SHORT_LINK_BASE_URL'], short_code)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'short_code': short_code,
|
|
||||||
'short_link': short_link,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def before_replacing_node(item, original):
|
|
||||||
check_permissions('nodes', original, 'PUT')
|
|
||||||
update_file_name(item)
|
|
||||||
|
|
||||||
|
|
||||||
def after_replacing_node(item, original):
|
|
||||||
"""Push an update to the Algolia index when a node item is updated. If the
|
|
||||||
project is private, prevent public indexing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pillar.celery import search_index_tasks as index
|
|
||||||
|
|
||||||
projects_collection = current_app.data.driver.db['projects']
|
|
||||||
project = projects_collection.find_one({'_id': item['project']})
|
|
||||||
if project.get('is_private', False):
|
|
||||||
# Skip index updating and return
|
|
||||||
return
|
|
||||||
|
|
||||||
status = item['properties'].get('status', 'unpublished')
|
|
||||||
node_id = str(item['_id'])
|
|
||||||
|
|
||||||
if status == 'published':
|
|
||||||
index.node_save.delay(node_id)
|
|
||||||
else:
|
|
||||||
index.node_delete.delay(node_id)
|
|
||||||
|
|
||||||
|
|
||||||
def before_inserting_nodes(items):
|
|
||||||
"""Before inserting a node in the collection we check if the user is allowed
|
|
||||||
and we append the project id to it.
|
|
||||||
"""
|
|
||||||
from pillar.auth import current_user
|
|
||||||
|
|
||||||
nodes_collection = current_app.data.driver.db['nodes']
|
|
||||||
|
|
||||||
def find_parent_project(node):
|
|
||||||
"""Recursive function that finds the ultimate parent of a node."""
|
|
||||||
if node and 'parent' in node:
|
|
||||||
parent = nodes_collection.find_one({'_id': node['parent']})
|
|
||||||
return find_parent_project(parent)
|
|
||||||
if node:
|
|
||||||
return node
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
check_permissions('nodes', item, 'POST')
|
|
||||||
if 'parent' in item and 'project' not in item:
|
|
||||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
|
||||||
project = find_parent_project(parent)
|
|
||||||
if project:
|
|
||||||
item['project'] = project['_id']
|
|
||||||
|
|
||||||
# Default the 'user' property to the current user.
|
|
||||||
item.setdefault('user', current_user.user_id)
|
|
||||||
|
|
||||||
|
|
||||||
def after_inserting_nodes(items):
|
|
||||||
for item in items:
|
|
||||||
# Skip subscriptions for first level items (since the context is not a
|
|
||||||
# node, but a project).
|
|
||||||
# TODO: support should be added for mixed context
|
|
||||||
if 'parent' not in item:
|
|
||||||
return
|
|
||||||
context_object_id = item['parent']
|
|
||||||
if item['node_type'] == 'comment':
|
|
||||||
nodes_collection = current_app.data.driver.db['nodes']
|
|
||||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
|
||||||
# Always subscribe to the parent node
|
|
||||||
activity_subscribe(item['user'], 'node', item['parent'])
|
|
||||||
if parent['node_type'] == 'comment':
|
|
||||||
# If the parent is a comment, we provide its own parent as
|
|
||||||
# context. We do this in order to point the user to an asset
|
|
||||||
# or group when viewing the notification.
|
|
||||||
verb = 'replied'
|
|
||||||
context_object_id = parent['parent']
|
|
||||||
# Subscribe to the parent of the parent comment (post or group)
|
|
||||||
activity_subscribe(item['user'], 'node', parent['parent'])
|
|
||||||
else:
|
|
||||||
activity_subscribe(item['user'], 'node', item['_id'])
|
|
||||||
verb = 'commented'
|
|
||||||
elif item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
|
||||||
verb = 'posted'
|
|
||||||
activity_subscribe(item['user'], 'node', item['_id'])
|
|
||||||
else:
|
|
||||||
# Don't automatically create activities for non-Pillar node types,
|
|
||||||
# as we don't know what would be a suitable verb (among other things).
|
|
||||||
continue
|
|
||||||
|
|
||||||
activity_object_add(
|
|
||||||
item['user'],
|
|
||||||
verb,
|
|
||||||
'node',
|
|
||||||
item['_id'],
|
|
||||||
'node',
|
|
||||||
context_object_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def deduct_content_type(node_doc, original=None):
|
|
||||||
"""Deduct the content type from the attached file, if any."""
|
|
||||||
|
|
||||||
if node_doc['node_type'] != 'asset':
|
|
||||||
log.debug('deduct_content_type: called on node type %r, ignoring', node_doc['node_type'])
|
|
||||||
return
|
|
||||||
|
|
||||||
node_id = node_doc.get('_id')
|
|
||||||
try:
|
|
||||||
file_id = ObjectId(node_doc['properties']['file'])
|
|
||||||
except KeyError:
|
|
||||||
if node_id is None:
|
|
||||||
# Creation of a file-less node is allowed, but updates aren't.
|
|
||||||
return
|
|
||||||
log.warning('deduct_content_type: Asset without properties.file, rejecting.')
|
|
||||||
raise wz_exceptions.UnprocessableEntity('Missing file property for asset node')
|
|
||||||
|
|
||||||
files = current_app.data.driver.db['files']
|
|
||||||
file_doc = files.find_one({'_id': file_id},
|
|
||||||
{'content_type': 1})
|
|
||||||
if not file_doc:
|
|
||||||
log.warning('deduct_content_type: Node %s refers to non-existing file %s, rejecting.',
|
|
||||||
node_id, file_id)
|
|
||||||
raise wz_exceptions.UnprocessableEntity('File property refers to non-existing file')
|
|
||||||
|
|
||||||
# Guess the node content type from the file content type
|
|
||||||
file_type = file_doc['content_type']
|
|
||||||
if file_type.startswith('video/'):
|
|
||||||
content_type = 'video'
|
|
||||||
elif file_type.startswith('image/'):
|
|
||||||
content_type = 'image'
|
|
||||||
else:
|
|
||||||
content_type = 'file'
|
|
||||||
|
|
||||||
node_doc['properties']['content_type'] = content_type
|
|
||||||
|
|
||||||
|
|
||||||
def nodes_deduct_content_type(nodes):
|
|
||||||
for node in nodes:
|
|
||||||
deduct_content_type(node)
|
|
||||||
|
|
||||||
|
|
||||||
def before_returning_node(node):
|
|
||||||
# Run validation process, since GET on nodes entry point is public
|
|
||||||
check_permissions('nodes', node, 'GET', append_allowed_methods=True)
|
|
||||||
|
|
||||||
# Embed short_link_info if the node has a short_code.
|
|
||||||
short_code = node.get('short_code')
|
|
||||||
if short_code:
|
|
||||||
node['short_link'] = short_link_info(short_code)['short_link']
|
|
||||||
|
|
||||||
|
|
||||||
def before_returning_nodes(nodes):
|
|
||||||
for node in nodes['_items']:
|
|
||||||
before_returning_node(node)
|
|
||||||
|
|
||||||
|
|
||||||
def node_set_default_picture(node, original=None):
|
|
||||||
"""Uses the image of an image asset or colour map of texture node as picture."""
|
|
||||||
|
|
||||||
if node.get('picture'):
|
|
||||||
log.debug('Node %s already has a picture, not overriding', node.get('_id'))
|
|
||||||
return
|
|
||||||
|
|
||||||
node_type = node.get('node_type')
|
|
||||||
props = node.get('properties', {})
|
|
||||||
content = props.get('content_type')
|
|
||||||
|
|
||||||
if node_type == 'asset' and content == 'image':
|
|
||||||
image_file_id = props.get('file')
|
|
||||||
elif node_type == 'texture':
|
|
||||||
# Find the colour map, defaulting to the first image map available.
|
|
||||||
image_file_id = None
|
|
||||||
for image in props.get('files', []):
|
|
||||||
if image_file_id is None or image.get('map_type') == 'color':
|
|
||||||
image_file_id = image.get('file')
|
|
||||||
else:
|
|
||||||
log.debug('Not setting default picture on node type %s content type %s',
|
|
||||||
node_type, content)
|
|
||||||
return
|
|
||||||
|
|
||||||
if image_file_id is None:
|
|
||||||
log.debug('Nothing to set the picture to.')
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug('Setting default picture for node %s to %s', node.get('_id'), image_file_id)
|
|
||||||
node['picture'] = image_file_id
|
|
||||||
|
|
||||||
|
|
||||||
def nodes_set_default_picture(nodes):
|
|
||||||
for node in nodes:
|
|
||||||
node_set_default_picture(node)
|
|
||||||
|
|
||||||
|
|
||||||
def before_deleting_node(node: dict):
|
|
||||||
check_permissions('nodes', node, 'DELETE')
|
|
||||||
|
|
||||||
|
|
||||||
def after_deleting_node(item):
|
|
||||||
from pillar.celery import search_index_tasks as index
|
|
||||||
index.node_delete.delay(str(item['_id']))
|
|
||||||
|
|
||||||
|
|
||||||
only_for_textures = only_for_node_type_decorator('texture')
|
|
||||||
|
|
||||||
|
|
||||||
@only_for_textures
|
|
||||||
def texture_sort_files(node, original=None):
|
|
||||||
"""Sort files alphabetically by map type, with colour map first."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
files = node['properties']['files']
|
|
||||||
except KeyError:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sort the map types alphabetically, ensuring 'color' comes first.
|
|
||||||
as_dict = {f['map_type']: f for f in files}
|
|
||||||
types = sorted(as_dict.keys(), key=lambda k: '\0' if k == 'color' else k)
|
|
||||||
node['properties']['files'] = [as_dict[map_type] for map_type in types]
|
|
||||||
|
|
||||||
|
|
||||||
def textures_sort_files(nodes):
|
|
||||||
for node in nodes:
|
|
||||||
texture_sort_files(node)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_markdown(node, original=None):
|
|
||||||
import copy
|
|
||||||
|
|
||||||
projects_collection = current_app.data.driver.db['projects']
|
|
||||||
project = projects_collection.find_one({'_id': node['project']}, {'node_types': 1})
|
|
||||||
# Query node type directly using the key
|
|
||||||
node_type = next(nt for nt in project['node_types']
|
|
||||||
if nt['name'] == node['node_type'])
|
|
||||||
|
|
||||||
# Create a copy to not overwrite the actual schema.
|
|
||||||
schema = copy.deepcopy(current_app.config['DOMAIN']['nodes']['schema'])
|
|
||||||
schema['properties'] = node_type['dyn_schema']
|
|
||||||
|
|
||||||
def find_markdown_fields(schema, node):
|
|
||||||
"""Find and process all makrdown validated fields."""
|
|
||||||
for k, v in schema.items():
|
|
||||||
if not isinstance(v, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if v.get('validator') == 'markdown':
|
|
||||||
# If there is a match with the validator: markdown pair, assign the sibling
|
|
||||||
# property (following the naming convention _<property>_html)
|
|
||||||
# the processed value.
|
|
||||||
if k in node:
|
|
||||||
html = pillar.markdown.markdown(node[k])
|
|
||||||
field_name = pillar.markdown.cache_field_name(k)
|
|
||||||
node[field_name] = html
|
|
||||||
if isinstance(node, dict) and k in node:
|
|
||||||
find_markdown_fields(v, node[k])
|
|
||||||
|
|
||||||
find_markdown_fields(schema, node)
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
|
|
||||||
def parse_markdowns(items):
|
|
||||||
for item in items:
|
|
||||||
parse_markdown(item)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_app(app, url_prefix):
|
def setup_app(app, url_prefix):
|
||||||
|
global _tagged
|
||||||
|
|
||||||
|
cached = app.cache.memoize(timeout=300)
|
||||||
|
_tagged = cached(_tagged)
|
||||||
|
|
||||||
from . import patch
|
from . import patch
|
||||||
patch.setup_app(app, url_prefix=url_prefix)
|
patch.setup_app(app, url_prefix=url_prefix)
|
||||||
|
|
||||||
app.on_fetched_item_nodes += before_returning_node
|
app.on_fetched_item_nodes += eve_hooks.before_returning_node
|
||||||
app.on_fetched_resource_nodes += before_returning_nodes
|
app.on_fetched_resource_nodes += eve_hooks.before_returning_nodes
|
||||||
|
|
||||||
app.on_replace_nodes += before_replacing_node
|
app.on_replace_nodes += eve_hooks.before_replacing_node
|
||||||
app.on_replace_nodes += parse_markdown
|
app.on_replace_nodes += eve_hooks.parse_markdown
|
||||||
app.on_replace_nodes += texture_sort_files
|
app.on_replace_nodes += eve_hooks.texture_sort_files
|
||||||
app.on_replace_nodes += deduct_content_type
|
app.on_replace_nodes += eve_hooks.deduct_content_type
|
||||||
app.on_replace_nodes += node_set_default_picture
|
app.on_replace_nodes += eve_hooks.node_set_default_picture
|
||||||
app.on_replaced_nodes += after_replacing_node
|
app.on_replaced_nodes += eve_hooks.after_replacing_node
|
||||||
|
|
||||||
app.on_insert_nodes += before_inserting_nodes
|
app.on_insert_nodes += eve_hooks.before_inserting_nodes
|
||||||
app.on_insert_nodes += parse_markdowns
|
app.on_insert_nodes += eve_hooks.parse_markdowns
|
||||||
app.on_insert_nodes += nodes_deduct_content_type
|
app.on_insert_nodes += eve_hooks.nodes_deduct_content_type
|
||||||
app.on_insert_nodes += nodes_set_default_picture
|
app.on_insert_nodes += eve_hooks.nodes_set_default_picture
|
||||||
app.on_insert_nodes += textures_sort_files
|
app.on_insert_nodes += eve_hooks.textures_sort_files
|
||||||
app.on_inserted_nodes += after_inserting_nodes
|
app.on_inserted_nodes += eve_hooks.after_inserting_nodes
|
||||||
|
|
||||||
app.on_update_nodes += texture_sort_files
|
app.on_update_nodes += eve_hooks.texture_sort_files
|
||||||
|
|
||||||
app.on_delete_item_nodes += before_deleting_node
|
app.on_delete_item_nodes += eve_hooks.before_deleting_node
|
||||||
app.on_deleted_item_nodes += after_deleting_node
|
app.on_deleted_item_nodes += eve_hooks.after_deleting_node
|
||||||
|
|
||||||
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|
||||||
|
325
pillar/api/nodes/eve_hooks.py
Normal file
325
pillar/api/nodes/eve_hooks.py
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
from bson import ObjectId
|
||||||
|
from flask import current_app
|
||||||
|
from werkzeug import exceptions as wz_exceptions
|
||||||
|
|
||||||
|
import pillar.markdown
|
||||||
|
from pillar.api.activities import activity_subscribe, activity_object_add
|
||||||
|
from pillar.api.file_storage_backends.gcs import update_file_name
|
||||||
|
from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES
|
||||||
|
from pillar.api.utils.authorization import check_permissions
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def before_returning_node(node):
|
||||||
|
# Run validation process, since GET on nodes entry point is public
|
||||||
|
check_permissions('nodes', node, 'GET', append_allowed_methods=True)
|
||||||
|
|
||||||
|
# Embed short_link_info if the node has a short_code.
|
||||||
|
short_code = node.get('short_code')
|
||||||
|
if short_code:
|
||||||
|
node['short_link'] = short_link_info(short_code)['short_link']
|
||||||
|
|
||||||
|
|
||||||
|
def before_returning_nodes(nodes):
|
||||||
|
for node in nodes['_items']:
|
||||||
|
before_returning_node(node)
|
||||||
|
|
||||||
|
|
||||||
|
def only_for_node_type_decorator(*required_node_type_names):
|
||||||
|
"""Returns a decorator that checks its first argument's node type.
|
||||||
|
|
||||||
|
If the node type is not of the required node type, returns None,
|
||||||
|
otherwise calls the wrapped function.
|
||||||
|
|
||||||
|
>>> deco = only_for_node_type_decorator('comment')
|
||||||
|
>>> @deco
|
||||||
|
... def handle_comment(node): pass
|
||||||
|
|
||||||
|
>>> deco = only_for_node_type_decorator('comment', 'post')
|
||||||
|
>>> @deco
|
||||||
|
... def handle_comment_or_post(node): pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Convert to a set for efficient 'x in required_node_type_names' queries.
|
||||||
|
required_node_type_names = set(required_node_type_names)
|
||||||
|
|
||||||
|
def only_for_node_type(wrapped):
|
||||||
|
@functools.wraps(wrapped)
|
||||||
|
def wrapper(node, *args, **kwargs):
|
||||||
|
if node.get('node_type') not in required_node_type_names:
|
||||||
|
return
|
||||||
|
|
||||||
|
return wrapped(node, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
only_for_node_type.__doc__ = "Decorator, immediately returns when " \
|
||||||
|
"the first argument is not of type %s." % required_node_type_names
|
||||||
|
return only_for_node_type
|
||||||
|
|
||||||
|
|
||||||
|
def before_replacing_node(item, original):
|
||||||
|
check_permissions('nodes', original, 'PUT')
|
||||||
|
update_file_name(item)
|
||||||
|
|
||||||
|
|
||||||
|
def after_replacing_node(item, original):
|
||||||
|
"""Push an update to the Algolia index when a node item is updated. If the
|
||||||
|
project is private, prevent public indexing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pillar.celery import search_index_tasks as index
|
||||||
|
|
||||||
|
projects_collection = current_app.data.driver.db['projects']
|
||||||
|
project = projects_collection.find_one({'_id': item['project']})
|
||||||
|
if project.get('is_private', False):
|
||||||
|
# Skip index updating and return
|
||||||
|
return
|
||||||
|
|
||||||
|
status = item['properties'].get('status', 'unpublished')
|
||||||
|
node_id = str(item['_id'])
|
||||||
|
|
||||||
|
if status == 'published':
|
||||||
|
index.node_save.delay(node_id)
|
||||||
|
else:
|
||||||
|
index.node_delete.delay(node_id)
|
||||||
|
|
||||||
|
|
||||||
|
def before_inserting_nodes(items):
|
||||||
|
"""Before inserting a node in the collection we check if the user is allowed
|
||||||
|
and we append the project id to it.
|
||||||
|
"""
|
||||||
|
from pillar.auth import current_user
|
||||||
|
|
||||||
|
nodes_collection = current_app.data.driver.db['nodes']
|
||||||
|
|
||||||
|
def find_parent_project(node):
|
||||||
|
"""Recursive function that finds the ultimate parent of a node."""
|
||||||
|
if node and 'parent' in node:
|
||||||
|
parent = nodes_collection.find_one({'_id': node['parent']})
|
||||||
|
return find_parent_project(parent)
|
||||||
|
if node:
|
||||||
|
return node
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
check_permissions('nodes', item, 'POST')
|
||||||
|
if 'parent' in item and 'project' not in item:
|
||||||
|
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||||
|
project = find_parent_project(parent)
|
||||||
|
if project:
|
||||||
|
item['project'] = project['_id']
|
||||||
|
|
||||||
|
# Default the 'user' property to the current user.
|
||||||
|
item.setdefault('user', current_user.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def after_inserting_nodes(items):
|
||||||
|
for item in items:
|
||||||
|
# Skip subscriptions for first level items (since the context is not a
|
||||||
|
# node, but a project).
|
||||||
|
# TODO: support should be added for mixed context
|
||||||
|
if 'parent' not in item:
|
||||||
|
return
|
||||||
|
context_object_id = item['parent']
|
||||||
|
if item['node_type'] == 'comment':
|
||||||
|
nodes_collection = current_app.data.driver.db['nodes']
|
||||||
|
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||||
|
# Always subscribe to the parent node
|
||||||
|
activity_subscribe(item['user'], 'node', item['parent'])
|
||||||
|
if parent['node_type'] == 'comment':
|
||||||
|
# If the parent is a comment, we provide its own parent as
|
||||||
|
# context. We do this in order to point the user to an asset
|
||||||
|
# or group when viewing the notification.
|
||||||
|
verb = 'replied'
|
||||||
|
context_object_id = parent['parent']
|
||||||
|
# Subscribe to the parent of the parent comment (post or group)
|
||||||
|
activity_subscribe(item['user'], 'node', parent['parent'])
|
||||||
|
else:
|
||||||
|
activity_subscribe(item['user'], 'node', item['_id'])
|
||||||
|
verb = 'commented'
|
||||||
|
elif item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
||||||
|
verb = 'posted'
|
||||||
|
activity_subscribe(item['user'], 'node', item['_id'])
|
||||||
|
else:
|
||||||
|
# Don't automatically create activities for non-Pillar node types,
|
||||||
|
# as we don't know what would be a suitable verb (among other things).
|
||||||
|
continue
|
||||||
|
|
||||||
|
activity_object_add(
|
||||||
|
item['user'],
|
||||||
|
verb,
|
||||||
|
'node',
|
||||||
|
item['_id'],
|
||||||
|
'node',
|
||||||
|
context_object_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deduct_content_type(node_doc, original=None):
|
||||||
|
"""Deduct the content type from the attached file, if any."""
|
||||||
|
|
||||||
|
if node_doc['node_type'] != 'asset':
|
||||||
|
log.debug('deduct_content_type: called on node type %r, ignoring', node_doc['node_type'])
|
||||||
|
return
|
||||||
|
|
||||||
|
node_id = node_doc.get('_id')
|
||||||
|
try:
|
||||||
|
file_id = ObjectId(node_doc['properties']['file'])
|
||||||
|
except KeyError:
|
||||||
|
if node_id is None:
|
||||||
|
# Creation of a file-less node is allowed, but updates aren't.
|
||||||
|
return
|
||||||
|
log.warning('deduct_content_type: Asset without properties.file, rejecting.')
|
||||||
|
raise wz_exceptions.UnprocessableEntity('Missing file property for asset node')
|
||||||
|
|
||||||
|
files = current_app.data.driver.db['files']
|
||||||
|
file_doc = files.find_one({'_id': file_id},
|
||||||
|
{'content_type': 1})
|
||||||
|
if not file_doc:
|
||||||
|
log.warning('deduct_content_type: Node %s refers to non-existing file %s, rejecting.',
|
||||||
|
node_id, file_id)
|
||||||
|
raise wz_exceptions.UnprocessableEntity('File property refers to non-existing file')
|
||||||
|
|
||||||
|
# Guess the node content type from the file content type
|
||||||
|
file_type = file_doc['content_type']
|
||||||
|
if file_type.startswith('video/'):
|
||||||
|
content_type = 'video'
|
||||||
|
elif file_type.startswith('image/'):
|
||||||
|
content_type = 'image'
|
||||||
|
else:
|
||||||
|
content_type = 'file'
|
||||||
|
|
||||||
|
node_doc['properties']['content_type'] = content_type
|
||||||
|
|
||||||
|
|
||||||
|
def nodes_deduct_content_type(nodes):
|
||||||
|
for node in nodes:
|
||||||
|
deduct_content_type(node)
|
||||||
|
|
||||||
|
|
||||||
|
def node_set_default_picture(node, original=None):
|
||||||
|
"""Uses the image of an image asset or colour map of texture node as picture."""
|
||||||
|
|
||||||
|
if node.get('picture'):
|
||||||
|
log.debug('Node %s already has a picture, not overriding', node.get('_id'))
|
||||||
|
return
|
||||||
|
|
||||||
|
node_type = node.get('node_type')
|
||||||
|
props = node.get('properties', {})
|
||||||
|
content = props.get('content_type')
|
||||||
|
|
||||||
|
if node_type == 'asset' and content == 'image':
|
||||||
|
image_file_id = props.get('file')
|
||||||
|
elif node_type == 'texture':
|
||||||
|
# Find the colour map, defaulting to the first image map available.
|
||||||
|
image_file_id = None
|
||||||
|
for image in props.get('files', []):
|
||||||
|
if image_file_id is None or image.get('map_type') == 'color':
|
||||||
|
image_file_id = image.get('file')
|
||||||
|
else:
|
||||||
|
log.debug('Not setting default picture on node type %s content type %s',
|
||||||
|
node_type, content)
|
||||||
|
return
|
||||||
|
|
||||||
|
if image_file_id is None:
|
||||||
|
log.debug('Nothing to set the picture to.')
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug('Setting default picture for node %s to %s', node.get('_id'), image_file_id)
|
||||||
|
node['picture'] = image_file_id
|
||||||
|
|
||||||
|
|
||||||
|
def nodes_set_default_picture(nodes):
|
||||||
|
for node in nodes:
|
||||||
|
node_set_default_picture(node)
|
||||||
|
|
||||||
|
|
||||||
|
def before_deleting_node(node: dict):
|
||||||
|
check_permissions('nodes', node, 'DELETE')
|
||||||
|
|
||||||
|
|
||||||
|
def after_deleting_node(item):
|
||||||
|
from pillar.celery import search_index_tasks as index
|
||||||
|
index.node_delete.delay(str(item['_id']))
|
||||||
|
|
||||||
|
|
||||||
|
only_for_textures = only_for_node_type_decorator('texture')
|
||||||
|
|
||||||
|
|
||||||
|
@only_for_textures
|
||||||
|
def texture_sort_files(node, original=None):
|
||||||
|
"""Sort files alphabetically by map type, with colour map first."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
files = node['properties']['files']
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort the map types alphabetically, ensuring 'color' comes first.
|
||||||
|
as_dict = {f['map_type']: f for f in files}
|
||||||
|
types = sorted(as_dict.keys(), key=lambda k: '\0' if k == 'color' else k)
|
||||||
|
node['properties']['files'] = [as_dict[map_type] for map_type in types]
|
||||||
|
|
||||||
|
|
||||||
|
def textures_sort_files(nodes):
|
||||||
|
for node in nodes:
|
||||||
|
texture_sort_files(node)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown(node, original=None):
|
||||||
|
import copy
|
||||||
|
|
||||||
|
projects_collection = current_app.data.driver.db['projects']
|
||||||
|
project = projects_collection.find_one({'_id': node['project']}, {'node_types': 1})
|
||||||
|
# Query node type directly using the key
|
||||||
|
node_type = next(nt for nt in project['node_types']
|
||||||
|
if nt['name'] == node['node_type'])
|
||||||
|
|
||||||
|
# Create a copy to not overwrite the actual schema.
|
||||||
|
schema = copy.deepcopy(current_app.config['DOMAIN']['nodes']['schema'])
|
||||||
|
schema['properties'] = node_type['dyn_schema']
|
||||||
|
|
||||||
|
def find_markdown_fields(schema, node):
|
||||||
|
"""Find and process all makrdown validated fields."""
|
||||||
|
for k, v in schema.items():
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if v.get('validator') == 'markdown':
|
||||||
|
# If there is a match with the validator: markdown pair, assign the sibling
|
||||||
|
# property (following the naming convention _<property>_html)
|
||||||
|
# the processed value.
|
||||||
|
if k in node:
|
||||||
|
html = pillar.markdown.markdown(node[k])
|
||||||
|
field_name = pillar.markdown.cache_field_name(k)
|
||||||
|
node[field_name] = html
|
||||||
|
if isinstance(node, dict) and k in node:
|
||||||
|
find_markdown_fields(v, node[k])
|
||||||
|
|
||||||
|
find_markdown_fields(schema, node)
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdowns(items):
|
||||||
|
for item in items:
|
||||||
|
parse_markdown(item)
|
||||||
|
|
||||||
|
|
||||||
|
def short_link_info(short_code):
|
||||||
|
"""Returns the short link info in a dict."""
|
||||||
|
|
||||||
|
short_link = urllib.parse.urljoin(
|
||||||
|
current_app.config['SHORT_LINK_BASE_URL'], short_code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'short_code': short_code,
|
||||||
|
'short_link': short_link,
|
||||||
|
}
|
@@ -142,7 +142,7 @@ def after_fetching_user(user):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Remove all fields except public ones.
|
# Remove all fields except public ones.
|
||||||
public_fields = {'full_name', 'username', 'email', 'extension_props_public'}
|
public_fields = {'full_name', 'username', 'email', 'extension_props_public', 'badges'}
|
||||||
for field in list(user.keys()):
|
for field in list(user.keys()):
|
||||||
if field not in public_fields:
|
if field not in public_fields:
|
||||||
del user[field]
|
del user[field]
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from eve.methods.get import get
|
from eve.methods.get import get
|
||||||
from flask import Blueprint
|
from flask import Blueprint, request
|
||||||
|
import werkzeug.exceptions as wz_exceptions
|
||||||
|
|
||||||
from pillar.api.utils import jsonify
|
from pillar import current_app
|
||||||
|
from pillar.api import utils
|
||||||
from pillar.api.utils.authorization import require_login
|
from pillar.api.utils.authorization import require_login
|
||||||
from pillar.auth import current_user
|
from pillar.auth import current_user
|
||||||
|
|
||||||
@@ -15,7 +17,128 @@ blueprint_api = Blueprint('users_api', __name__)
|
|||||||
@require_login()
|
@require_login()
|
||||||
def my_info():
|
def my_info():
|
||||||
eve_resp, _, _, status, _ = get('users', {'_id': current_user.user_id})
|
eve_resp, _, _, status, _ = get('users', {'_id': current_user.user_id})
|
||||||
resp = jsonify(eve_resp['_items'][0], status=status)
|
resp = utils.jsonify(eve_resp['_items'][0], status=status)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint_api.route('/video/<video_id>/progress')
|
||||||
|
@require_login()
|
||||||
|
def get_video_progress(video_id: str):
|
||||||
|
"""Return video progress information.
|
||||||
|
|
||||||
|
Either a `204 No Content` is returned (no information stored),
|
||||||
|
or a `200 Ok` with JSON from Eve's 'users' schema, from the key
|
||||||
|
video.view_progress.<video_id>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Validation of the video ID; raises a BadRequest when it's not an ObjectID.
|
||||||
|
# This isn't strictly necessary, but it makes this function behave symmetrical
|
||||||
|
# to the set_video_progress() function.
|
||||||
|
utils.str2id(video_id)
|
||||||
|
|
||||||
|
users_coll = current_app.db('users')
|
||||||
|
user_doc = users_coll.find_one(current_user.user_id, projection={'nodes.view_progress': True})
|
||||||
|
try:
|
||||||
|
progress = user_doc['nodes']['view_progress'][video_id]
|
||||||
|
except KeyError:
|
||||||
|
return '', 204
|
||||||
|
if not progress:
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
return utils.jsonify(progress)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint_api.route('/video/<video_id>/progress', methods=['POST'])
|
||||||
|
@require_login()
|
||||||
|
def set_video_progress(video_id: str):
|
||||||
|
"""Save progress information about a certain video.
|
||||||
|
|
||||||
|
Expected parameters:
|
||||||
|
- progress_in_sec: float number of seconds
|
||||||
|
- progress_in_perc: integer percentage of video watched (interval [0-100])
|
||||||
|
"""
|
||||||
|
my_log = log.getChild('set_video_progress')
|
||||||
|
my_log.debug('Setting video progress for user %r video %r', current_user.user_id, video_id)
|
||||||
|
|
||||||
|
# Constructing this response requires an active app, and thus can't be done on module load.
|
||||||
|
no_video_response = utils.jsonify({'_message': 'No such video'}, status=404)
|
||||||
|
|
||||||
|
try:
|
||||||
|
progress_in_sec = float(request.form['progress_in_sec'])
|
||||||
|
progress_in_perc = int(request.form['progress_in_perc'])
|
||||||
|
except KeyError as ex:
|
||||||
|
my_log.debug('Missing POST field in request: %s', ex)
|
||||||
|
raise wz_exceptions.BadRequest(f'missing a form field')
|
||||||
|
except ValueError as ex:
|
||||||
|
my_log.debug('Invalid value for POST field in request: %s', ex)
|
||||||
|
raise wz_exceptions.BadRequest(f'Invalid value for field: {ex}')
|
||||||
|
|
||||||
|
users_coll = current_app.db('users')
|
||||||
|
nodes_coll = current_app.db('nodes')
|
||||||
|
|
||||||
|
# First check whether this is actually an existing video
|
||||||
|
video_oid = utils.str2id(video_id)
|
||||||
|
video_doc = nodes_coll.find_one(video_oid, projection={
|
||||||
|
'node_type': True,
|
||||||
|
'properties.content_type': True,
|
||||||
|
'properties.file': True,
|
||||||
|
})
|
||||||
|
if not video_doc:
|
||||||
|
my_log.debug('Node %r not found, unable to set progress for user %r',
|
||||||
|
video_oid, current_user.user_id)
|
||||||
|
return no_video_response
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_video = (video_doc['node_type'] == 'asset'
|
||||||
|
and video_doc['properties']['content_type'] == 'video')
|
||||||
|
except KeyError:
|
||||||
|
is_video = False
|
||||||
|
|
||||||
|
if not is_video:
|
||||||
|
my_log.info('Node %r is not a video, unable to set progress for user %r',
|
||||||
|
video_oid, current_user.user_id)
|
||||||
|
# There is no video found at this URL, so act as if it doesn't even exist.
|
||||||
|
return no_video_response
|
||||||
|
|
||||||
|
# Compute the progress
|
||||||
|
percent = min(100, max(0, progress_in_perc))
|
||||||
|
progress = {
|
||||||
|
'progress_in_sec': progress_in_sec,
|
||||||
|
'progress_in_percent': percent,
|
||||||
|
'last_watched': utils.utcnow(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# After watching a certain percentage of the video, we consider it 'done'
|
||||||
|
#
|
||||||
|
# Total Credit start Total Credit Percent
|
||||||
|
# HH:MM:SS HH:MM:SS sec sec of duration
|
||||||
|
# Sintel 00:14:48 00:12:24 888 744 83.78%
|
||||||
|
# Tears of Steel 00:12:14 00:09:49 734 589 80.25%
|
||||||
|
# Cosmos Laundro 00:12:10 00:10:05 730 605 82.88%
|
||||||
|
# Agent 327 00:03:51 00:03:26 231 206 89.18%
|
||||||
|
# Caminandes 3 00:02:30 00:02:18 150 138 92.00%
|
||||||
|
# Glass Half 00:03:13 00:02:52 193 172 89.12%
|
||||||
|
# Big Buck Bunny 00:09:56 00:08:11 596 491 82.38%
|
||||||
|
# Elephant’s Drea 00:10:54 00:09:25 654 565 86.39%
|
||||||
|
#
|
||||||
|
# Median 85.09%
|
||||||
|
# Average 85.75%
|
||||||
|
#
|
||||||
|
# For training videos marking at done at 85% of the video may be a bit
|
||||||
|
# early, since those probably won't have (long) credits. This is why we
|
||||||
|
# stick to 90% here.
|
||||||
|
if percent >= 90:
|
||||||
|
progress['done'] = True
|
||||||
|
|
||||||
|
# Setting each property individually prevents us from overwriting any
|
||||||
|
# existing {done: true} fields.
|
||||||
|
updates = {f'nodes.view_progress.{video_id}.{k}': v
|
||||||
|
for k, v in progress.items()}
|
||||||
|
result = users_coll.update_one({'_id': current_user.user_id},
|
||||||
|
{'$set': updates})
|
||||||
|
|
||||||
|
if result.matched_count == 0:
|
||||||
|
my_log.error('Current user %r could not be updated', current_user.user_id)
|
||||||
|
raise wz_exceptions.InternalServerError('Unable to find logged-in user')
|
||||||
|
|
||||||
|
return '', 204
|
||||||
|
@@ -245,4 +245,10 @@ def random_etag() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def utcnow() -> datetime.datetime:
|
def utcnow() -> datetime.datetime:
|
||||||
return datetime.datetime.now(tz=bson.tz_util.utc)
|
"""Construct timezone-aware 'now' in UTC with millisecond precision."""
|
||||||
|
now = datetime.datetime.now(tz=bson.tz_util.utc)
|
||||||
|
|
||||||
|
# MongoDB stores in millisecond precision, so truncate the microseconds.
|
||||||
|
# This way the returned datetime can be round-tripped via MongoDB and stay the same.
|
||||||
|
trunc_now = now.replace(microsecond=now.microsecond - (now.microsecond % 1000))
|
||||||
|
return trunc_now
|
||||||
|
@@ -13,7 +13,7 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
import bson
|
import bson
|
||||||
from flask import g, current_app
|
from flask import g, current_app, session
|
||||||
from flask import request
|
from flask import request
|
||||||
from werkzeug import exceptions as wz_exceptions
|
from werkzeug import exceptions as wz_exceptions
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ def find_user_in_db(user_info: dict, provider='blender-id') -> dict:
|
|||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
|
|
||||||
def validate_token(*, force=False):
|
def validate_token(*, force=False) -> bool:
|
||||||
"""Validate the token provided in the request and populate the current_user
|
"""Validate the token provided in the request and populate the current_user
|
||||||
flask.g object, so that permissions and access to a resource can be defined
|
flask.g object, so that permissions and access to a resource can be defined
|
||||||
from it.
|
from it.
|
||||||
@@ -115,7 +115,7 @@ def validate_token(*, force=False):
|
|||||||
:returns: True iff the user is logged in with a valid Blender ID token.
|
:returns: True iff the user is logged in with a valid Blender ID token.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pillar.auth import AnonymousUser
|
import pillar.auth
|
||||||
|
|
||||||
# Trust a pre-existing g.current_user
|
# Trust a pre-existing g.current_user
|
||||||
if not force:
|
if not force:
|
||||||
@@ -133,16 +133,22 @@ def validate_token(*, force=False):
|
|||||||
oauth_subclient = ''
|
oauth_subclient = ''
|
||||||
else:
|
else:
|
||||||
# Check the session, the user might be logged in through Flask-Login.
|
# Check the session, the user might be logged in through Flask-Login.
|
||||||
from pillar import auth
|
|
||||||
|
|
||||||
token = auth.get_blender_id_oauth_token()
|
# The user has a logged-in session; trust only if this request passes a CSRF check.
|
||||||
|
# FIXME(Sybren): we should stop saving the token as 'user_id' in the sesion.
|
||||||
|
token = session.get('user_id')
|
||||||
|
if token:
|
||||||
|
log.debug('skipping token check because current user already has a session')
|
||||||
|
current_app.csrf.protect()
|
||||||
|
else:
|
||||||
|
token = pillar.auth.get_blender_id_oauth_token()
|
||||||
oauth_subclient = None
|
oauth_subclient = None
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
# If no authorization headers are provided, we are getting a request
|
# If no authorization headers are provided, we are getting a request
|
||||||
# from a non logged in user. Proceed accordingly.
|
# from a non logged in user. Proceed accordingly.
|
||||||
log.debug('No authentication headers, so not logged in.')
|
log.debug('No authentication headers, so not logged in.')
|
||||||
g.current_user = AnonymousUser()
|
g.current_user = pillar.auth.AnonymousUser()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return validate_this_token(token, oauth_subclient) is not None
|
return validate_this_token(token, oauth_subclient) is not None
|
||||||
@@ -194,7 +200,7 @@ def remove_token(token: str):
|
|||||||
tokens_coll = current_app.db('tokens')
|
tokens_coll = current_app.db('tokens')
|
||||||
token_hashed = hash_auth_token(token)
|
token_hashed = hash_auth_token(token)
|
||||||
|
|
||||||
# TODO: remove matching on unhashed tokens once all tokens have been hashed.
|
# TODO: remove matching on hashed tokens once all hashed tokens have expired.
|
||||||
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]}
|
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]}
|
||||||
del_res = tokens_coll.delete_many(lookup)
|
del_res = tokens_coll.delete_many(lookup)
|
||||||
log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count)
|
log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count)
|
||||||
@@ -206,7 +212,7 @@ def find_token(token, is_subclient_token=False, **extra_filters):
|
|||||||
tokens_coll = current_app.db('tokens')
|
tokens_coll = current_app.db('tokens')
|
||||||
token_hashed = hash_auth_token(token)
|
token_hashed = hash_auth_token(token)
|
||||||
|
|
||||||
# TODO: remove matching on unhashed tokens once all tokens have been hashed.
|
# TODO: remove matching on hashed tokens once all hashed tokens have expired.
|
||||||
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}],
|
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}],
|
||||||
'is_subclient_token': True if is_subclient_token else {'$in': [False, None]},
|
'is_subclient_token': True if is_subclient_token else {'$in': [False, None]},
|
||||||
'expire_time': {"$gt": utcnow()}}
|
'expire_time': {"$gt": utcnow()}}
|
||||||
@@ -229,8 +235,14 @@ def hash_auth_token(token: str) -> str:
|
|||||||
return base64.b64encode(digest).decode('ascii')
|
return base64.b64encode(digest).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
|
def store_token(user_id,
|
||||||
org_roles: typing.Set[str] = frozenset()):
|
token: str,
|
||||||
|
token_expiry,
|
||||||
|
oauth_subclient_id=False,
|
||||||
|
*,
|
||||||
|
org_roles: typing.Set[str] = frozenset(),
|
||||||
|
oauth_scopes: typing.Optional[typing.List[str]] = None,
|
||||||
|
):
|
||||||
"""Stores an authentication token.
|
"""Stores an authentication token.
|
||||||
|
|
||||||
:returns: the token document from MongoDB
|
:returns: the token document from MongoDB
|
||||||
@@ -240,13 +252,15 @@ def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
|
|||||||
|
|
||||||
token_data = {
|
token_data = {
|
||||||
'user': user_id,
|
'user': user_id,
|
||||||
'token_hashed': hash_auth_token(token),
|
'token': token,
|
||||||
'expire_time': token_expiry,
|
'expire_time': token_expiry,
|
||||||
}
|
}
|
||||||
if oauth_subclient_id:
|
if oauth_subclient_id:
|
||||||
token_data['is_subclient_token'] = True
|
token_data['is_subclient_token'] = True
|
||||||
if org_roles:
|
if org_roles:
|
||||||
token_data['org_roles'] = sorted(org_roles)
|
token_data['org_roles'] = sorted(org_roles)
|
||||||
|
if oauth_scopes:
|
||||||
|
token_data['oauth_scopes'] = oauth_scopes
|
||||||
|
|
||||||
r, _, _, status = current_app.post_internal('tokens', token_data)
|
r, _, _, status = current_app.post_internal('tokens', token_data)
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import functools
|
import functools
|
||||||
|
import typing
|
||||||
|
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
from flask import g
|
from flask import g
|
||||||
@@ -12,8 +13,9 @@ CHECK_PERMISSIONS_IMPLEMENTED_FOR = {'projects', 'nodes', 'flamenco_jobs'}
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_permissions(collection_name, resource, method, append_allowed_methods=False,
|
def check_permissions(collection_name: str, resource: dict, method: str,
|
||||||
check_node_type=None):
|
append_allowed_methods=False,
|
||||||
|
check_node_type: typing.Optional[str] = None):
|
||||||
"""Check user permissions to access a node. We look up node permissions from
|
"""Check user permissions to access a node. We look up node permissions from
|
||||||
world to groups to users and match them with the computed user permissions.
|
world to groups to users and match them with the computed user permissions.
|
||||||
If there is not match, we raise 403.
|
If there is not match, we raise 403.
|
||||||
@@ -93,8 +95,9 @@ def compute_allowed_methods(collection_name, resource, check_node_type=None):
|
|||||||
return allowed_methods
|
return allowed_methods
|
||||||
|
|
||||||
|
|
||||||
def has_permissions(collection_name, resource, method, append_allowed_methods=False,
|
def has_permissions(collection_name: str, resource: dict, method: str,
|
||||||
check_node_type=None):
|
append_allowed_methods=False,
|
||||||
|
check_node_type: typing.Optional[str] = None):
|
||||||
"""Check user permissions to access a node. We look up node permissions from
|
"""Check user permissions to access a node. We look up node permissions from
|
||||||
world to groups to users and match them with the computed user permissions.
|
world to groups to users and match them with the computed user permissions.
|
||||||
|
|
||||||
|
@@ -38,6 +38,8 @@ class UserClass(flask_login.UserMixin):
|
|||||||
self.groups: typing.List[str] = [] # NOTE: these are stringified object IDs.
|
self.groups: typing.List[str] = [] # NOTE: these are stringified object IDs.
|
||||||
self.group_ids: typing.List[bson.ObjectId] = []
|
self.group_ids: typing.List[bson.ObjectId] = []
|
||||||
self.capabilities: typing.Set[str] = set()
|
self.capabilities: typing.Set[str] = set()
|
||||||
|
self.nodes: dict = {} # see the 'nodes' key in eve_settings.py::user_schema.
|
||||||
|
self.badges_html: str = ''
|
||||||
|
|
||||||
# Lazily evaluated
|
# Lazily evaluated
|
||||||
self._has_organizations: typing.Optional[bool] = None
|
self._has_organizations: typing.Optional[bool] = None
|
||||||
@@ -56,6 +58,12 @@ class UserClass(flask_login.UserMixin):
|
|||||||
user.email = db_user.get('email') or ''
|
user.email = db_user.get('email') or ''
|
||||||
user.username = db_user.get('username') or ''
|
user.username = db_user.get('username') or ''
|
||||||
user.full_name = db_user.get('full_name') or ''
|
user.full_name = db_user.get('full_name') or ''
|
||||||
|
user.badges_html = db_user.get('badges', {}).get('html') or ''
|
||||||
|
|
||||||
|
# Be a little more specific than just db_user['nodes']
|
||||||
|
user.nodes = {
|
||||||
|
'view_progress': db_user.get('nodes', {}).get('view_progress', {}),
|
||||||
|
}
|
||||||
|
|
||||||
# Derived properties
|
# Derived properties
|
||||||
user.objectid = str(user.user_id or '')
|
user.objectid = str(user.user_id or '')
|
||||||
@@ -210,6 +218,11 @@ def login_user(oauth_token: str, *, load_from_db=False):
|
|||||||
user = _load_user(oauth_token)
|
user = _load_user(oauth_token)
|
||||||
else:
|
else:
|
||||||
user = UserClass(oauth_token)
|
user = UserClass(oauth_token)
|
||||||
|
login_user_object(user)
|
||||||
|
|
||||||
|
|
||||||
|
def login_user_object(user: UserClass):
|
||||||
|
"""Log in the given user."""
|
||||||
flask_login.login_user(user, remember=True)
|
flask_login.login_user(user, remember=True)
|
||||||
g.current_user = user
|
g.current_user = user
|
||||||
user_authenticated.send(None)
|
user_authenticated.send(None)
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import abc
|
import abc
|
||||||
import attr
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import attr
|
||||||
from rauth import OAuth2Service
|
from rauth import OAuth2Service
|
||||||
from flask import current_app, url_for, request, redirect, session, Response
|
from flask import current_app, url_for, request, redirect, session, Response
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ class OAuthUserResponse:
|
|||||||
|
|
||||||
id = attr.ib(validator=attr.validators.instance_of(str))
|
id = attr.ib(validator=attr.validators.instance_of(str))
|
||||||
email = attr.ib(validator=attr.validators.instance_of(str))
|
email = attr.ib(validator=attr.validators.instance_of(str))
|
||||||
|
access_token = attr.ib(validator=attr.validators.instance_of(str))
|
||||||
|
scopes: typing.List[str] = attr.ib(validator=attr.validators.instance_of(list))
|
||||||
|
|
||||||
|
|
||||||
class OAuthError(Exception):
|
class OAuthError(Exception):
|
||||||
@@ -127,6 +130,7 @@ class OAuthSignIn(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
class BlenderIdSignIn(OAuthSignIn):
|
class BlenderIdSignIn(OAuthSignIn):
|
||||||
provider_name = 'blender-id'
|
provider_name = 'blender-id'
|
||||||
|
scopes = ['email', 'badge']
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
@@ -140,12 +144,12 @@ class BlenderIdSignIn(OAuthSignIn):
|
|||||||
client_secret=self.consumer_secret,
|
client_secret=self.consumer_secret,
|
||||||
authorize_url=urljoin(base_url, 'oauth/authorize'),
|
authorize_url=urljoin(base_url, 'oauth/authorize'),
|
||||||
access_token_url=urljoin(base_url, 'oauth/token'),
|
access_token_url=urljoin(base_url, 'oauth/token'),
|
||||||
base_url='%s/api/' % base_url
|
base_url=urljoin(base_url, 'api/'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def authorize(self):
|
def authorize(self):
|
||||||
return redirect(self.service.get_authorize_url(
|
return redirect(self.service.get_authorize_url(
|
||||||
scope='email',
|
scope=' '.join(self.scopes),
|
||||||
response_type='code',
|
response_type='code',
|
||||||
redirect_uri=self.get_callback_url())
|
redirect_uri=self.get_callback_url())
|
||||||
)
|
)
|
||||||
@@ -159,7 +163,11 @@ class BlenderIdSignIn(OAuthSignIn):
|
|||||||
|
|
||||||
session['blender_id_oauth_token'] = access_token
|
session['blender_id_oauth_token'] = access_token
|
||||||
me = oauth_session.get('user').json()
|
me = oauth_session.get('user').json()
|
||||||
return OAuthUserResponse(str(me['id']), me['email'])
|
|
||||||
|
# Blender ID doesn't tell us which scopes were granted by the user, so
|
||||||
|
# for now assume we got all the scopes we requested.
|
||||||
|
# (see https://github.com/jazzband/django-oauth-toolkit/issues/644)
|
||||||
|
return OAuthUserResponse(str(me['id']), me['email'], access_token, self.scopes)
|
||||||
|
|
||||||
|
|
||||||
class FacebookSignIn(OAuthSignIn):
|
class FacebookSignIn(OAuthSignIn):
|
||||||
@@ -189,7 +197,7 @@ class FacebookSignIn(OAuthSignIn):
|
|||||||
me = oauth_session.get('me?fields=id,email').json()
|
me = oauth_session.get('me?fields=id,email').json()
|
||||||
# TODO handle case when user chooses not to disclose en email
|
# TODO handle case when user chooses not to disclose en email
|
||||||
# see https://developers.facebook.com/docs/graph-api/reference/user/
|
# see https://developers.facebook.com/docs/graph-api/reference/user/
|
||||||
return OAuthUserResponse(me['id'], me.get('email'))
|
return OAuthUserResponse(me['id'], me.get('email'), '', [])
|
||||||
|
|
||||||
|
|
||||||
class GoogleSignIn(OAuthSignIn):
|
class GoogleSignIn(OAuthSignIn):
|
||||||
@@ -217,4 +225,4 @@ class GoogleSignIn(OAuthSignIn):
|
|||||||
oauth_session = self.make_oauth_session()
|
oauth_session = self.make_oauth_session()
|
||||||
|
|
||||||
me = oauth_session.get('userinfo').json()
|
me = oauth_session.get('userinfo').json()
|
||||||
return OAuthUserResponse(str(me['id']), me['email'])
|
return OAuthUserResponse(str(me['id']), me['email'], '', [])
|
||||||
|
183
pillar/badge_sync.py
Normal file
183
pillar/badge_sync.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import collections
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import typing
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import bson
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from pillar import current_app
|
||||||
|
from pillar.api.utils import utcnow
|
||||||
|
|
||||||
|
SyncUser = collections.namedtuple('SyncUser', 'user_id token bid_user_id')
|
||||||
|
BadgeHTML = collections.namedtuple('BadgeHTML', 'html expires')
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StopRefreshing(Exception):
|
||||||
|
"""Indicates that Blender ID is having problems.
|
||||||
|
|
||||||
|
Further badge refreshes should be put on hold to avoid bludgeoning
|
||||||
|
a suffering Blender ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def find_users_to_sync() -> typing.Iterable[SyncUser]:
|
||||||
|
"""Return user information of syncable users with badges."""
|
||||||
|
|
||||||
|
now = utcnow()
|
||||||
|
tokens_coll = current_app.db('tokens')
|
||||||
|
cursor = tokens_coll.aggregate([
|
||||||
|
# Find all users who have a 'badge' scope in their OAuth token.
|
||||||
|
{'$match': {
|
||||||
|
'token': {'$exists': True},
|
||||||
|
'oauth_scopes': 'badge',
|
||||||
|
'expire_time': {'$gt': now},
|
||||||
|
}},
|
||||||
|
{'$lookup': {
|
||||||
|
'from': 'users',
|
||||||
|
'localField': 'user',
|
||||||
|
'foreignField': '_id',
|
||||||
|
'as': 'user'
|
||||||
|
}},
|
||||||
|
|
||||||
|
# Prevent 'user' from being an array.
|
||||||
|
{'$unwind': {'path': '$user'}},
|
||||||
|
|
||||||
|
# Get the Blender ID user ID only.
|
||||||
|
{'$unwind': {'path': '$user.auth'}},
|
||||||
|
{'$match': {'user.auth.provider': 'blender-id'}},
|
||||||
|
|
||||||
|
# Only select those users whose badge doesn't exist or has expired.
|
||||||
|
{'$match': {
|
||||||
|
'user.badges.expires': {'$not': {'$gt': now}}
|
||||||
|
}},
|
||||||
|
|
||||||
|
# Make sure that the badges that expire last are also refreshed last.
|
||||||
|
{'$sort': {'user.badges.expires': 1}},
|
||||||
|
|
||||||
|
# Reduce the document to the info we're after.
|
||||||
|
{'$project': {
|
||||||
|
'token': True,
|
||||||
|
'user._id': True,
|
||||||
|
'user.auth.user_id': True,
|
||||||
|
'user.badges.expires': True,
|
||||||
|
}},
|
||||||
|
])
|
||||||
|
|
||||||
|
log.debug('Aggregating tokens and users')
|
||||||
|
for user_info in cursor:
|
||||||
|
log.debug('User %s has badges %s',
|
||||||
|
user_info['user']['_id'], user_info['user'].get('badges'))
|
||||||
|
yield SyncUser(
|
||||||
|
user_id=user_info['user']['_id'],
|
||||||
|
token=user_info['token'],
|
||||||
|
bid_user_id=user_info['user']['auth']['user_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_badge_html(session: requests.Session, user: SyncUser, size: str) \
|
||||||
|
-> str:
|
||||||
|
"""Fetch a Blender ID badge for this user.
|
||||||
|
|
||||||
|
:param session:
|
||||||
|
:param user:
|
||||||
|
:param size: Size indication for the badge images, see the Blender ID
|
||||||
|
documentation/code. As of this writing valid sizes are {'s', 'm', 'l'}.
|
||||||
|
"""
|
||||||
|
my_log = log.getChild('fetch_badge_html')
|
||||||
|
|
||||||
|
blender_id_endpoint = current_app.config['BLENDER_ID_ENDPOINT']
|
||||||
|
url = urljoin(blender_id_endpoint, f'api/badges/{user.bid_user_id}/html/{size}')
|
||||||
|
|
||||||
|
my_log.debug('Fetching badge HTML at %s for user %s', url, user.user_id)
|
||||||
|
try:
|
||||||
|
resp = session.get(url, headers={'Authorization': f'Bearer {user.token}'})
|
||||||
|
except requests.ConnectionError as ex:
|
||||||
|
my_log.warning('Unable to connect to Blender ID at %s: %s', url, ex)
|
||||||
|
raise StopRefreshing()
|
||||||
|
|
||||||
|
if resp.status_code == 204:
|
||||||
|
my_log.debug('No badges for user %s', user.user_id)
|
||||||
|
return ''
|
||||||
|
if resp.status_code == 403:
|
||||||
|
my_log.warning('Tried fetching %s for user %s but received a 403: %s',
|
||||||
|
url, user.user_id, resp.text)
|
||||||
|
return ''
|
||||||
|
if resp.status_code == 400:
|
||||||
|
my_log.warning('Blender ID did not accept our GET request at %s for user %s: %s',
|
||||||
|
url, user.user_id, resp.text)
|
||||||
|
return ''
|
||||||
|
if resp.status_code == 500:
|
||||||
|
my_log.warning('Blender ID returned an internal server error on %s for user %s, '
|
||||||
|
'aborting all badge refreshes: %s', url, user.user_id, resp.text)
|
||||||
|
raise StopRefreshing()
|
||||||
|
if resp.status_code == 404:
|
||||||
|
my_log.warning('Blender ID has no user %s for our user %s', user.bid_user_id, user.user_id)
|
||||||
|
return ''
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.text
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_all_badges(only_user_id: typing.Optional[bson.ObjectId] = None, *,
|
||||||
|
dry_run=False,
|
||||||
|
timelimit: datetime.timedelta):
|
||||||
|
"""Re-fetch all badges for all users, except when already refreshed recently.
|
||||||
|
|
||||||
|
:param only_user_id: Only refresh this user. This is expected to be used
|
||||||
|
sparingly during manual maintenance / debugging sessions only. It does
|
||||||
|
fetch all users to refresh, and in Python code skips all except the
|
||||||
|
given one.
|
||||||
|
:param dry_run: if True the changes are described in the log, but not performed.
|
||||||
|
:param timelimit: Refreshing will stop after this time. This allows for cron(-like)
|
||||||
|
jobs to run without overlapping, even when the number fo badges to refresh
|
||||||
|
becomes larger than possible within the period of the cron job.
|
||||||
|
"""
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
my_log = log.getChild('fetch_badge_html')
|
||||||
|
|
||||||
|
# Test the config before we start looping over the world.
|
||||||
|
badge_expiry = badge_expiry_config()
|
||||||
|
if not badge_expiry or not isinstance(badge_expiry, datetime.timedelta):
|
||||||
|
raise ValueError('BLENDER_ID_BADGE_EXPIRY not configured properly, should be a timedelta')
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.mount('https://', HTTPAdapter(max_retries=5))
|
||||||
|
users_coll = current_app.db('users')
|
||||||
|
|
||||||
|
deadline = utcnow() + timelimit
|
||||||
|
|
||||||
|
num_updates = 0
|
||||||
|
for user_info in find_users_to_sync():
|
||||||
|
if utcnow() > deadline:
|
||||||
|
my_log.info('Stopping badge refresh because the timelimit %s (H:MM:SS) was hit.',
|
||||||
|
timelimit)
|
||||||
|
break
|
||||||
|
|
||||||
|
if only_user_id and user_info.user_id != only_user_id:
|
||||||
|
my_log.debug('Skipping user %s', user_info.user_id)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
badge_html = fetch_badge_html(session, user_info, 's')
|
||||||
|
except StopRefreshing:
|
||||||
|
my_log.error('Blender ID has internal problems, stopping badge refreshing at user %s',
|
||||||
|
user_info)
|
||||||
|
break
|
||||||
|
|
||||||
|
update = {'badges': {
|
||||||
|
'html': badge_html,
|
||||||
|
'expires': utcnow() + badge_expiry,
|
||||||
|
}}
|
||||||
|
num_updates += 1
|
||||||
|
my_log.info('Updating badges HTML for Blender ID %s, user %s',
|
||||||
|
user_info.bid_user_id, user_info.user_id)
|
||||||
|
if not dry_run:
|
||||||
|
result = users_coll.update_one({'_id': user_info.user_id},
|
||||||
|
{'$set': update})
|
||||||
|
if result.matched_count != 1:
|
||||||
|
my_log.warning('Unable to update badges for user %s', user_info.user_id)
|
||||||
|
my_log.info('Updated badges of %d users%s', num_updates, ' (dry-run)' if dry_run else '')
|
||||||
|
|
||||||
|
|
||||||
|
def badge_expiry_config() -> datetime.timedelta:
|
||||||
|
return current_app.config.get('BLENDER_ID_BADGE_EXPIRY')
|
20
pillar/celery/badges.py
Normal file
20
pillar/celery/badges.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Badge HTML synchronisation.
|
||||||
|
|
||||||
|
Note that this module can only be imported when an application context is
|
||||||
|
active. Best to late-import this in the functions where it's needed.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pillar import current_app, badge_sync
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@current_app.celery.task(ignore_result=True)
|
||||||
|
def sync_badges_for_users(timelimit_seconds: int):
|
||||||
|
"""Synchronises Blender ID badges for the most-urgent users."""
|
||||||
|
|
||||||
|
timelimit = datetime.timedelta(seconds=timelimit_seconds)
|
||||||
|
log.info('Refreshing badges, timelimit is %s (H:MM:SS)', timelimit)
|
||||||
|
badge_sync.refresh_all_badges(timelimit=timelimit)
|
@@ -13,6 +13,7 @@ from pillar.cli.maintenance import manager_maintenance
|
|||||||
from pillar.cli.operations import manager_operations
|
from pillar.cli.operations import manager_operations
|
||||||
from pillar.cli.setup import manager_setup
|
from pillar.cli.setup import manager_setup
|
||||||
from pillar.cli.elastic import manager_elastic
|
from pillar.cli.elastic import manager_elastic
|
||||||
|
from . import badges
|
||||||
|
|
||||||
from pillar.cli import translations
|
from pillar.cli import translations
|
||||||
|
|
||||||
@@ -24,3 +25,4 @@ manager.add_command("maintenance", manager_maintenance)
|
|||||||
manager.add_command("setup", manager_setup)
|
manager.add_command("setup", manager_setup)
|
||||||
manager.add_command("operations", manager_operations)
|
manager.add_command("operations", manager_operations)
|
||||||
manager.add_command("elastic", manager_elastic)
|
manager.add_command("elastic", manager_elastic)
|
||||||
|
manager.add_command("badges", badges.manager)
|
||||||
|
39
pillar/cli/badges.py
Normal file
39
pillar/cli/badges.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask_script import Manager
|
||||||
|
from pillar import current_app, badge_sync
|
||||||
|
from pillar.api.utils import utcnow
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
manager = Manager(current_app, usage="Badge operations")
|
||||||
|
|
||||||
|
|
||||||
|
@manager.option('-u', '--user', dest='email', default='', help='Email address of the user to sync')
|
||||||
|
@manager.option('-a', '--all', dest='sync_all', action='store_true', default=False,
|
||||||
|
help='Sync all users')
|
||||||
|
@manager.option('--go', action='store_true', default=False,
|
||||||
|
help='Actually perform the sync; otherwise it is a dry-run.')
|
||||||
|
def sync(email: str = '', sync_all: bool=False, go: bool=False):
|
||||||
|
if bool(email) == bool(sync_all):
|
||||||
|
raise ValueError('Use either --user or --all.')
|
||||||
|
|
||||||
|
if email:
|
||||||
|
users_coll = current_app.db('users')
|
||||||
|
db_user = users_coll.find_one({'email': email}, projection={'_id': True})
|
||||||
|
if not db_user:
|
||||||
|
raise ValueError(f'No user with email {email!r} found')
|
||||||
|
specific_user = db_user['_id']
|
||||||
|
else:
|
||||||
|
specific_user = None
|
||||||
|
|
||||||
|
if not go:
|
||||||
|
log.info('Performing dry-run, not going to change the user database.')
|
||||||
|
start_time = utcnow()
|
||||||
|
badge_sync.refresh_all_badges(specific_user, dry_run=not go,
|
||||||
|
timelimit=datetime.timedelta(hours=1))
|
||||||
|
end_time = utcnow()
|
||||||
|
log.info('%s took %s (H:MM:SS)',
|
||||||
|
'Updating user badges' if go else 'Dry-run',
|
||||||
|
end_time - start_time)
|
@@ -559,50 +559,6 @@ def replace_pillar_node_type_schemas(project_url=None, all_projects=False, missi
|
|||||||
projects_changed, projects_seen)
|
projects_changed, projects_seen)
|
||||||
|
|
||||||
|
|
||||||
@manager_maintenance.command
|
|
||||||
def remarkdown_comments():
|
|
||||||
"""Retranslates all Markdown to HTML for all comment nodes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pillar.api.nodes import convert_markdown
|
|
||||||
|
|
||||||
nodes_collection = current_app.db()['nodes']
|
|
||||||
comments = nodes_collection.find({'node_type': 'comment'},
|
|
||||||
projection={'properties.content': 1,
|
|
||||||
'node_type': 1})
|
|
||||||
|
|
||||||
updated = identical = skipped = errors = 0
|
|
||||||
for node in comments:
|
|
||||||
convert_markdown(node)
|
|
||||||
node_id = node['_id']
|
|
||||||
|
|
||||||
try:
|
|
||||||
content_html = node['properties']['content_html']
|
|
||||||
except KeyError:
|
|
||||||
log.warning('Node %s has no content_html', node_id)
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = nodes_collection.update_one(
|
|
||||||
{'_id': node_id},
|
|
||||||
{'$set': {'properties.content_html': content_html}}
|
|
||||||
)
|
|
||||||
if result.matched_count != 1:
|
|
||||||
log.error('Unable to update node %s', node_id)
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if result.modified_count:
|
|
||||||
updated += 1
|
|
||||||
else:
|
|
||||||
identical += 1
|
|
||||||
|
|
||||||
log.info('updated : %i', updated)
|
|
||||||
log.info('identical: %i', identical)
|
|
||||||
log.info('skipped : %i', skipped)
|
|
||||||
log.info('errors : %i', errors)
|
|
||||||
|
|
||||||
|
|
||||||
@manager_maintenance.option('-p', '--project', dest='proj_url', nargs='?',
|
@manager_maintenance.option('-p', '--project', dest='proj_url', nargs='?',
|
||||||
help='Project URL')
|
help='Project URL')
|
||||||
@manager_maintenance.option('-a', '--all', dest='all_projects', action='store_true', default=False,
|
@manager_maintenance.option('-a', '--all', dest='all_projects', action='store_true', default=False,
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
import datetime
|
||||||
import os.path
|
import os.path
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from collections import defaultdict
|
|
||||||
import requests.certs
|
import requests.certs
|
||||||
|
|
||||||
# Certificate file for communication with other systems.
|
# Certificate file for communication with other systems.
|
||||||
@@ -29,6 +31,7 @@ DEBUG = False
|
|||||||
SECRET_KEY = ''
|
SECRET_KEY = ''
|
||||||
|
|
||||||
# Authentication token hashing key. If empty falls back to UTF8-encoded SECRET_KEY with a warning.
|
# Authentication token hashing key. If empty falls back to UTF8-encoded SECRET_KEY with a warning.
|
||||||
|
# Not used to hash new tokens, but it is used to check pre-existing hashed tokens.
|
||||||
AUTH_TOKEN_HMAC_KEY = b''
|
AUTH_TOKEN_HMAC_KEY = b''
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
@@ -203,8 +206,18 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
'schedule': 600, # every N seconds
|
'schedule': 600, # every N seconds
|
||||||
'args': ('gcs', 100)
|
'args': ('gcs', 100)
|
||||||
},
|
},
|
||||||
|
'refresh-blenderid-badges': {
|
||||||
|
'task': 'pillar.celery.badges.sync_badges_for_users',
|
||||||
|
'schedule': 600, # every N seconds
|
||||||
|
'args': (540, ), # time limit in seconds, keep shorter than 'schedule'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Badges will be re-fetched every timedelta.
|
||||||
|
# TODO(Sybren): A proper value should be determined after we actually have users with badges.
|
||||||
|
BLENDER_ID_BADGE_EXPIRY = datetime.timedelta(hours=4)
|
||||||
|
|
||||||
|
|
||||||
# Mapping from user role to capabilities obtained by users with that role.
|
# Mapping from user role to capabilities obtained by users with that role.
|
||||||
USER_CAPABILITIES = defaultdict(**{
|
USER_CAPABILITIES = defaultdict(**{
|
||||||
'subscriber': {'subscriber', 'home-project'},
|
'subscriber': {'subscriber', 'home-project'},
|
||||||
|
@@ -162,9 +162,12 @@ class YouTube:
|
|||||||
if not youtube_id:
|
if not youtube_id:
|
||||||
return html_module.escape('{youtube invalid YouTube ID/URL}')
|
return html_module.escape('{youtube invalid YouTube ID/URL}')
|
||||||
|
|
||||||
src = f'https://www.youtube.com/embed/{youtube_id}?rel=0'
|
src = f'https://www.youtube.com/embed/{youtube_id}?rel=0'
|
||||||
html = f'<iframe class="shortcode youtube" width="{width}" height="{height}" src="{src}"' \
|
html = f'<div class="embed-responsive embed-responsive-16by9">' \
|
||||||
f' frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>'
|
f'<iframe class="shortcode youtube embed-responsive-item"' \
|
||||||
|
f' width="{width}" height="{height}" src="{src}"' \
|
||||||
|
f' frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>' \
|
||||||
|
f'</div>'
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
@@ -225,12 +228,25 @@ class Attachment:
|
|||||||
|
|
||||||
return self.render(file_doc, pargs, kwargs)
|
return self.render(file_doc, pargs, kwargs)
|
||||||
|
|
||||||
def sdk_file(self, slug: str, node_properties: dict) -> pillarsdk.File:
|
def sdk_file(self, slug: str, document: dict) -> pillarsdk.File:
|
||||||
"""Return the file document for the attachment with this slug."""
|
"""Return the file document for the attachment with this slug."""
|
||||||
|
|
||||||
from pillar.web import system_util
|
from pillar.web import system_util
|
||||||
|
|
||||||
attachments = node_properties.get('attachments', {})
|
# TODO (fsiddi) Make explicit what 'document' is.
|
||||||
|
# In some cases we pass the entire node or project documents, in other cases
|
||||||
|
# we pass node.properties. This should be unified at the level of do_markdown.
|
||||||
|
# For now we do a quick hack and first look for 'properties' in the doc,
|
||||||
|
# then we look for 'attachments'.
|
||||||
|
|
||||||
|
doc_properties = document.get('properties')
|
||||||
|
if doc_properties:
|
||||||
|
# We passed an entire document (all nodes must have 'properties')
|
||||||
|
attachments = doc_properties.get('attachments', {})
|
||||||
|
else:
|
||||||
|
# The value of document could have been defined as 'node.properties'
|
||||||
|
attachments = document.get('attachments', {})
|
||||||
|
|
||||||
attachment = attachments.get(slug)
|
attachment = attachments.get(slug)
|
||||||
if not attachment:
|
if not attachment:
|
||||||
raise self.NoSuchSlug(slug)
|
raise self.NoSuchSlug(slug)
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
@@ -10,11 +11,7 @@ import pathlib
|
|||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
|
from urllib.parse import urlencode, urljoin
|
||||||
try:
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
except ImportError:
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from bson import ObjectId, tz_util
|
from bson import ObjectId, tz_util
|
||||||
|
|
||||||
@@ -186,7 +183,7 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
else:
|
else:
|
||||||
self.ensure_project_exists()
|
self.ensure_project_exists()
|
||||||
|
|
||||||
with self.app.test_request_context():
|
with self.app.app_context():
|
||||||
files_collection = self.app.data.driver.db['files']
|
files_collection = self.app.data.driver.db['files']
|
||||||
assert isinstance(files_collection, pymongo.collection.Collection)
|
assert isinstance(files_collection, pymongo.collection.Collection)
|
||||||
|
|
||||||
@@ -327,15 +324,46 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def create_valid_auth_token(self, user_id, token='token'):
|
@contextlib.contextmanager
|
||||||
|
def login_as(self, user_id: typing.Union[str, ObjectId]):
|
||||||
|
"""Context manager, within the context the app context is active and the user logged in.
|
||||||
|
|
||||||
|
The logging-in happens when a request starts, so it's only active when
|
||||||
|
e.g. self.get() or self.post() or somesuch request is used.
|
||||||
|
"""
|
||||||
|
from pillar.auth import UserClass, login_user_object
|
||||||
|
|
||||||
|
if isinstance(user_id, str):
|
||||||
|
user_oid = ObjectId(user_id)
|
||||||
|
elif isinstance(user_id, ObjectId):
|
||||||
|
user_oid = user_id
|
||||||
|
else:
|
||||||
|
raise TypeError(f'invalid type {type(user_id)} for parameter user_id')
|
||||||
|
user_doc = self.fetch_user_from_db(user_oid)
|
||||||
|
|
||||||
|
def signal_handler(sender, **kwargs):
|
||||||
|
login_user_object(user)
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
user = UserClass.construct('', user_doc)
|
||||||
|
with flask.request_started.connected_to(signal_handler, self.app):
|
||||||
|
yield
|
||||||
|
|
||||||
|
# TODO: rename to 'create_auth_token' now that 'expire_in_days' can be negative.
|
||||||
|
def create_valid_auth_token(self,
|
||||||
|
user_id: ObjectId,
|
||||||
|
token='token',
|
||||||
|
*,
|
||||||
|
oauth_scopes: typing.Optional[typing.List[str]]=None,
|
||||||
|
expire_in_days=1) -> dict:
|
||||||
from pillar.api.utils import utcnow
|
from pillar.api.utils import utcnow
|
||||||
|
|
||||||
future = utcnow() + datetime.timedelta(days=1)
|
future = utcnow() + datetime.timedelta(days=expire_in_days)
|
||||||
|
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
from pillar.api.utils import authentication as auth
|
from pillar.api.utils import authentication as auth
|
||||||
|
|
||||||
token_data = auth.store_token(user_id, token, future, None)
|
token_data = auth.store_token(user_id, token, future, oauth_scopes=oauth_scopes)
|
||||||
|
|
||||||
return token_data
|
return token_data
|
||||||
|
|
||||||
@@ -365,7 +393,7 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
|
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
def create_node(self, node_doc):
|
def create_node(self, node_doc) -> ObjectId:
|
||||||
"""Creates a node, returning its ObjectId. """
|
"""Creates a node, returning its ObjectId. """
|
||||||
|
|
||||||
with self.app.test_request_context():
|
with self.app.test_request_context():
|
||||||
@@ -407,7 +435,7 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
"""Sets up Responses to mock unhappy validation flow."""
|
"""Sets up Responses to mock unhappy validation flow."""
|
||||||
|
|
||||||
responses.add(responses.POST,
|
responses.add(responses.POST,
|
||||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
urljoin(self.app.config['BLENDER_ID_ENDPOINT'], 'u/validate_token'),
|
||||||
json={'status': 'fail'},
|
json={'status': 'fail'},
|
||||||
status=403)
|
status=403)
|
||||||
|
|
||||||
@@ -415,7 +443,7 @@ class AbstractPillarTest(TestMinimal):
|
|||||||
"""Sets up Responses to mock happy validation flow."""
|
"""Sets up Responses to mock happy validation flow."""
|
||||||
|
|
||||||
responses.add(responses.POST,
|
responses.add(responses.POST,
|
||||||
'%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
|
urljoin(self.app.config['BLENDER_ID_ENDPOINT'], 'u/validate_token'),
|
||||||
json=BLENDER_ID_USER_RESPONSE,
|
json=BLENDER_ID_USER_RESPONSE,
|
||||||
status=200)
|
status=200)
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Flask configuration file for unit testing."""
|
"""Flask configuration file for unit testing."""
|
||||||
|
|
||||||
BLENDER_ID_ENDPOINT = 'http://id.local:8001' # Non existant server
|
BLENDER_ID_ENDPOINT = 'http://id.local:8001/' # Non existant server
|
||||||
|
|
||||||
SERVER_NAME = 'localhost.local'
|
SERVER_NAME = 'localhost.local'
|
||||||
PILLAR_SERVER_ENDPOINT = 'http://localhost.local/api/'
|
PILLAR_SERVER_ENDPOINT = 'http://localhost.local/api/'
|
||||||
|
@@ -19,6 +19,7 @@ from pillar.web.nodes.routes import url_for_node
|
|||||||
from pillar.web.nodes.forms import get_node_form
|
from pillar.web.nodes.forms import get_node_form
|
||||||
import pillar.web.nodes.attachments
|
import pillar.web.nodes.attachments
|
||||||
from pillar.web.projects.routes import project_update_nodes_list
|
from pillar.web.projects.routes import project_update_nodes_list
|
||||||
|
from pillar.web.projects.routes import project_navigation_links
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -61,16 +62,10 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
|||||||
post.picture = get_file(post.picture, api=api)
|
post.picture = get_file(post.picture, api=api)
|
||||||
post.url = url_for_node(node=post)
|
post.url = url_for_node(node=post)
|
||||||
|
|
||||||
# Use the *_main_project.html template for the main blog
|
|
||||||
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
|
|
||||||
main_project_template = '_main_project' if is_main_project else ''
|
|
||||||
main_project_template = '_main_project'
|
|
||||||
index_arch = 'archive' if archive else 'index'
|
index_arch = 'archive' if archive else 'index'
|
||||||
template_path = f'nodes/custom/blog/{index_arch}{main_project_template}.html',
|
template_path = f'nodes/custom/blog/{index_arch}.html',
|
||||||
|
|
||||||
if url:
|
if url:
|
||||||
template_path = f'nodes/custom/post/view{main_project_template}.html',
|
|
||||||
|
|
||||||
post = Node.find_one({
|
post = Node.find_one({
|
||||||
'where': {'parent': blog._id, 'properties.url': url},
|
'where': {'parent': blog._id, 'properties.url': url},
|
||||||
'embedded': {'node_type': 1, 'user': 1},
|
'embedded': {'node_type': 1, 'user': 1},
|
||||||
@@ -95,6 +90,7 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
|||||||
can_create_blog_posts = project.node_type_has_method('post', 'POST', api=api)
|
can_create_blog_posts = project.node_type_has_method('post', 'POST', api=api)
|
||||||
|
|
||||||
# Use functools.partial so we can later pass page=X.
|
# Use functools.partial so we can later pass page=X.
|
||||||
|
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
|
||||||
if is_main_project:
|
if is_main_project:
|
||||||
url_func = functools.partial(url_for, 'main.main_blog_archive')
|
url_func = functools.partial(url_for, 'main.main_blog_archive')
|
||||||
else:
|
else:
|
||||||
@@ -112,24 +108,19 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
|||||||
else:
|
else:
|
||||||
project.blog_archive_prev = None
|
project.blog_archive_prev = None
|
||||||
|
|
||||||
title = 'blog_main' if is_main_project else 'blog'
|
navigation_links = project_navigation_links(project, api)
|
||||||
|
|
||||||
pages = Node.all({
|
|
||||||
'where': {'project': project._id, 'node_type': 'page'},
|
|
||||||
'projection': {'name': 1}}, api=api)
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
template_path,
|
template_path,
|
||||||
blog=blog,
|
blog=blog,
|
||||||
node=post,
|
node=post, # node is used by the generic comments rendering (see custom/_scripts.pug)
|
||||||
posts=posts._items,
|
posts=posts._items,
|
||||||
posts_meta=pmeta,
|
posts_meta=pmeta,
|
||||||
more_posts_available=pmeta['total'] > pmeta['max_results'],
|
more_posts_available=pmeta['total'] > pmeta['max_results'],
|
||||||
project=project,
|
project=project,
|
||||||
title=title,
|
|
||||||
node_type_post=project.get_node_type('post'),
|
node_type_post=project.get_node_type('post'),
|
||||||
can_create_blog_posts=can_create_blog_posts,
|
can_create_blog_posts=can_create_blog_posts,
|
||||||
pages=pages._items,
|
navigation_links=navigation_links,
|
||||||
api=api)
|
api=api)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -94,6 +94,16 @@ def find_for_post(project, node):
|
|||||||
url=node.properties.url)
|
url=node.properties.url)
|
||||||
|
|
||||||
|
|
||||||
|
@register_node_finder('page')
|
||||||
|
def find_for_page(project, node):
|
||||||
|
"""Returns the URL for a page."""
|
||||||
|
|
||||||
|
project_id = project['_id']
|
||||||
|
|
||||||
|
the_project = project_url(project_id, project=project)
|
||||||
|
return url_for('projects.view_node', project_url=the_project.url, node_id=node.properties.url)
|
||||||
|
|
||||||
|
|
||||||
def find_for_other(project, node):
|
def find_for_other(project, node):
|
||||||
"""Fallback: Assets, textures, and other node types.
|
"""Fallback: Assets, textures, and other node types.
|
||||||
|
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import pillarsdk
|
import pillarsdk
|
||||||
from flask import current_app
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField
|
from wtforms import StringField
|
||||||
from wtforms import DateField
|
from wtforms import DateField
|
||||||
@@ -17,6 +18,8 @@ from wtforms import DateTimeField
|
|||||||
from wtforms import SelectMultipleField
|
from wtforms import SelectMultipleField
|
||||||
from wtforms import FieldList
|
from wtforms import FieldList
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
from pillar import current_app
|
||||||
from pillar.web.utils import system_util
|
from pillar.web.utils import system_util
|
||||||
from pillar.web.utils.forms import FileSelectField
|
from pillar.web.utils.forms import FileSelectField
|
||||||
from pillar.web.utils.forms import CustomFormField
|
from pillar.web.utils.forms import CustomFormField
|
||||||
@@ -44,6 +47,13 @@ def iter_node_properties(node_type):
|
|||||||
yield prop_name, prop_schema, prop_fschema
|
yield prop_name, prop_schema, prop_fschema
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def tag_choices() -> typing.List[typing.Tuple[str, str]]:
|
||||||
|
"""Return (value, label) tuples for the NODE_TAGS config setting."""
|
||||||
|
tags = current_app.config.get('NODE_TAGS') or []
|
||||||
|
return [(tag, tag.title()) for tag in tags] # (value, label) tuples
|
||||||
|
|
||||||
|
|
||||||
def add_form_properties(form_class, node_type):
|
def add_form_properties(form_class, node_type):
|
||||||
"""Add fields to a form based on the node and form schema provided.
|
"""Add fields to a form based on the node and form schema provided.
|
||||||
:type node_schema: dict
|
:type node_schema: dict
|
||||||
@@ -60,7 +70,9 @@ def add_form_properties(form_class, node_type):
|
|||||||
# Recursive call if detects a dict
|
# Recursive call if detects a dict
|
||||||
field_type = schema_prop['type']
|
field_type = schema_prop['type']
|
||||||
|
|
||||||
if field_type == 'dict':
|
if prop_name == 'tags' and field_type == 'list':
|
||||||
|
field = SelectMultipleField(choices=tag_choices())
|
||||||
|
elif field_type == 'dict':
|
||||||
assert prop_name == 'attachments'
|
assert prop_name == 'attachments'
|
||||||
field = attachments.attachment_form_group_create(schema_prop)
|
field = attachments.attachment_form_group_create(schema_prop)
|
||||||
elif field_type == 'list':
|
elif field_type == 'list':
|
||||||
|
@@ -24,6 +24,7 @@ from pillar import current_app
|
|||||||
from pillar.api.utils import utcnow
|
from pillar.api.utils import utcnow
|
||||||
from pillar.web import system_util
|
from pillar.web import system_util
|
||||||
from pillar.web import utils
|
from pillar.web import utils
|
||||||
|
from pillar.web.nodes import finders
|
||||||
from pillar.web.utils.jstree import jstree_get_children
|
from pillar.web.utils.jstree import jstree_get_children
|
||||||
import pillar.extension
|
import pillar.extension
|
||||||
|
|
||||||
@@ -302,6 +303,51 @@ def view(project_url):
|
|||||||
'header_video_node': header_video_node})
|
'header_video_node': header_video_node})
|
||||||
|
|
||||||
|
|
||||||
|
def project_navigation_links(project, api) -> list:
|
||||||
|
"""Returns a list of nodes for the project, for top navigation display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: A Project object.
|
||||||
|
api: the api client credential.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of links for the Project.
|
||||||
|
For example we display a link to the project blog if present, as well
|
||||||
|
as pages. The list is structured as follows:
|
||||||
|
|
||||||
|
[{'url': '/p/spring/about', 'label': 'About'},
|
||||||
|
{'url': '/p/spring/blog', 'label': 'Blog'}]
|
||||||
|
"""
|
||||||
|
|
||||||
|
links = []
|
||||||
|
|
||||||
|
# Fetch the blog
|
||||||
|
blog = Node.find_first({
|
||||||
|
'where': {'project': project._id, 'node_type': 'blog', '_deleted': {'$ne': True}},
|
||||||
|
'projection': {
|
||||||
|
'name': 1,
|
||||||
|
}
|
||||||
|
}, api=api)
|
||||||
|
|
||||||
|
if blog:
|
||||||
|
links.append({'url': finders.find_url_for_node(blog), 'label': blog.name, 'slug': 'blog'})
|
||||||
|
|
||||||
|
# Fetch pages
|
||||||
|
pages = Node.all({
|
||||||
|
'where': {'project': project._id, 'node_type': 'page', '_deleted': {'$ne': True}},
|
||||||
|
'projection': {
|
||||||
|
'name': 1,
|
||||||
|
'properties.url': 1
|
||||||
|
}
|
||||||
|
}, api=api)
|
||||||
|
|
||||||
|
# Process the results and append the links to the list
|
||||||
|
for p in pages._items:
|
||||||
|
links.append({'url': finders.find_url_for_node(p), 'label': p.name, 'slug': p.properties.url})
|
||||||
|
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
def render_project(project, api, extra_context=None, template_name=None):
|
def render_project(project, api, extra_context=None, template_name=None):
|
||||||
project.picture_square = utils.get_file(project.picture_square, api=api)
|
project.picture_square = utils.get_file(project.picture_square, api=api)
|
||||||
project.picture_header = utils.get_file(project.picture_header, api=api)
|
project.picture_header = utils.get_file(project.picture_header, api=api)
|
||||||
@@ -370,6 +416,8 @@ def render_project(project, api, extra_context=None, template_name=None):
|
|||||||
|
|
||||||
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
||||||
|
|
||||||
|
navigation_links = project_navigation_links(project, api)
|
||||||
|
|
||||||
return render_template(template_name,
|
return render_template(template_name,
|
||||||
api=api,
|
api=api,
|
||||||
project=project,
|
project=project,
|
||||||
@@ -378,6 +426,7 @@ def render_project(project, api, extra_context=None, template_name=None):
|
|||||||
show_project=True,
|
show_project=True,
|
||||||
og_picture=project.picture_header,
|
og_picture=project.picture_header,
|
||||||
activity_stream=activity_stream,
|
activity_stream=activity_stream,
|
||||||
|
navigation_links=navigation_links,
|
||||||
extension_sidebar_links=extension_sidebar_links,
|
extension_sidebar_links=extension_sidebar_links,
|
||||||
**extra_context)
|
**extra_context)
|
||||||
|
|
||||||
@@ -447,16 +496,14 @@ def view_node(project_url, node_id):
|
|||||||
|
|
||||||
# Append _theatre to load the proper template
|
# Append _theatre to load the proper template
|
||||||
theatre = '_theatre' if theatre_mode else ''
|
theatre = '_theatre' if theatre_mode else ''
|
||||||
|
navigation_links = project_navigation_links(project, api)
|
||||||
|
|
||||||
if node.node_type == 'page':
|
if node.node_type == 'page':
|
||||||
pages = Node.all({
|
|
||||||
'where': {'project': project._id, 'node_type': 'page'},
|
|
||||||
'projection': {'name': 1}}, api=api)
|
|
||||||
return render_template('nodes/custom/page/view_embed.html',
|
return render_template('nodes/custom/page/view_embed.html',
|
||||||
api=api,
|
api=api,
|
||||||
node=node,
|
node=node,
|
||||||
project=project,
|
project=project,
|
||||||
pages=pages._items,
|
navigation_links=navigation_links,
|
||||||
og_picture=og_picture,)
|
og_picture=og_picture,)
|
||||||
|
|
||||||
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
extension_sidebar_links = current_app.extension_sidebar_links(project)
|
||||||
@@ -468,6 +515,7 @@ def view_node(project_url, node_id):
|
|||||||
show_node=True,
|
show_node=True,
|
||||||
show_project=False,
|
show_project=False,
|
||||||
og_picture=og_picture,
|
og_picture=og_picture,
|
||||||
|
navigation_links=navigation_links,
|
||||||
extension_sidebar_links=extension_sidebar_links)
|
extension_sidebar_links=extension_sidebar_links)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -872,12 +872,6 @@
|
|||||||
"code": 61930,
|
"code": 61930,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"uid": "31972e4e9d080eaa796290349ae6c1fd",
|
|
||||||
"css": "users",
|
|
||||||
"code": 59502,
|
|
||||||
"src": "fontawesome"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"uid": "c8585e1e5b0467f28b70bce765d5840c",
|
"uid": "c8585e1e5b0467f28b70bce765d5840c",
|
||||||
"css": "clipboard-copy",
|
"css": "clipboard-copy",
|
||||||
@@ -990,6 +984,30 @@
|
|||||||
"code": 59394,
|
"code": 59394,
|
||||||
"src": "entypo"
|
"src": "entypo"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"uid": "347c38a8b96a509270fdcabc951e7571",
|
||||||
|
"css": "database",
|
||||||
|
"code": 61888,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "3a6f0140c3a390bdb203f56d1bfdefcb",
|
||||||
|
"css": "speed",
|
||||||
|
"code": 59471,
|
||||||
|
"src": "entypo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "4c1ef492f1d2c39a2250ae457cee2a6e",
|
||||||
|
"css": "social-instagram",
|
||||||
|
"code": 61805,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "e36d581e4f2844db345bddc205d15dda",
|
||||||
|
"css": "users",
|
||||||
|
"code": 59507,
|
||||||
|
"src": "elusive"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"uid": "053a214a098a9453877363eeb45f004e",
|
"uid": "053a214a098a9453877363eeb45f004e",
|
||||||
"css": "log-in",
|
"css": "log-in",
|
||||||
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -33,7 +33,8 @@ def get_user_info(user_id):
|
|||||||
# TODO: put those fields into a config var or module-level global.
|
# TODO: put those fields into a config var or module-level global.
|
||||||
return {'email': user.email,
|
return {'email': user.email,
|
||||||
'full_name': user.full_name,
|
'full_name': user.full_name,
|
||||||
'username': user.username}
|
'username': user.username,
|
||||||
|
'badges_html': (user.badges and user.badges.html) or ''}
|
||||||
|
|
||||||
|
|
||||||
def setup_app(app):
|
def setup_app(app):
|
||||||
|
@@ -48,6 +48,10 @@ def oauth_authorize(provider):
|
|||||||
|
|
||||||
@blueprint.route('/oauth/<provider>/authorized')
|
@blueprint.route('/oauth/<provider>/authorized')
|
||||||
def oauth_callback(provider):
|
def oauth_callback(provider):
|
||||||
|
import datetime
|
||||||
|
from pillar.api.utils.authentication import store_token
|
||||||
|
from pillar.api.utils import utcnow
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.homepage'))
|
return redirect(url_for('main.homepage'))
|
||||||
|
|
||||||
@@ -65,7 +69,17 @@ def oauth_callback(provider):
|
|||||||
user_info = {'id': oauth_user.id, 'email': oauth_user.email, 'full_name': ''}
|
user_info = {'id': oauth_user.id, 'email': oauth_user.email, 'full_name': ''}
|
||||||
db_user = find_user_in_db(user_info, provider=provider)
|
db_user = find_user_in_db(user_info, provider=provider)
|
||||||
db_id, status = upsert_user(db_user)
|
db_id, status = upsert_user(db_user)
|
||||||
token = generate_and_store_token(db_id)
|
|
||||||
|
# TODO(Sybren): If the user doesn't have any badges, but the access token
|
||||||
|
# does have 'badge' scope, we should fetch the badges in the background.
|
||||||
|
|
||||||
|
if oauth_user.access_token:
|
||||||
|
# TODO(Sybren): make nr of days configurable, or get from OAuthSignIn subclass.
|
||||||
|
token_expiry = utcnow() + datetime.timedelta(days=15)
|
||||||
|
token = store_token(db_id, oauth_user.access_token, token_expiry,
|
||||||
|
oauth_scopes=oauth_user.scopes)
|
||||||
|
else:
|
||||||
|
token = generate_and_store_token(db_id)
|
||||||
|
|
||||||
# Login user
|
# Login user
|
||||||
pillar.auth.login_user(token['token'], load_from_db=True)
|
pillar.auth.login_user(token['token'], load_from_db=True)
|
||||||
|
@@ -62,7 +62,7 @@ def jstree_get_children(node_id, project_id=None):
|
|||||||
'where': {
|
'where': {
|
||||||
'$and': [
|
'$and': [
|
||||||
{'node_type': {'$regex': '^(?!attract_)'}},
|
{'node_type': {'$regex': '^(?!attract_)'}},
|
||||||
{'node_type': {'$not': {'$in': ['comment', 'post']}}},
|
{'node_type': {'$not': {'$in': ['comment', 'post', 'blog', 'page']}}},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var output, Converter;
|
|
||||||
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
|
|
||||||
output = exports;
|
|
||||||
Converter = require("./Markdown.Converter").Converter;
|
|
||||||
} else {
|
|
||||||
output = window.Markdown;
|
|
||||||
Converter = output.Converter;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.getSanitizingConverter = function () {
|
|
||||||
var converter = new Converter();
|
|
||||||
converter.hooks.chain("postConversion", sanitizeHtml);
|
|
||||||
converter.hooks.chain("postConversion", balanceTags);
|
|
||||||
return converter;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeHtml(html) {
|
|
||||||
return html.replace(/<[^>]*>?/gi, sanitizeTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// (tags that can be opened/closed) | (tags that stand alone)
|
|
||||||
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|iframe|kbd|li|ol(?: start="\d+")?|p|pre|s|sup|sub|strong|strike|ul|video)>|<(br|hr)\s?\/?>)$/i;
|
|
||||||
// <a href="url..." optional title>|</a>
|
|
||||||
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\stitle="[^"<>]+")?(\sclass="[^"<>]+")?\s?>|<\/a>)$/i;
|
|
||||||
|
|
||||||
// Cloud custom: Allow iframe embed from YouTube, Vimeo and SoundCloud
|
|
||||||
var iframe_youtube = /^(<iframe(\swidth="\d{1,3}")?(\sheight="\d{1,3}")\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\sframeborder="\d{1,3}")?(\sallowfullscreen)\s?>|<\/iframe>)$/i;
|
|
||||||
var iframe_vimeo = /^(<iframe(\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"?\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\sframeborder="\d{1,3}")?(\swebkitallowfullscreen)\s?(\smozallowfullscreen)\s?(\sallowfullscreen)\s?>|<\/iframe>)$/i;
|
|
||||||
var iframe_soundcloud = /^(<iframe(\swidth="\d{1,3}\%")?(\sheight="\d{1,3}")?(\sscrolling="(?:yes|no)")?(\sframeborder="(?:yes|no)")\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"\s?>|<\/iframe>)$/i;
|
|
||||||
var iframe_googlestorage = /^(<iframe\ssrc="https:\/\/storage.googleapis.com\/institute-storage\/.+"\sstyle=".*"\s?>|<\/iframe>)$/i;
|
|
||||||
|
|
||||||
// <img src="url..." optional width optional height optional alt optional title
|
|
||||||
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
|
|
||||||
var video_white = /<video(.*?)>/;
|
|
||||||
|
|
||||||
function sanitizeTag(tag) {
|
|
||||||
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(iframe_youtube) || tag.match(iframe_vimeo) || tag.match(iframe_soundcloud) || tag.match(iframe_googlestorage) || tag.match(video_white)) {
|
|
||||||
return tag;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// attempt to balance HTML tags in the html string
|
|
||||||
/// by removing any unmatched opening or closing tags
|
|
||||||
/// IMPORTANT: we *assume* HTML has *already* been
|
|
||||||
/// sanitized and is safe/sane before balancing!
|
|
||||||
///
|
|
||||||
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
|
|
||||||
/// </summary>
|
|
||||||
function balanceTags(html) {
|
|
||||||
|
|
||||||
if (html == "")
|
|
||||||
return "";
|
|
||||||
|
|
||||||
var re = /<\/?\w+[^>]*(\s|$|>)/g;
|
|
||||||
// convert everything to lower case; this makes
|
|
||||||
// our case insensitive comparisons easier
|
|
||||||
var tags = html.toLowerCase().match(re);
|
|
||||||
|
|
||||||
// no HTML tags present? nothing to do; exit now
|
|
||||||
var tagcount = (tags || []).length;
|
|
||||||
if (tagcount == 0)
|
|
||||||
return html;
|
|
||||||
|
|
||||||
var tagname, tag;
|
|
||||||
var ignoredtags = "<p><img><br><li><hr>";
|
|
||||||
var match;
|
|
||||||
var tagpaired = [];
|
|
||||||
var tagremove = [];
|
|
||||||
var needsRemoval = false;
|
|
||||||
|
|
||||||
// loop through matched tags in forward order
|
|
||||||
for (var ctag = 0; ctag < tagcount; ctag++) {
|
|
||||||
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
|
|
||||||
// skip any already paired tags
|
|
||||||
// and skip tags in our ignore list; assume they're self-closed
|
|
||||||
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
tag = tags[ctag];
|
|
||||||
match = -1;
|
|
||||||
|
|
||||||
if (!/^<\//.test(tag)) {
|
|
||||||
// this is an opening tag
|
|
||||||
// search forwards (next tags), look for closing tags
|
|
||||||
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
|
|
||||||
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
|
|
||||||
match = ntag;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match == -1)
|
|
||||||
needsRemoval = tagremove[ctag] = true; // mark for removal
|
|
||||||
else
|
|
||||||
tagpaired[match] = true; // mark paired
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!needsRemoval)
|
|
||||||
return html;
|
|
||||||
|
|
||||||
// delete all orphaned tags from the string
|
|
||||||
|
|
||||||
var ctag = 0;
|
|
||||||
html = html.replace(re, function (match) {
|
|
||||||
var res = tagremove[ctag] ? "" : match;
|
|
||||||
ctag++;
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
})();
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,874 +0,0 @@
|
|||||||
(function () {
|
|
||||||
// A quick way to make sure we're only keeping span-level tags when we need to.
|
|
||||||
// This isn't supposed to be foolproof. It's just a quick way to make sure we
|
|
||||||
// keep all span-level tags returned by a pagedown converter. It should allow
|
|
||||||
// all span-level tags through, with or without attributes.
|
|
||||||
var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|',
|
|
||||||
'bdo|big|button|cite|code|del|dfn|em|figcaption|',
|
|
||||||
'font|i|iframe|img|input|ins|kbd|label|map|',
|
|
||||||
'mark|meter|object|param|progress|q|ruby|rp|rt|s|',
|
|
||||||
'samp|script|select|small|span|strike|strong|',
|
|
||||||
'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|',
|
|
||||||
'<(br)\\s?\\/?>)$'].join(''), 'i');
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Utility Functions *
|
|
||||||
*****************************************************************/
|
|
||||||
|
|
||||||
// patch for ie7
|
|
||||||
if (!Array.indexOf) {
|
|
||||||
Array.prototype.indexOf = function(obj) {
|
|
||||||
for (var i = 0; i < this.length; i++) {
|
|
||||||
if (this[i] == obj) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function trim(str) {
|
|
||||||
return str.replace(/^\s+|\s+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function rtrim(str) {
|
|
||||||
return str.replace(/\s+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove one level of indentation from text. Indent is 4 spaces.
|
|
||||||
function outdent(text) {
|
|
||||||
return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function contains(str, substr) {
|
|
||||||
return str.indexOf(substr) != -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize html, removing tags that aren't in the whitelist
|
|
||||||
function sanitizeHtml(html, whitelist) {
|
|
||||||
return html.replace(/<[^>]*>?/gi, function(tag) {
|
|
||||||
return tag.match(whitelist) ? tag : '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge two arrays, keeping only unique elements.
|
|
||||||
function union(x, y) {
|
|
||||||
var obj = {};
|
|
||||||
for (var i = 0; i < x.length; i++)
|
|
||||||
obj[x[i]] = x[i];
|
|
||||||
for (i = 0; i < y.length; i++)
|
|
||||||
obj[y[i]] = y[i];
|
|
||||||
var res = [];
|
|
||||||
for (var k in obj) {
|
|
||||||
if (obj.hasOwnProperty(k))
|
|
||||||
res.push(obj[k]);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS regexes don't support \A or \Z, so we add sentinels, as Pagedown
|
|
||||||
// does. In this case, we add the ascii codes for start of text (STX) and
|
|
||||||
// end of text (ETX), an idea borrowed from:
|
|
||||||
// https://github.com/tanakahisateru/js-markdown-extra
|
|
||||||
function addAnchors(text) {
|
|
||||||
if(text.charAt(0) != '\x02')
|
|
||||||
text = '\x02' + text;
|
|
||||||
if(text.charAt(text.length - 1) != '\x03')
|
|
||||||
text = text + '\x03';
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove STX and ETX sentinels.
|
|
||||||
function removeAnchors(text) {
|
|
||||||
if(text.charAt(0) == '\x02')
|
|
||||||
text = text.substr(1);
|
|
||||||
if(text.charAt(text.length - 1) == '\x03')
|
|
||||||
text = text.substr(0, text.length - 1);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert markdown within an element, retaining only span-level tags
|
|
||||||
function convertSpans(text, extra) {
|
|
||||||
return sanitizeHtml(convertAll(text, extra), inlineTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal markdown using the stock pagedown converter
|
|
||||||
function convertAll(text, extra) {
|
|
||||||
var result = extra.blockGamutHookCallback(text);
|
|
||||||
// We need to perform these operations since we skip the steps in the converter
|
|
||||||
result = unescapeSpecialChars(result);
|
|
||||||
result = result.replace(/~D/g, "$$").replace(/~T/g, "~");
|
|
||||||
result = extra.previousPostConversion(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert escaped special characters
|
|
||||||
function processEscapesStep1(text) {
|
|
||||||
// Markdown extra adds two escapable characters, `:` and `|`
|
|
||||||
return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i');
|
|
||||||
}
|
|
||||||
function processEscapesStep2(text) {
|
|
||||||
return text.replace(/~I/g, '|').replace(/~i/g, ':');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicated from PageDown converter
|
|
||||||
function unescapeSpecialChars(text) {
|
|
||||||
// Swap back in all the special characters we've hidden.
|
|
||||||
text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) {
|
|
||||||
var charCodeToReplace = parseInt(m1);
|
|
||||||
return String.fromCharCode(charCodeToReplace);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function slugify(text) {
|
|
||||||
return text.toLowerCase()
|
|
||||||
.replace(/\s+/g, '-') // Replace spaces with -
|
|
||||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
|
||||||
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
|
||||||
.replace(/^-+/, '') // Trim - from start of text
|
|
||||||
.replace(/-+$/, ''); // Trim - from end of text
|
|
||||||
}
|
|
||||||
|
|
||||||
/*****************************************************************************
|
|
||||||
* Markdown.Extra *
|
|
||||||
****************************************************************************/
|
|
||||||
|
|
||||||
Markdown.Extra = function() {
|
|
||||||
// For converting internal markdown (in tables for instance).
|
|
||||||
// This is necessary since these methods are meant to be called as
|
|
||||||
// preConversion hooks, and the Markdown converter passed to init()
|
|
||||||
// won't convert any markdown contained in the html tags we return.
|
|
||||||
this.converter = null;
|
|
||||||
|
|
||||||
// Stores html blocks we generate in hooks so that
|
|
||||||
// they're not destroyed if the user is using a sanitizing converter
|
|
||||||
this.hashBlocks = [];
|
|
||||||
|
|
||||||
// Stores footnotes
|
|
||||||
this.footnotes = {};
|
|
||||||
this.usedFootnotes = [];
|
|
||||||
|
|
||||||
// Special attribute blocks for fenced code blocks and headers enabled.
|
|
||||||
this.attributeBlocks = false;
|
|
||||||
|
|
||||||
// Fenced code block options
|
|
||||||
this.googleCodePrettify = false;
|
|
||||||
this.highlightJs = false;
|
|
||||||
|
|
||||||
// Table options
|
|
||||||
this.tableClass = '';
|
|
||||||
|
|
||||||
this.tabWidth = 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
Markdown.Extra.init = function(converter, options) {
|
|
||||||
// Each call to init creates a new instance of Markdown.Extra so it's
|
|
||||||
// safe to have multiple converters, with different options, on a single page
|
|
||||||
var extra = new Markdown.Extra();
|
|
||||||
var postNormalizationTransformations = [];
|
|
||||||
var preBlockGamutTransformations = [];
|
|
||||||
var postSpanGamutTransformations = [];
|
|
||||||
var postConversionTransformations = ["unHashExtraBlocks"];
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
options.extensions = options.extensions || ["all"];
|
|
||||||
if (contains(options.extensions, "all")) {
|
|
||||||
options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"];
|
|
||||||
}
|
|
||||||
preBlockGamutTransformations.push("wrapHeaders");
|
|
||||||
if (contains(options.extensions, "attr_list")) {
|
|
||||||
postNormalizationTransformations.push("hashFcbAttributeBlocks");
|
|
||||||
preBlockGamutTransformations.push("hashHeaderAttributeBlocks");
|
|
||||||
postConversionTransformations.push("applyAttributeBlocks");
|
|
||||||
extra.attributeBlocks = true;
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "fenced_code_gfm")) {
|
|
||||||
// This step will convert fcb inside list items and blockquotes
|
|
||||||
preBlockGamutTransformations.push("fencedCodeBlocks");
|
|
||||||
// This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb
|
|
||||||
postNormalizationTransformations.push("fencedCodeBlocks");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "tables")) {
|
|
||||||
preBlockGamutTransformations.push("tables");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "def_list")) {
|
|
||||||
preBlockGamutTransformations.push("definitionLists");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "footnotes")) {
|
|
||||||
postNormalizationTransformations.push("stripFootnoteDefinitions");
|
|
||||||
preBlockGamutTransformations.push("doFootnotes");
|
|
||||||
postConversionTransformations.push("printFootnotes");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "smartypants")) {
|
|
||||||
postConversionTransformations.push("runSmartyPants");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "strikethrough")) {
|
|
||||||
postSpanGamutTransformations.push("strikethrough");
|
|
||||||
}
|
|
||||||
if (contains(options.extensions, "newlines")) {
|
|
||||||
postSpanGamutTransformations.push("newlines");
|
|
||||||
}
|
|
||||||
|
|
||||||
converter.hooks.chain("postNormalization", function(text) {
|
|
||||||
return extra.doTransform(postNormalizationTransformations, text) + '\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) {
|
|
||||||
// Keep a reference to the block gamut callback to run recursively
|
|
||||||
extra.blockGamutHookCallback = blockGamutHookCallback;
|
|
||||||
text = processEscapesStep1(text);
|
|
||||||
text = extra.doTransform(preBlockGamutTransformations, text) + '\n';
|
|
||||||
text = processEscapesStep2(text);
|
|
||||||
return text;
|
|
||||||
});
|
|
||||||
|
|
||||||
converter.hooks.chain("postSpanGamut", function(text) {
|
|
||||||
return extra.doTransform(postSpanGamutTransformations, text);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks
|
|
||||||
extra.previousPostConversion = converter.hooks.postConversion;
|
|
||||||
converter.hooks.chain("postConversion", function(text) {
|
|
||||||
text = extra.doTransform(postConversionTransformations, text);
|
|
||||||
// Clear state vars that may use unnecessary memory
|
|
||||||
extra.hashBlocks = [];
|
|
||||||
extra.footnotes = {};
|
|
||||||
extra.usedFootnotes = [];
|
|
||||||
return text;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("highlighter" in options) {
|
|
||||||
extra.googleCodePrettify = options.highlighter === 'prettify';
|
|
||||||
extra.highlightJs = options.highlighter === 'highlight';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("table_class" in options) {
|
|
||||||
extra.tableClass = options.table_class;
|
|
||||||
}
|
|
||||||
|
|
||||||
extra.converter = converter;
|
|
||||||
|
|
||||||
// Caller usually won't need this, but it's handy for testing.
|
|
||||||
return extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Do transformations
|
|
||||||
Markdown.Extra.prototype.doTransform = function(transformations, text) {
|
|
||||||
for(var i = 0; i < transformations.length; i++)
|
|
||||||
text = this[transformations[i]](text);
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return a placeholder containing a key, which is the block's index in the
|
|
||||||
// hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't.
|
|
||||||
Markdown.Extra.prototype.hashExtraBlock = function(block) {
|
|
||||||
return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\n';
|
|
||||||
};
|
|
||||||
Markdown.Extra.prototype.hashExtraInline = function(block) {
|
|
||||||
return '~X' + (this.hashBlocks.push(block) - 1) + 'X';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace placeholder blocks in `text` with their corresponding
|
|
||||||
// html blocks in the hashBlocks array.
|
|
||||||
Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
|
|
||||||
var self = this;
|
|
||||||
function recursiveUnHash() {
|
|
||||||
var hasHash = false;
|
|
||||||
text = text.replace(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) {
|
|
||||||
hasHash = true;
|
|
||||||
var key = parseInt(m1, 10);
|
|
||||||
return self.hashBlocks[key];
|
|
||||||
});
|
|
||||||
if(hasHash === true) {
|
|
||||||
recursiveUnHash();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recursiveUnHash();
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wrap headers to make sure they won't be in def lists
|
|
||||||
Markdown.Extra.prototype.wrapHeaders = function(text) {
|
|
||||||
function wrap(text) {
|
|
||||||
return '\n' + text + '\n';
|
|
||||||
}
|
|
||||||
text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap);
|
|
||||||
text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap);
|
|
||||||
text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap);
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Attribute Blocks *
|
|
||||||
*****************************************************************/
|
|
||||||
|
|
||||||
// TODO: use sentinels. Should we just add/remove them in doConversion?
|
|
||||||
// TODO: better matches for id / class attributes
|
|
||||||
var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}";
|
|
||||||
var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm");
|
|
||||||
var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
|
|
||||||
"(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead
|
|
||||||
var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
|
|
||||||
"(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm");
|
|
||||||
|
|
||||||
// Extract headers attribute blocks, move them above the element they will be
|
|
||||||
// applied to, and hash them for later.
|
|
||||||
Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
function attributeCallback(wholeMatch, pre, attr) {
|
|
||||||
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.replace(hdrAttributesA, attributeCallback); // ## headers
|
|
||||||
text = text.replace(hdrAttributesB, attributeCallback); // underline headers
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract FCB attribute blocks, move them above the element they will be
|
|
||||||
// applied to, and hash them for later.
|
|
||||||
Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) {
|
|
||||||
// TODO: use sentinels. Should we just add/remove them in doConversion?
|
|
||||||
// TODO: better matches for id / class attributes
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
function attributeCallback(wholeMatch, pre, attr) {
|
|
||||||
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.replace(fcbAttributes, attributeCallback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
|
|
||||||
var self = this;
|
|
||||||
var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\s]*' +
|
|
||||||
'(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?</\\2>))', "gm");
|
|
||||||
text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
|
|
||||||
if (!tag) // no following header or fenced code block.
|
|
||||||
return '';
|
|
||||||
|
|
||||||
// get attributes list from hash
|
|
||||||
var key = parseInt(k, 10);
|
|
||||||
var attributes = self.hashBlocks[key];
|
|
||||||
|
|
||||||
// get id
|
|
||||||
var id = attributes.match(/#[^\s#.]+/g) || [];
|
|
||||||
var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : '';
|
|
||||||
|
|
||||||
// get classes and merge with existing classes
|
|
||||||
var classes = attributes.match(/\.[^\s#.]+/g) || [];
|
|
||||||
for (var i = 0; i < classes.length; i++) // Remove leading dot
|
|
||||||
classes[i] = classes[i].substr(1, classes[i].length - 1);
|
|
||||||
|
|
||||||
var classStr = '';
|
|
||||||
if (cls)
|
|
||||||
classes = union(classes, [cls]);
|
|
||||||
|
|
||||||
if (classes.length > 0)
|
|
||||||
classStr = ' class="' + classes.join(' ') + '"';
|
|
||||||
|
|
||||||
return "<" + tag + idStr + classStr + rest;
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Tables *
|
|
||||||
*****************************************************************/
|
|
||||||
|
|
||||||
// Find and convert Markdown Extra tables into html.
|
|
||||||
Markdown.Extra.prototype.tables = function(text) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var leadingPipe = new RegExp(
|
|
||||||
['^' ,
|
|
||||||
'[ ]{0,3}' , // Allowed whitespace
|
|
||||||
'[|]' , // Initial pipe
|
|
||||||
'(.+)\\n' , // $1: Header Row
|
|
||||||
|
|
||||||
'[ ]{0,3}' , // Allowed whitespace
|
|
||||||
'[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator
|
|
||||||
|
|
||||||
'(' , // $3: Table Body
|
|
||||||
'(?:[ ]*[|].*\\n?)*' , // Table rows
|
|
||||||
')',
|
|
||||||
'(?:\\n|$)' // Stop at final newline
|
|
||||||
].join(''),
|
|
||||||
'gm'
|
|
||||||
);
|
|
||||||
|
|
||||||
var noLeadingPipe = new RegExp(
|
|
||||||
['^' ,
|
|
||||||
'[ ]{0,3}' , // Allowed whitespace
|
|
||||||
'(\\S.*[|].*)\\n' , // $1: Header Row
|
|
||||||
|
|
||||||
'[ ]{0,3}' , // Allowed whitespace
|
|
||||||
'([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator
|
|
||||||
|
|
||||||
'(' , // $3: Table Body
|
|
||||||
'(?:.*[|].*\\n?)*' , // Table rows
|
|
||||||
')' ,
|
|
||||||
'(?:\\n|$)' // Stop at final newline
|
|
||||||
].join(''),
|
|
||||||
'gm'
|
|
||||||
);
|
|
||||||
|
|
||||||
text = text.replace(leadingPipe, doTable);
|
|
||||||
text = text.replace(noLeadingPipe, doTable);
|
|
||||||
|
|
||||||
// $1 = header, $2 = separator, $3 = body
|
|
||||||
function doTable(match, header, separator, body, offset, string) {
|
|
||||||
// remove any leading pipes and whitespace
|
|
||||||
header = header.replace(/^ *[|]/m, '');
|
|
||||||
separator = separator.replace(/^ *[|]/m, '');
|
|
||||||
body = body.replace(/^ *[|]/gm, '');
|
|
||||||
|
|
||||||
// remove trailing pipes and whitespace
|
|
||||||
header = header.replace(/[|] *$/m, '');
|
|
||||||
separator = separator.replace(/[|] *$/m, '');
|
|
||||||
body = body.replace(/[|] *$/gm, '');
|
|
||||||
|
|
||||||
// determine column alignments
|
|
||||||
var alignspecs = separator.split(/ *[|] */);
|
|
||||||
var align = [];
|
|
||||||
for (var i = 0; i < alignspecs.length; i++) {
|
|
||||||
var spec = alignspecs[i];
|
|
||||||
if (spec.match(/^ *-+: *$/m))
|
|
||||||
align[i] = ' align="right"';
|
|
||||||
else if (spec.match(/^ *:-+: *$/m))
|
|
||||||
align[i] = ' align="center"';
|
|
||||||
else if (spec.match(/^ *:-+ *$/m))
|
|
||||||
align[i] = ' align="left"';
|
|
||||||
else align[i] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: parse spans in header and rows before splitting, so that pipes
|
|
||||||
// inside of tags are not interpreted as separators
|
|
||||||
var headers = header.split(/ *[|] */);
|
|
||||||
var colCount = headers.length;
|
|
||||||
|
|
||||||
// build html
|
|
||||||
var cls = self.tableClass ? ' class="' + self.tableClass + '"' : '';
|
|
||||||
var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join('');
|
|
||||||
|
|
||||||
// build column headers.
|
|
||||||
for (i = 0; i < colCount; i++) {
|
|
||||||
var headerHtml = convertSpans(trim(headers[i]), self);
|
|
||||||
html += [" <th", align[i], ">", headerHtml, "</th>\n"].join('');
|
|
||||||
}
|
|
||||||
html += "</tr>\n</thead>\n";
|
|
||||||
|
|
||||||
// build rows
|
|
||||||
var rows = body.split('\n');
|
|
||||||
for (i = 0; i < rows.length; i++) {
|
|
||||||
if (rows[i].match(/^\s*$/)) // can apply to final row
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// ensure number of rowCells matches colCount
|
|
||||||
var rowCells = rows[i].split(/ *[|] */);
|
|
||||||
var lenDiff = colCount - rowCells.length;
|
|
||||||
for (var j = 0; j < lenDiff; j++)
|
|
||||||
rowCells.push('');
|
|
||||||
|
|
||||||
html += "<tr>\n";
|
|
||||||
for (j = 0; j < colCount; j++) {
|
|
||||||
var colHtml = convertSpans(trim(rowCells[j]), self);
|
|
||||||
html += [" <td", align[j], ">", colHtml, "</td>\n"].join('');
|
|
||||||
}
|
|
||||||
html += "</tr>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
html += "</table>\n";
|
|
||||||
|
|
||||||
// replace html with placeholder until postConversion step
|
|
||||||
return self.hashExtraBlock(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Footnotes *
|
|
||||||
*****************************************************************/
|
|
||||||
|
|
||||||
// Strip footnote, store in hashes.
|
|
||||||
Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
text = text.replace(
|
|
||||||
/\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g,
|
|
||||||
function(wholeMatch, m1, m2) {
|
|
||||||
m1 = slugify(m1);
|
|
||||||
m2 += "\n";
|
|
||||||
m2 = m2.replace(/^[ ]{0,3}/g, "");
|
|
||||||
self.footnotes[m1] = m2;
|
|
||||||
return "\n";
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Find and convert footnotes references.
|
|
||||||
Markdown.Extra.prototype.doFootnotes = function(text) {
|
|
||||||
var self = this;
|
|
||||||
if(self.isConvertingFootnote === true) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
var footnoteCounter = 0;
|
|
||||||
text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) {
|
|
||||||
var id = slugify(m1);
|
|
||||||
var footnote = self.footnotes[id];
|
|
||||||
if (footnote === undefined) {
|
|
||||||
return wholeMatch;
|
|
||||||
}
|
|
||||||
footnoteCounter++;
|
|
||||||
self.usedFootnotes.push(id);
|
|
||||||
var html = '<a href="#fn:' + id + '" id="fnref:' + id
|
|
||||||
+ '" title="See footnote" class="footnote">' + footnoteCounter
|
|
||||||
+ '</a>';
|
|
||||||
return self.hashExtraInline(html);
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Print footnotes at the end of the document
|
|
||||||
Markdown.Extra.prototype.printFootnotes = function(text) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (self.usedFootnotes.length === 0) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n';
|
|
||||||
for(var i=0; i<self.usedFootnotes.length; i++) {
|
|
||||||
var id = self.usedFootnotes[i];
|
|
||||||
var footnote = self.footnotes[id];
|
|
||||||
self.isConvertingFootnote = true;
|
|
||||||
var formattedfootnote = convertSpans(footnote, self);
|
|
||||||
delete self.isConvertingFootnote;
|
|
||||||
text += '<li id="fn:'
|
|
||||||
+ id
|
|
||||||
+ '">'
|
|
||||||
+ formattedfootnote
|
|
||||||
+ ' <a href="#fnref:'
|
|
||||||
+ id
|
|
||||||
+ '" title="Return to article" class="reversefootnote">↩</a></li>\n\n';
|
|
||||||
}
|
|
||||||
text += '</ol>\n</div>';
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Fenced Code Blocks (gfm) *
|
|
||||||
******************************************************************/
|
|
||||||
|
|
||||||
// Find and convert gfm-inspired fenced code blocks into html.
|
|
||||||
Markdown.Extra.prototype.fencedCodeBlocks = function(text) {
|
|
||||||
function encodeCode(code) {
|
|
||||||
code = code.replace(/&/g, "&");
|
|
||||||
code = code.replace(/</g, "<");
|
|
||||||
code = code.replace(/>/g, ">");
|
|
||||||
// These were escaped by PageDown before postNormalization
|
|
||||||
code = code.replace(/~D/g, "$$");
|
|
||||||
code = code.replace(/~T/g, "~");
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) {
|
|
||||||
var language = trim(m1), codeblock = m2;
|
|
||||||
|
|
||||||
// adhere to specified options
|
|
||||||
var preclass = self.googleCodePrettify ? ' class="prettyprint"' : '';
|
|
||||||
var codeclass = '';
|
|
||||||
if (language) {
|
|
||||||
if (self.googleCodePrettify || self.highlightJs) {
|
|
||||||
// use html5 language- class names. supported by both prettify and highlight.js
|
|
||||||
codeclass = ' class="language-' + language + '"';
|
|
||||||
} else {
|
|
||||||
codeclass = ' class="' + language + '"';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = ['<pre', preclass, '><code', codeclass, '>',
|
|
||||||
encodeCode(codeblock), '</code></pre>'].join('');
|
|
||||||
|
|
||||||
// replace codeblock with placeholder until postConversion step
|
|
||||||
return self.hashExtraBlock(html);
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* SmartyPants *
|
|
||||||
******************************************************************/
|
|
||||||
|
|
||||||
Markdown.Extra.prototype.educatePants = function(text) {
|
|
||||||
var self = this;
|
|
||||||
var result = '';
|
|
||||||
var blockOffset = 0;
|
|
||||||
// Here we parse HTML in a very bad manner
|
|
||||||
text.replace(/(?:<!--[\s\S]*?-->)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) {
|
|
||||||
var token = text.substring(blockOffset, offset);
|
|
||||||
result += self.applyPants(token);
|
|
||||||
self.smartyPantsLastChar = result.substring(result.length - 1);
|
|
||||||
blockOffset = offset + wholeMatch.length;
|
|
||||||
if(!m1) {
|
|
||||||
// Skip commentary
|
|
||||||
result += wholeMatch;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Skip special tags
|
|
||||||
if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) {
|
|
||||||
m4 = self.educatePants(m4);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.smartyPantsLastChar = m4.substring(m4.length - 1);
|
|
||||||
}
|
|
||||||
result += m1 + m2 + m3 + m4 + m5;
|
|
||||||
});
|
|
||||||
var lastToken = text.substring(blockOffset);
|
|
||||||
result += self.applyPants(lastToken);
|
|
||||||
self.smartyPantsLastChar = result.substring(result.length - 1);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
function revertPants(wholeMatch, m1) {
|
|
||||||
var blockText = m1;
|
|
||||||
blockText = blockText.replace(/&\#8220;/g, "\"");
|
|
||||||
blockText = blockText.replace(/&\#8221;/g, "\"");
|
|
||||||
blockText = blockText.replace(/&\#8216;/g, "'");
|
|
||||||
blockText = blockText.replace(/&\#8217;/g, "'");
|
|
||||||
blockText = blockText.replace(/&\#8212;/g, "---");
|
|
||||||
blockText = blockText.replace(/&\#8211;/g, "--");
|
|
||||||
blockText = blockText.replace(/&\#8230;/g, "...");
|
|
||||||
return blockText;
|
|
||||||
}
|
|
||||||
|
|
||||||
Markdown.Extra.prototype.applyPants = function(text) {
|
|
||||||
// Dashes
|
|
||||||
text = text.replace(/---/g, "—").replace(/--/g, "–");
|
|
||||||
// Ellipses
|
|
||||||
text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…");
|
|
||||||
// Backticks
|
|
||||||
text = text.replace(/``/g, "“").replace (/''/g, "”");
|
|
||||||
|
|
||||||
if(/^'$/.test(text)) {
|
|
||||||
// Special case: single-character ' token
|
|
||||||
if(/\S/.test(this.smartyPantsLastChar)) {
|
|
||||||
return "’";
|
|
||||||
}
|
|
||||||
return "‘";
|
|
||||||
}
|
|
||||||
if(/^"$/.test(text)) {
|
|
||||||
// Special case: single-character " token
|
|
||||||
if(/\S/.test(this.smartyPantsLastChar)) {
|
|
||||||
return "”";
|
|
||||||
}
|
|
||||||
return "“";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case if the very first character is a quote
|
|
||||||
// followed by punctuation at a non-word-break. Close the quotes by brute force:
|
|
||||||
text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’");
|
|
||||||
text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”");
|
|
||||||
|
|
||||||
// Special case for double sets of quotes, e.g.:
|
|
||||||
// <p>He said, "'Quoted' words in a larger quote."</p>
|
|
||||||
text = text.replace(/"'(?=\w)/g, "“‘");
|
|
||||||
text = text.replace(/'"(?=\w)/g, "‘“");
|
|
||||||
|
|
||||||
// Special case for decade abbreviations (the '80s):
|
|
||||||
text = text.replace(/'(?=\d{2}s)/g, "’");
|
|
||||||
|
|
||||||
// Get most opening single quotes:
|
|
||||||
text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘");
|
|
||||||
|
|
||||||
// Single closing quotes:
|
|
||||||
text = text.replace(/([^\s\[\{\(\-])'/g, "$1’");
|
|
||||||
text = text.replace(/'(?=\s|s\b)/g, "’");
|
|
||||||
|
|
||||||
// Any remaining single quotes should be opening ones:
|
|
||||||
text = text.replace(/'/g, "‘");
|
|
||||||
|
|
||||||
// Get most opening double quotes:
|
|
||||||
text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“");
|
|
||||||
|
|
||||||
// Double closing quotes:
|
|
||||||
text = text.replace(/([^\s\[\{\(\-])"/g, "$1”");
|
|
||||||
text = text.replace(/"(?=\s)/g, "”");
|
|
||||||
|
|
||||||
// Any remaining quotes should be opening ones.
|
|
||||||
text = text.replace(/"/ig, "“");
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find and convert markdown extra definition lists into html.
|
|
||||||
Markdown.Extra.prototype.runSmartyPants = function(text) {
|
|
||||||
this.smartyPantsLastChar = '';
|
|
||||||
text = this.educatePants(text);
|
|
||||||
// Clean everything inside html tags (some of them may have been converted due to our rough html parsing)
|
|
||||||
text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants);
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************
|
|
||||||
* Definition Lists *
|
|
||||||
******************************************************************/
|
|
||||||
|
|
||||||
// Find and convert markdown extra definition lists into html.
|
|
||||||
Markdown.Extra.prototype.definitionLists = function(text) {
|
|
||||||
var wholeList = new RegExp(
|
|
||||||
['(\\x02\\n?|\\n\\n)' ,
|
|
||||||
'(?:' ,
|
|
||||||
'(' , // $1 = whole list
|
|
||||||
'(' , // $2
|
|
||||||
'[ ]{0,3}' ,
|
|
||||||
'((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
|
|
||||||
'\\n?' ,
|
|
||||||
'[ ]{0,3}:[ ]+' , // colon starting definition
|
|
||||||
')' ,
|
|
||||||
'([\\s\\S]+?)' ,
|
|
||||||
'(' , // $4
|
|
||||||
'(?=\\0x03)' , // \z
|
|
||||||
'|' ,
|
|
||||||
'(?=' ,
|
|
||||||
'\\n{2,}' ,
|
|
||||||
'(?=\\S)' ,
|
|
||||||
'(?!' , // Negative lookahead for another term
|
|
||||||
'[ ]{0,3}' ,
|
|
||||||
'(?:\\S.*\\n)+?' , // defined term
|
|
||||||
'\\n?' ,
|
|
||||||
'[ ]{0,3}:[ ]+' , // colon starting definition
|
|
||||||
')' ,
|
|
||||||
'(?!' , // Negative lookahead for another definition
|
|
||||||
'[ ]{0,3}:[ ]+' , // colon starting definition
|
|
||||||
')' ,
|
|
||||||
')' ,
|
|
||||||
')' ,
|
|
||||||
')' ,
|
|
||||||
')'
|
|
||||||
].join(''),
|
|
||||||
'gm'
|
|
||||||
);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
text = addAnchors(text);
|
|
||||||
|
|
||||||
text = text.replace(wholeList, function(match, pre, list) {
|
|
||||||
var result = trim(self.processDefListItems(list));
|
|
||||||
result = "<dl>\n" + result + "\n</dl>";
|
|
||||||
return pre + self.hashExtraBlock(result) + "\n\n";
|
|
||||||
});
|
|
||||||
|
|
||||||
return removeAnchors(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process the contents of a single definition list, splitting it
|
|
||||||
// into individual term and definition list items.
|
|
||||||
Markdown.Extra.prototype.processDefListItems = function(listStr) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var dt = new RegExp(
|
|
||||||
['(\\x02\\n?|\\n\\n+)' , // leading line
|
|
||||||
'(' , // definition terms = $1
|
|
||||||
'[ ]{0,3}' , // leading whitespace
|
|
||||||
'(?![:][ ]|[ ])' , // negative lookahead for a definition
|
|
||||||
// mark (colon) or more whitespace
|
|
||||||
'(?:\\S.*\\n)+?' , // actual term (not whitespace)
|
|
||||||
')' ,
|
|
||||||
'(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed
|
|
||||||
].join(''), // with a definition mark
|
|
||||||
'gm'
|
|
||||||
);
|
|
||||||
|
|
||||||
var dd = new RegExp(
|
|
||||||
['\\n(\\n+)?' , // leading line = $1
|
|
||||||
'(' , // marker space = $2
|
|
||||||
'[ ]{0,3}' , // whitespace before colon
|
|
||||||
'[:][ ]+' , // definition mark (colon)
|
|
||||||
')' ,
|
|
||||||
'([\\s\\S]+?)' , // definition text = $3
|
|
||||||
'(?=\\n*' , // stop at next definition mark,
|
|
||||||
'(?:' , // next term or end of text
|
|
||||||
'\\n[ ]{0,3}[:][ ]|' ,
|
|
||||||
'<dt>|\\x03' , // \z
|
|
||||||
')' ,
|
|
||||||
')'
|
|
||||||
].join(''),
|
|
||||||
'gm'
|
|
||||||
);
|
|
||||||
|
|
||||||
listStr = addAnchors(listStr);
|
|
||||||
// trim trailing blank lines:
|
|
||||||
listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n");
|
|
||||||
|
|
||||||
// Process definition terms.
|
|
||||||
listStr = listStr.replace(dt, function(match, pre, termsStr) {
|
|
||||||
var terms = trim(termsStr).split("\n");
|
|
||||||
var text = '';
|
|
||||||
for (var i = 0; i < terms.length; i++) {
|
|
||||||
var term = terms[i];
|
|
||||||
// process spans inside dt
|
|
||||||
term = convertSpans(trim(term), self);
|
|
||||||
text += "\n<dt>" + term + "</dt>";
|
|
||||||
}
|
|
||||||
return text + "\n";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process actual definitions.
|
|
||||||
listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) {
|
|
||||||
if (leadingLine || def.match(/\n{2,}/)) {
|
|
||||||
// replace marker with the appropriate whitespace indentation
|
|
||||||
def = Array(markerSpace.length + 1).join(' ') + def;
|
|
||||||
// process markdown inside definition
|
|
||||||
// TODO?: currently doesn't apply extensions
|
|
||||||
def = outdent(def) + "\n\n";
|
|
||||||
def = "\n" + convertAll(def, self) + "\n";
|
|
||||||
} else {
|
|
||||||
// convert span-level markdown inside definition
|
|
||||||
def = rtrim(def);
|
|
||||||
def = convertSpans(outdent(def), self);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\n<dd>" + def + "</dd>\n";
|
|
||||||
});
|
|
||||||
|
|
||||||
return removeAnchors(listStr);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/***********************************************************
|
|
||||||
* Strikethrough *
|
|
||||||
************************************************************/
|
|
||||||
|
|
||||||
Markdown.Extra.prototype.strikethrough = function(text) {
|
|
||||||
// Pretty much duplicated from _DoItalicsAndBold
|
|
||||||
return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g,
|
|
||||||
"$1<del>$2</del>$3");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/***********************************************************
|
|
||||||
* New lines *
|
|
||||||
************************************************************/
|
|
||||||
|
|
||||||
Markdown.Extra.prototype.newlines = function(text) {
|
|
||||||
// We have to ignore already converted newlines and line breaks in sub-list items
|
|
||||||
return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) {
|
|
||||||
return previousTag ? wholeMatch : " <br>\n";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@@ -64,4 +64,13 @@
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// jQuery's show() sets display as 'inline', this utility sets it to whatever we want.
|
||||||
|
// Useful for buttons or links that need 'inline-block' or flex for correct padding and alignment.
|
||||||
|
$.fn.displayAs = function(display_type) {
|
||||||
|
if (typeof(display_type) === 'undefined') {
|
||||||
|
display_type = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.css('display', display_type);
|
||||||
|
}
|
||||||
}(jQuery));
|
}(jQuery));
|
||||||
|
@@ -32,7 +32,7 @@ var DocumentTitleAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/* Status Bar */
|
/* Status Bar * DEPRECATED * USE TOASTR INSTEAD */
|
||||||
function statusBarClear(delay_class, delay_html){
|
function statusBarClear(delay_class, delay_html){
|
||||||
var statusBar = $("#status-bar");
|
var statusBar = $("#status-bar");
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ function statusBarClear(delay_class, delay_html){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status Bar * DEPRECATED - USE TOASTR INSTEAD * */
|
||||||
function statusBarSet(classes, html, icon_name, time){
|
function statusBarSet(classes, html, icon_name, time){
|
||||||
/* Utility to notify the user by temporarily flashing text on the project header
|
/* Utility to notify the user by temporarily flashing text on the project header
|
||||||
Usage:
|
Usage:
|
||||||
|
@@ -66,12 +66,9 @@ function containerResizeY(window_height){
|
|||||||
|
|
||||||
var project_container = document.getElementById('project-container');
|
var project_container = document.getElementById('project-container');
|
||||||
var container_offset = project_container.offsetTop;
|
var container_offset = project_container.offsetTop;
|
||||||
var nav_header_height = $('#project_nav-header').height();
|
|
||||||
var container_height = window_height - container_offset.top;
|
var container_height = window_height - container_offset.top;
|
||||||
var container_height_wheader = window_height - container_offset.top - nav_header_height;
|
var container_height_wheader = window_height - container_offset;
|
||||||
var window_height_minus_nav = window_height - nav_header_height - 1; // 1 is border width
|
var window_height_minus_nav = window_height - container_offset;
|
||||||
|
|
||||||
$('#project_context-header').width($('#project_context-container').width());
|
|
||||||
|
|
||||||
if ($(window).width() > 768) {
|
if ($(window).width() > 768) {
|
||||||
$('#project-container').css(
|
$('#project-container').css(
|
||||||
@@ -79,13 +76,14 @@ function containerResizeY(window_height){
|
|||||||
'height': window_height_minus_nav + 'px'}
|
'height': window_height_minus_nav + 'px'}
|
||||||
);
|
);
|
||||||
|
|
||||||
$('#project_nav-container, #project_tree, .project_split').css(
|
$('#project_nav-container, #project_tree').css(
|
||||||
{'max-height': (window_height_minus_nav - 50) + 'px',
|
{'max-height': (window_height_minus_nav) + 'px',
|
||||||
'height': (window_height_minus_nav - 50) + 'px'}
|
'height': (window_height_minus_nav) + 'px'}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (container_height > parseInt($('#project-container').css("min-height"))) {
|
if (container_height > parseInt($('#project-container').css("min-height"))) {
|
||||||
if (typeof projectTree !== "undefined"){
|
if (typeof projectTree !== "undefined"){
|
||||||
|
|
||||||
$(projectTree).css(
|
$(projectTree).css(
|
||||||
{'max-height': container_height_wheader + 'px',
|
{'max-height': container_height_wheader + 'px',
|
||||||
'height': container_height_wheader + 'px'}
|
'height': container_height_wheader + 'px'}
|
||||||
|
@@ -40,11 +40,6 @@ $(document).on('click','body .comment-action-reply',function(e){
|
|||||||
parentDiv.after(commentForm);
|
parentDiv.after(commentForm);
|
||||||
// document.getElementById('comment_field').focus();
|
// document.getElementById('comment_field').focus();
|
||||||
$(commentField).focus();
|
$(commentField).focus();
|
||||||
|
|
||||||
// Convert Markdown
|
|
||||||
var convert = new Markdown.getSanitizingConverter().makeHtml;
|
|
||||||
var preview = $('.comment-reply-preview-md');
|
|
||||||
preview.html(convert($(commentField).val()));
|
|
||||||
$('.comment-reply-field').addClass('filled');
|
$('.comment-reply-field').addClass('filled');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,10 +54,6 @@ $(document).on('click','body .comment-action-cancel',function(e){
|
|||||||
delete commentField.dataset.originalParentId;
|
delete commentField.dataset.originalParentId;
|
||||||
|
|
||||||
$(commentField).val('');
|
$(commentField).val('');
|
||||||
// Convert Markdown
|
|
||||||
var convert = new Markdown.getSanitizingConverter().makeHtml;
|
|
||||||
var preview = $('.comment-reply-preview-md');
|
|
||||||
preview.html(convert($(commentField).val()));
|
|
||||||
|
|
||||||
$('.comment-reply-field').removeClass('filled');
|
$('.comment-reply-field').removeClass('filled');
|
||||||
$('.comment-container').removeClass('is-replying');
|
$('.comment-container').removeClass('is-replying');
|
||||||
|
202
src/scripts/video_plugins.js
Normal file
202
src/scripts/video_plugins.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/* Video.JS plugin for keeping track of user's viewing progress.
|
||||||
|
Also registers the analytics plugin.
|
||||||
|
|
||||||
|
Progress is reported after a number of seconds or a percentage
|
||||||
|
of the duration of the video, whichever comes first.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
videojs(videoPlayerElement, options).ready(function() {
|
||||||
|
let report_url = '{{ url_for("users_api.set_video_progress", video_id=node._id) }}';
|
||||||
|
this.progressPlugin({'report_url': report_url});
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Report after progressing this many seconds video-time.
|
||||||
|
let PROGRESS_REPORT_INTERVAL_SEC = 30;
|
||||||
|
|
||||||
|
// Report after progressing this percentage of the entire video (scale 0-100).
|
||||||
|
let PROGRESS_REPORT_INTERVAL_PERC = 10;
|
||||||
|
|
||||||
|
// Don't report within this many milliseconds of wall-clock time of the previous report.
|
||||||
|
let PROGRESS_RELAXING_TIME_MSEC = 500;
|
||||||
|
|
||||||
|
|
||||||
|
var Plugin = videojs.getPlugin('plugin');
|
||||||
|
var VideoProgressPlugin = videojs.extend(Plugin, {
|
||||||
|
constructor: function(player, options) {
|
||||||
|
Plugin.call(this, player, options);
|
||||||
|
|
||||||
|
this.last_wallclock_time_ms = 0;
|
||||||
|
this.last_inspected_progress_in_sec = 0;
|
||||||
|
this.last_reported_progress_in_sec = 0;
|
||||||
|
this.last_reported_progress_in_perc = 0;
|
||||||
|
this.report_url = options.report_url;
|
||||||
|
this.fetch_progress_url = options.fetch_progress_url;
|
||||||
|
this.reported_error = false;
|
||||||
|
this.reported_looping = false;
|
||||||
|
|
||||||
|
if (typeof this.report_url === 'undefined' || !this.report_url) {
|
||||||
|
/* If we can't report anything, don't bother registering event handlers. */
|
||||||
|
videojs.log('VideoProgressPlugin: no report_url option given. Not storing video progress.');
|
||||||
|
} else {
|
||||||
|
/* Those events will have 'this' bound to the player,
|
||||||
|
* which is why we explicitly re-bind to 'this''. */
|
||||||
|
player.on('timeupdate', this.on_timeupdate.bind(this));
|
||||||
|
player.on('pause', this.on_pause.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.fetch_progress_url === 'undefined' || !this.fetch_progress_url) {
|
||||||
|
/* If we can't report anything, don't bother registering event handlers. */
|
||||||
|
videojs.log('VideoProgressPlugin: no fetch_progress_url option given. Not restoring video progress.');
|
||||||
|
} else {
|
||||||
|
this.resume_playback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resume_playback: function() {
|
||||||
|
let on_done = function(progress, status, xhr) {
|
||||||
|
/* 'progress' is an object like:
|
||||||
|
{"progress_in_sec": 3,
|
||||||
|
"progress_in_percent": 51,
|
||||||
|
"last_watched": "Fri, 31 Aug 2018 13:53:06 GMT",
|
||||||
|
"done": true}
|
||||||
|
*/
|
||||||
|
switch (xhr.status) {
|
||||||
|
case 204: return; // no info found.
|
||||||
|
case 200:
|
||||||
|
/* Don't do anything when the progress is at 100%.
|
||||||
|
* Moving the current time to the end makes no sense then. */
|
||||||
|
if (progress.progress_in_percent >= 100) return;
|
||||||
|
|
||||||
|
/* Set the 'last reported' props before manipulating the
|
||||||
|
* player, so that the manipulation doesn't trigger more
|
||||||
|
* API calls to remember what we just restored. */
|
||||||
|
this.last_reported_progress_in_sec = progress.progress_in_sec;
|
||||||
|
this.last_reported_progress_in_perc = progress.progress_in_perc;
|
||||||
|
|
||||||
|
console.log("Continuing playback at ", progress.progress_in_percent, "% from", progress.last_watched);
|
||||||
|
this.player.currentTime(progress.progress_in_sec);
|
||||||
|
this.player.play();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
console.log("Unknown code", xhr.status, "getting video progress information.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.get(this.fetch_progress_url)
|
||||||
|
.fail(function(error) {
|
||||||
|
console.log("Unable to fetch video progress information:", xhrErrorResponseMessage(error));
|
||||||
|
})
|
||||||
|
.done(on_done.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Pausing playback should report the progress.
|
||||||
|
* This function is also called when playback stops at the end of the video,
|
||||||
|
* so it's important to report in this case; otherwise progress will never
|
||||||
|
* reach 100%. */
|
||||||
|
on_pause: function(event) {
|
||||||
|
this.inspect_progress(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
on_timeupdate: function() {
|
||||||
|
this.inspect_progress(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
inspect_progress: function(force_report) {
|
||||||
|
// Don't report seeking when paused, only report actual playback.
|
||||||
|
if (!force_report && this.player.paused()) return;
|
||||||
|
|
||||||
|
let now_in_ms = new Date().getTime();
|
||||||
|
if (!force_report && now_in_ms - this.last_wallclock_time_ms < PROGRESS_RELAXING_TIME_MSEC) {
|
||||||
|
// We're trying too fast, don't bother doing any other calculation.
|
||||||
|
// console.log('skipping, already reported', now_in_ms - this.last_wallclock_time_ms, 'ms ago.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress_in_sec = this.player.currentTime();
|
||||||
|
let duration_in_sec = this.player.duration();
|
||||||
|
|
||||||
|
/* Instead of reporting the current time, report reaching the end
|
||||||
|
* of the video. This ensures that it's properly marked as 'done'. */
|
||||||
|
if (!this.reported_looping) {
|
||||||
|
let margin = 1.25 * PROGRESS_RELAXING_TIME_MSEC / 1000.0;
|
||||||
|
let is_looping = progress_in_sec == 0 && duration_in_sec - this.last_inspected_progress_in_sec < margin;
|
||||||
|
this.last_inspected_progress_in_sec = progress_in_sec;
|
||||||
|
if (is_looping) {
|
||||||
|
this.reported_looping = true;
|
||||||
|
this.report(this.player.duration(), 100, now_in_ms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(progress_in_sec - this.last_reported_progress_in_sec) < 0.01) {
|
||||||
|
// Already reported this, don't bother doing it again.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let progress_in_perc = 100 * progress_in_sec / duration_in_sec;
|
||||||
|
let diff_sec = progress_in_sec - this.last_reported_progress_in_sec;
|
||||||
|
let diff_perc = progress_in_perc - this.last_reported_progress_in_perc;
|
||||||
|
|
||||||
|
if (!force_report
|
||||||
|
&& Math.abs(diff_perc) < PROGRESS_REPORT_INTERVAL_PERC
|
||||||
|
&& Math.abs(diff_sec) < PROGRESS_REPORT_INTERVAL_SEC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.report(progress_in_sec, progress_in_perc, now_in_ms);
|
||||||
|
},
|
||||||
|
|
||||||
|
report: function(progress_in_sec, progress_in_perc, now_in_ms) {
|
||||||
|
/* Store when we tried, not when we succeeded. This function can be
|
||||||
|
* called every 15-250 milliseconds, so we don't want to retry with
|
||||||
|
* that frequency. */
|
||||||
|
this.last_wallclock_time_ms = now_in_ms;
|
||||||
|
|
||||||
|
let on_fail = function(error) {
|
||||||
|
/* Don't show (as in: a toastr popup) the error to the user,
|
||||||
|
* as it doesn't impact their ability to play the video.
|
||||||
|
* Also show the error only once, instead of spamming. */
|
||||||
|
if (this.reported_error) return;
|
||||||
|
|
||||||
|
let msg = xhrErrorResponseMessage(error);
|
||||||
|
console.log('Unable to report viewing progress:', msg);
|
||||||
|
this.reported_error = true;
|
||||||
|
};
|
||||||
|
let on_done = function() {
|
||||||
|
this.last_reported_progress_in_sec = progress_in_sec;
|
||||||
|
this.last_reported_progress_in_perc = progress_in_perc;
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post(this.report_url, {
|
||||||
|
progress_in_sec: progress_in_sec,
|
||||||
|
progress_in_perc: Math.round(progress_in_perc),
|
||||||
|
})
|
||||||
|
.fail(on_fail.bind(this))
|
||||||
|
.done(on_done.bind(this));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var RememberVolumePlugin = videojs.extend(Plugin, {
|
||||||
|
constructor: function(player, options) {
|
||||||
|
Plugin.call(this, player, options);
|
||||||
|
player.on('volumechange', this.on_volumechange.bind(this));
|
||||||
|
this.restore_volume();
|
||||||
|
},
|
||||||
|
|
||||||
|
restore_volume: function() {
|
||||||
|
let volume_str = localStorage.getItem('video-player-volume');
|
||||||
|
if (volume_str == null) return;
|
||||||
|
this.player.volume(1.0 * volume_str);
|
||||||
|
},
|
||||||
|
|
||||||
|
on_volumechange: function(event) {
|
||||||
|
localStorage.setItem('video-player-volume', this.player.volume());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Register our watch-progress-bookkeeping plugin.
|
||||||
|
videojs.registerPlugin('progressPlugin', VideoProgressPlugin);
|
||||||
|
videojs.registerPlugin('rememberVolumePlugin', RememberVolumePlugin);
|
@@ -143,12 +143,17 @@ nav.sidebar
|
|||||||
left: 0
|
left: 0
|
||||||
width: $sidebar-width
|
width: $sidebar-width
|
||||||
height: 100%
|
height: 100%
|
||||||
background-color: $color-background-nav
|
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
|
> ul > li > .navbar-item
|
||||||
|
padding-top: 10px
|
||||||
|
padding-bottom: 10px
|
||||||
|
background: red
|
||||||
|
|
||||||
.dropdown
|
.dropdown
|
||||||
min-width: $sidebar-width
|
min-width: $sidebar-width
|
||||||
|
|
||||||
.dropdown-menu
|
.dropdown-menu
|
||||||
top: initial
|
top: initial
|
||||||
bottom: 3px
|
bottom: 3px
|
||||||
@@ -159,7 +164,7 @@ nav.sidebar
|
|||||||
li a
|
li a
|
||||||
justify-content: flex-start
|
justify-content: flex-start
|
||||||
|
|
||||||
ul
|
> ul
|
||||||
width: 100%
|
width: 100%
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
@@ -172,25 +177,11 @@ nav.sidebar
|
|||||||
|
|
||||||
a.navbar-item, button
|
a.navbar-item, button
|
||||||
display: flex
|
display: flex
|
||||||
color: $color-text-light-hint
|
|
||||||
font-size: 1.5em
|
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
padding: 10px 0
|
|
||||||
background: transparent
|
background: transparent
|
||||||
border: none
|
border: none
|
||||||
width: 100%
|
width: 100%
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $color-text-light-primary
|
|
||||||
&:active
|
|
||||||
outline: none
|
|
||||||
|
|
||||||
&.cloud
|
|
||||||
i
|
|
||||||
position: relative
|
|
||||||
left: -4px
|
|
||||||
|
|
||||||
a.dropdown-toggle
|
a.dropdown-toggle
|
||||||
padding: 0
|
padding: 0
|
||||||
@@ -408,3 +399,68 @@ nav.sidebar
|
|||||||
top: -1px
|
top: -1px
|
||||||
left: -19px
|
left: -19px
|
||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
|
$loader-bar-width: 100px
|
||||||
|
$loader-bar-height: 2px
|
||||||
|
.loader-bar
|
||||||
|
bottom: -$loader-bar-height
|
||||||
|
content: ''
|
||||||
|
display: none
|
||||||
|
height: 0
|
||||||
|
overflow: hidden
|
||||||
|
position: absolute
|
||||||
|
visibility: hidden
|
||||||
|
width: 100%
|
||||||
|
z-index: 20
|
||||||
|
|
||||||
|
&:before
|
||||||
|
animation: none
|
||||||
|
background-color: $primary
|
||||||
|
background-image: linear-gradient(to right, $primary-accent, $primary)
|
||||||
|
content: ''
|
||||||
|
display: block
|
||||||
|
height: $loader-bar-height
|
||||||
|
left: -$loader-bar-width
|
||||||
|
position: absolute
|
||||||
|
width: $loader-bar-width
|
||||||
|
|
||||||
|
&.active
|
||||||
|
display: block
|
||||||
|
height: $loader-bar-height
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
|
&:before
|
||||||
|
animation: loader-bar-slide 2s linear infinite
|
||||||
|
|
||||||
|
@keyframes loader-bar-slide
|
||||||
|
from
|
||||||
|
left: -($loader-bar-width / 2)
|
||||||
|
width: 3%
|
||||||
|
|
||||||
|
50%
|
||||||
|
width: 20%
|
||||||
|
|
||||||
|
70%
|
||||||
|
width: 70%
|
||||||
|
|
||||||
|
80%
|
||||||
|
left: 50%
|
||||||
|
|
||||||
|
95%
|
||||||
|
left: 120%
|
||||||
|
|
||||||
|
to
|
||||||
|
left: 100%
|
||||||
|
|
||||||
|
.progress-bar
|
||||||
|
background-color: $primary
|
||||||
|
background-image: linear-gradient(to right, $primary-accent, $primary)
|
||||||
|
|
||||||
|
.node-details-description
|
||||||
|
+node-details-description
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg)
|
||||||
|
max-width: map-get($grid-breakpoints, "md")
|
||||||
|
|
||||||
|
@include media-breakpoint-up(xl)
|
||||||
|
max-width: map-get($grid-breakpoints, "lg")
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
$comments-width-max: 710px
|
$comments-width-max: 710px
|
||||||
|
|
||||||
.comments-container
|
.comments-container
|
||||||
|
max-width: $comments-width-max
|
||||||
position: relative
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
|
||||||
#comments-reload
|
#comments-reload
|
||||||
text-align: center
|
text-align: center
|
||||||
@@ -30,7 +32,7 @@ $comments-width-max: 710px
|
|||||||
.comment-reply-container
|
.comment-reply-container
|
||||||
display: flex
|
display: flex
|
||||||
position: relative
|
position: relative
|
||||||
padding: 15px 0 20px 0
|
padding: 15px 0
|
||||||
transition: background-color 150ms ease-in-out, padding 150ms ease-in-out, margin 150ms ease-in-out
|
transition: background-color 150ms ease-in-out, padding 150ms ease-in-out, margin 150ms ease-in-out
|
||||||
|
|
||||||
&.comment-linked
|
&.comment-linked
|
||||||
@@ -194,8 +196,6 @@ $comments-width-max: 710px
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
font-family: 'pillar-font'
|
font-family: 'pillar-font'
|
||||||
height: 25px
|
height: 25px
|
||||||
position: relative
|
|
||||||
top: 4px
|
|
||||||
width: 16px
|
width: 16px
|
||||||
|
|
||||||
.comment-action-rating.up
|
.comment-action-rating.up
|
||||||
@@ -284,10 +284,11 @@ $comments-width-max: 710px
|
|||||||
color: $color-success
|
color: $color-success
|
||||||
|
|
||||||
&.is-replying
|
&.is-replying
|
||||||
box-shadow: inset 5px 0 0 $color-primary
|
box-shadow: -5px 0 0 $color-primary
|
||||||
|
@extend .pl-3
|
||||||
|
|
||||||
&.is-replying+.comment-reply-container
|
&.is-replying+.comment-reply-container
|
||||||
box-shadow: inset 5px 0 0 $color-primary
|
box-shadow: -5px 0 0 $color-primary
|
||||||
margin-left: 0
|
margin-left: 0
|
||||||
padding-left: 55px
|
padding-left: 55px
|
||||||
|
|
||||||
@@ -314,9 +315,6 @@ $comments-width-max: 710px
|
|||||||
color: $color-success
|
color: $color-success
|
||||||
|
|
||||||
.comment-reply
|
.comment-reply
|
||||||
&-container
|
|
||||||
background-color: $color-background
|
|
||||||
|
|
||||||
/* Little gravatar icon on the left */
|
/* Little gravatar icon on the left */
|
||||||
&-avatar
|
&-avatar
|
||||||
img
|
img
|
||||||
@@ -333,7 +331,7 @@ $comments-width-max: 710px
|
|||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
&-field
|
&-field
|
||||||
background-color: $color-background-dark
|
background-color: $color-background-light
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
box-shadow: inset 0 0 2px 0 rgba(darken($color-background-dark, 20%), .5)
|
box-shadow: inset 0 0 2px 0 rgba(darken($color-background-dark, 20%), .5)
|
||||||
display: flex
|
display: flex
|
||||||
@@ -342,6 +340,7 @@ $comments-width-max: 710px
|
|||||||
|
|
||||||
textarea
|
textarea
|
||||||
+node-details-description
|
+node-details-description
|
||||||
|
background-color: $color-background-light
|
||||||
border-bottom-right-radius: 0
|
border-bottom-right-radius: 0
|
||||||
border-top-right-radius: 0
|
border-top-right-radius: 0
|
||||||
border: none
|
border: none
|
||||||
@@ -376,7 +375,6 @@ $comments-width-max: 710px
|
|||||||
|
|
||||||
&.filled
|
&.filled
|
||||||
textarea
|
textarea
|
||||||
background-color: $color-background-light
|
|
||||||
border-bottom: thin solid $color-background
|
border-bottom: thin solid $color-background
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
@@ -453,12 +451,17 @@ $comments-width-max: 710px
|
|||||||
transition: background-color 150ms ease-in-out, color 150ms ease-in-out
|
transition: background-color 150ms ease-in-out, color 150ms ease-in-out
|
||||||
width: 100px
|
width: 100px
|
||||||
|
|
||||||
|
// The actual button for submitting the comment.
|
||||||
button.comment-action-submit
|
button.comment-action-submit
|
||||||
|
align-items: center
|
||||||
background: transparent
|
background: transparent
|
||||||
border: none
|
border: none
|
||||||
border-top-left-radius: 0
|
border-top-left-radius: 0
|
||||||
border-bottom-left-radius: 0
|
border-bottom-left-radius: 0
|
||||||
color: $color-success
|
color: $color-success
|
||||||
|
cursor: pointer
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
height: 100%
|
height: 100%
|
||||||
position: relative
|
position: relative
|
||||||
@@ -466,8 +469,12 @@ $comments-width-max: 710px
|
|||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba($color-success, .1)
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
background: lighten($color-success, 10%)
|
background: lighten($color-success, 10%)
|
||||||
|
color: $white
|
||||||
|
|
||||||
&.submitting
|
&.submitting
|
||||||
color: $color-info
|
color: $color-info
|
||||||
|
@@ -12,9 +12,10 @@ $color-background-active-dark: hsl(hue($color-background-active), 50%, 50%) !def
|
|||||||
$font-body: 'Roboto' !default
|
$font-body: 'Roboto' !default
|
||||||
$font-headings: 'Lato' !default
|
$font-headings: 'Lato' !default
|
||||||
$font-size: 14px !default
|
$font-size: 14px !default
|
||||||
|
$font-size-xs: .75rem
|
||||||
|
$font-size-xxs: .65rem
|
||||||
|
|
||||||
$color-text: #4d4e53 !default
|
$color-text: #4d4e53 !default
|
||||||
|
|
||||||
$color-text-dark: $color-text !default
|
$color-text-dark: $color-text !default
|
||||||
$color-text-dark-primary: #646469 !default
|
$color-text-dark-primary: #646469 !default
|
||||||
$color-text-dark-secondary: #9E9FA2 !default
|
$color-text-dark-secondary: #9E9FA2 !default
|
||||||
@@ -25,10 +26,11 @@ $color-text-light-primary: rgba($color-text-light, .87) !default
|
|||||||
$color-text-light-secondary: rgba($color-text-light, .54) !default
|
$color-text-light-secondary: rgba($color-text-light, .54) !default
|
||||||
$color-text-light-hint: rgba($color-text-light, .38) !default
|
$color-text-light-hint: rgba($color-text-light, .38) !default
|
||||||
|
|
||||||
$color-primary: #68B3C8 !default
|
$color-primary: #009eff !default
|
||||||
$color-primary-light: hsl(hue($color-primary), 30%, 90%) !default
|
$color-primary-light: hsl(hue($color-primary), 30%, 90%) !default
|
||||||
$color-primary-dark: hsl(hue($color-primary), 80%, 30%) !default
|
$color-primary-dark: hsl(hue($color-primary), 80%, 30%) !default
|
||||||
$color-primary-accent: hsl(hue($color-primary), 100%, 50%) !default
|
$color-primary-accent: hsl(hue($color-primary), 100%, 50%) !default
|
||||||
|
$primary-accent: #0bd
|
||||||
|
|
||||||
$color-secondary: #f42942 !default
|
$color-secondary: #f42942 !default
|
||||||
$color-secondary-light: hsl(hue($color-secondary), 30%, 90%) !default
|
$color-secondary-light: hsl(hue($color-secondary), 30%, 90%) !default
|
||||||
@@ -96,16 +98,17 @@ $screen-xs-max: $screen-sm-min - 1 !default
|
|||||||
$screen-sm-max: $screen-md-min - 1 !default
|
$screen-sm-max: $screen-md-min - 1 !default
|
||||||
$screen-md-max: $screen-lg-min - 1 !default
|
$screen-md-max: $screen-lg-min - 1 !default
|
||||||
|
|
||||||
$sidebar-width: 50px !default
|
$sidebar-width: 40px !default
|
||||||
|
|
||||||
/* Project specifics */
|
/* Project specifics */
|
||||||
$project_nav-width: 250px !default
|
$project_nav-width: 250px !default
|
||||||
$project-sidebar-width: 50px !default
|
$project_nav-width-xl: $project_nav-width * 1.4 !default
|
||||||
$project_header-height: 50px !default
|
$project_nav-width-lg: $project_nav-width * 1.2 !default
|
||||||
$project_footer-height: 30px !default
|
$project_nav-width-md: $project_nav-width
|
||||||
|
$project_nav-width-sm: $project_nav-width * 0.8 !default
|
||||||
$navbar-height: 50px !default
|
$project_nav-width-xs: 100% !default
|
||||||
$navbar-backdrop-height: 600px !default
|
$project-sidebar-width: 40px !default
|
||||||
|
$project_header-height: 40px !default
|
||||||
|
|
||||||
$node-type-asset_image: #e87d86 !default
|
$node-type-asset_image: #e87d86 !default
|
||||||
$node-type-asset_file: #CC91C7 !default
|
$node-type-asset_file: #CC91C7 !default
|
||||||
@@ -125,3 +128,39 @@ $z-index-base: 13 !default
|
|||||||
|
|
||||||
@media (min-width: $screen-lg-min)
|
@media (min-width: $screen-lg-min)
|
||||||
width: 1270px
|
width: 1270px
|
||||||
|
|
||||||
|
|
||||||
|
// Bootstrap overrides.
|
||||||
|
$enable-caret: false
|
||||||
|
|
||||||
|
$border-radius: .2rem
|
||||||
|
$btn-border-radius: $border-radius
|
||||||
|
|
||||||
|
$primary: $color-primary
|
||||||
|
|
||||||
|
$body-bg: $white
|
||||||
|
$body-color: $color-text
|
||||||
|
|
||||||
|
$color-background-nav: #fff
|
||||||
|
$link-color: $primary
|
||||||
|
|
||||||
|
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"
|
||||||
|
$font-size-base: .9rem
|
||||||
|
|
||||||
|
$dropdown-border-width: 0
|
||||||
|
$dropdown-box-shadow: 0 10px 25px rgba($black, .1)
|
||||||
|
$dropdown-padding-y: 0
|
||||||
|
$dropdown-item-padding-y: .4rem
|
||||||
|
|
||||||
|
// Tooltips.
|
||||||
|
$tooltip-font-size: 0.83rem
|
||||||
|
$tooltip-max-width: auto
|
||||||
|
$tooltip-opacity: 1
|
||||||
|
|
||||||
|
$nav-link-height: 37px
|
||||||
|
$navbar-padding-x: 0
|
||||||
|
$navbar-padding-y: 0
|
||||||
|
|
||||||
|
$btn-padding-y-sm: 0.1rem
|
||||||
|
|
||||||
|
$grid-breakpoints: (xs: 0,sm: 576px,md: 768px,lg: 1060px,xl: 1500px, xxl: 1800px)
|
||||||
|
@@ -60,14 +60,13 @@
|
|||||||
|
|
||||||
#node-overlay
|
#node-overlay
|
||||||
#error-container
|
#error-container
|
||||||
position: fixed
|
|
||||||
top: $navbar-height
|
|
||||||
align-items: flex-start
|
align-items: flex-start
|
||||||
|
position: fixed
|
||||||
|
top: $nav-link-height
|
||||||
|
|
||||||
#error-box
|
#error-box
|
||||||
box-shadow: 0 0 25px rgba(black, .1), 0 0 50px rgba(black, .1)
|
|
||||||
width: auto
|
|
||||||
border-top-left-radius: 0
|
border-top-left-radius: 0
|
||||||
border-top-right-radius: 0
|
border-top-right-radius: 0
|
||||||
|
box-shadow: 0 0 25px rgba(black, .1), 0 0 50px rgba(black, .1)
|
||||||
position: relative
|
position: relative
|
||||||
width: 100%
|
width: 100%
|
||||||
|
@@ -9,7 +9,6 @@
|
|||||||
color: $color-primary
|
color: $color-primary
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
float: right
|
float: right
|
||||||
font-family: $font-body
|
|
||||||
height: initial
|
height: initial
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 8px 10px 0 10px
|
padding: 8px 10px 0 10px
|
||||||
@@ -25,13 +24,16 @@
|
|||||||
color: $color-secondary
|
color: $color-secondary
|
||||||
|
|
||||||
#notifications-toggle
|
#notifications-toggle
|
||||||
|
color: $color-text
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
font-size: 1.5em
|
|
||||||
position: relative
|
position: relative
|
||||||
user-select: none
|
user-select: none
|
||||||
|
|
||||||
> i:before
|
> i:before
|
||||||
content: '\e815'
|
content: '\e815'
|
||||||
|
font-size: 1.3em
|
||||||
|
position: relative
|
||||||
|
top: 2px
|
||||||
|
|
||||||
&.has-notifications
|
&.has-notifications
|
||||||
> i:before
|
> i:before
|
||||||
@@ -46,10 +48,10 @@
|
|||||||
border-color: transparent transparent $color-background transparent
|
border-color: transparent transparent $color-background transparent
|
||||||
border-style: solid
|
border-style: solid
|
||||||
border-width: 0 8px 8px 8px
|
border-width: 0 8px 8px 8px
|
||||||
bottom: -15px
|
bottom: -10px
|
||||||
height: 0
|
height: 0
|
||||||
position: absolute
|
position: absolute
|
||||||
right: 22px
|
right: 7px
|
||||||
visibility: hidden
|
visibility: hidden
|
||||||
width: 0
|
width: 0
|
||||||
|
|
||||||
|
@@ -1,20 +1,4 @@
|
|||||||
body.organizations
|
body.organizations
|
||||||
ul#sub-nav-tabs__list
|
|
||||||
align-items: center
|
|
||||||
display: flex
|
|
||||||
|
|
||||||
li.result
|
|
||||||
padding: 10px 20px
|
|
||||||
li.create
|
|
||||||
margin-left: auto
|
|
||||||
|
|
||||||
|
|
||||||
.dashboard-secondary
|
|
||||||
.box
|
|
||||||
+container-box
|
|
||||||
padding: 10px 20px
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
#item-details
|
#item-details
|
||||||
.organization
|
.organization
|
||||||
label
|
label
|
||||||
|
@@ -409,7 +409,6 @@ a.page-card-cta
|
|||||||
display: block
|
display: block
|
||||||
+position-center-translate
|
+position-center-translate
|
||||||
|
|
||||||
|
|
||||||
+media-xs
|
+media-xs
|
||||||
display: none
|
display: none
|
||||||
+media-sm
|
+media-sm
|
||||||
@@ -419,9 +418,6 @@ a.page-card-cta
|
|||||||
+media-lg
|
+media-lg
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.services.navbar-backdrop-overlay
|
|
||||||
background: rgba(black, .5)
|
|
||||||
|
|
||||||
.services
|
.services
|
||||||
.page-card-side
|
.page-card-side
|
||||||
max-width: 500px
|
max-width: 500px
|
||||||
|
@@ -1,22 +1,16 @@
|
|||||||
|
|
||||||
.dashboard-container
|
.dashboard-container
|
||||||
section#home,
|
section#home,
|
||||||
section#projects
|
section#projects
|
||||||
background-color: $color-background
|
|
||||||
border-bottom-left-radius: 3px
|
border-bottom-left-radius: 3px
|
||||||
border-bottom-right-radius: 3px
|
border-bottom-right-radius: 3px
|
||||||
|
|
||||||
nav#sub-nav-tabs.home,
|
nav#sub-nav-tabs.home,
|
||||||
nav#sub-nav-tabs.projects
|
nav#sub-nav-tabs.projects
|
||||||
background-color: white
|
|
||||||
border-bottom: thin solid $color-background-dark
|
border-bottom: thin solid $color-background-dark
|
||||||
|
|
||||||
li.nav-tabs__list-tab
|
li.nav-tabs__list-tab
|
||||||
padding: 15px 20px 10px 20px
|
padding: 15px 20px 10px 20px
|
||||||
|
|
||||||
section#home
|
|
||||||
background-color: $color-background-dark
|
|
||||||
|
|
||||||
nav.nav-tabs__tab
|
nav.nav-tabs__tab
|
||||||
display: none
|
display: none
|
||||||
background-color: $color-background-light
|
background-color: $color-background-light
|
||||||
@@ -287,9 +281,8 @@
|
|||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
.title
|
.title
|
||||||
font-size: 1.2em
|
|
||||||
padding-bottom: 2px
|
|
||||||
color: $color-text-dark-primary
|
color: $color-text-dark-primary
|
||||||
|
padding-bottom: 2px
|
||||||
|
|
||||||
ul.meta
|
ul.meta
|
||||||
font-size: .9em
|
font-size: .9em
|
||||||
|
@@ -92,19 +92,6 @@ ul.sharing-users-list
|
|||||||
&:hover
|
&:hover
|
||||||
color: lighten($color-danger, 10%)
|
color: lighten($color-danger, 10%)
|
||||||
|
|
||||||
.sharing-users-intro,
|
|
||||||
.sharing-users-info
|
|
||||||
h4
|
|
||||||
font-family: $font-body
|
|
||||||
|
|
||||||
.sharing-users-info
|
|
||||||
padding-left: 15px
|
|
||||||
border-left: thin solid $color-text-dark-hint
|
|
||||||
|
|
||||||
p
|
|
||||||
font:
|
|
||||||
size: 1.1em
|
|
||||||
weight: 300
|
|
||||||
|
|
||||||
.sharing-users-search
|
.sharing-users-search
|
||||||
.disabled
|
.disabled
|
||||||
@@ -162,24 +149,26 @@ ul.list-generic
|
|||||||
list-style: none
|
list-style: none
|
||||||
|
|
||||||
> li
|
> li
|
||||||
padding: 5px 0
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
align-items: center
|
||||||
border-top: thin solid $color-background
|
border-top: thin solid $color-background
|
||||||
|
display: flex
|
||||||
|
padding: 5px 0
|
||||||
|
|
||||||
&:first-child
|
&:first-child
|
||||||
border-top: none
|
border-top: none
|
||||||
|
|
||||||
&:hover .item a
|
&:hover .item a
|
||||||
color: $color-primary
|
color: $primary
|
||||||
|
|
||||||
a
|
a
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
|
&.active
|
||||||
|
color: $primary !important
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
margin-left: auto
|
margin-left: auto
|
||||||
.btn
|
|
||||||
font-size: .7em
|
|
||||||
|
|
||||||
span
|
span
|
||||||
color: $color-text-dark-secondary
|
color: $color-text-dark-secondary
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ $search-hit-width_grid: 100px
|
|||||||
.search-hit-name
|
.search-hit-name
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
padding-top: 8px
|
padding-top: 8px
|
||||||
color: $color-primary-dark
|
color: $primary
|
||||||
|
|
||||||
.search-hit
|
.search-hit
|
||||||
padding: 0
|
padding: 0
|
||||||
@@ -29,14 +29,13 @@ $search-hit-width_grid: 100px
|
|||||||
font:
|
font:
|
||||||
size: .9em
|
size: .9em
|
||||||
weight: 400
|
weight: 400
|
||||||
family: $font-body
|
|
||||||
style: initial
|
style: initial
|
||||||
width: 100%
|
width: 100%
|
||||||
+text-overflow-ellipsis
|
+text-overflow-ellipsis
|
||||||
+clearfix
|
+clearfix
|
||||||
|
|
||||||
& em
|
& em
|
||||||
color: $color-primary-dark
|
color: $primary
|
||||||
font-style: normal
|
font-style: normal
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
@@ -71,7 +70,7 @@ $search-hit-width_grid: 100px
|
|||||||
min-width: 350px
|
min-width: 350px
|
||||||
border-bottom-left-radius: 3px
|
border-bottom-left-radius: 3px
|
||||||
border-bottom-right-radius: 3px
|
border-bottom-right-radius: 3px
|
||||||
border-top: 3px solid lighten($color-primary, 5%)
|
border-top: 3px solid lighten($primary, 5%)
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
||||||
.tt-suggestion
|
.tt-suggestion
|
||||||
@@ -93,235 +92,68 @@ $search-hit-width_grid: 100px
|
|||||||
&.tt-cursor:hover .search-hit
|
&.tt-cursor:hover .search-hit
|
||||||
background-color: lighten($color-background, 5%)
|
background-color: lighten($color-background, 5%)
|
||||||
|
|
||||||
#search-container
|
.search-list
|
||||||
display: flex
|
width: 50%
|
||||||
min-height: 600px
|
|
||||||
background-color: white
|
|
||||||
|
|
||||||
+media-lg
|
.embed-responsive
|
||||||
padding-left: 0
|
width: 100px
|
||||||
padding-right: 0
|
min-width: 100px
|
||||||
|
|
||||||
#search-sidebar
|
.card-deck.card-deck-vertical
|
||||||
width: 20%
|
.card
|
||||||
background-color: $color-background-light
|
flex-wrap: initial
|
||||||
|
|
||||||
+media-lg
|
.search-settings
|
||||||
border-top-left-radius: 3px
|
width: 30%
|
||||||
|
|
||||||
input.search-field
|
.card-deck.card-deck-vertical
|
||||||
background-color: $color-background-nav-dark
|
.card .embed-responsive
|
||||||
font-size: 1.1em
|
max-width: 80px
|
||||||
color: white
|
|
||||||
margin-bottom: 10px
|
input.search-field
|
||||||
|
border: none
|
||||||
|
border-bottom: 2px solid rgba($primary, .2)
|
||||||
|
border-radius: 0
|
||||||
|
width: 100%
|
||||||
|
transition: border 100ms ease-in-out
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: $color-text-dark-secondary
|
||||||
|
&:placeholder-shown
|
||||||
|
border-bottom-color: $primary
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
border: none
|
border: none
|
||||||
border-bottom: 2px solid rgba($color-primary, .2)
|
border-bottom: 2px solid lighten($primary, 5%)
|
||||||
border-radius: 0
|
|
||||||
width: 100%
|
|
||||||
padding: 5px 15px
|
|
||||||
height: 50px
|
|
||||||
transition: border 100ms ease-in-out
|
|
||||||
|
|
||||||
&::placeholder
|
.search-details
|
||||||
color: $color-text-dark-secondary
|
width: 70%
|
||||||
&:placeholder-shown
|
|
||||||
border-bottom-color: $color-primary
|
|
||||||
|
|
||||||
&:focus
|
.container-fluid .col-md-8
|
||||||
outline: none
|
flex: 1
|
||||||
border: none
|
max-width: 100%
|
||||||
border-bottom: 2px solid lighten($color-primary, 5%)
|
width: 100%
|
||||||
|
|
||||||
.search-list-filters
|
#search-details
|
||||||
padding:
|
position: relative
|
||||||
left: 10px
|
|
||||||
right: 10px
|
|
||||||
|
|
||||||
.panel.panel-default
|
#search-hit-container
|
||||||
margin-bottom: 10px
|
position: absolute // for scrollbars
|
||||||
border-radius: 3px
|
|
||||||
border: none
|
|
||||||
background-color: white
|
|
||||||
box-shadow: 1px 1px 0 rgba(black, .1)
|
|
||||||
|
|
||||||
a
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
.toggleRefine
|
|
||||||
display: block
|
|
||||||
padding-left: 7px
|
|
||||||
color: $color-text-dark
|
|
||||||
text-transform: capitalize
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration: none
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
&.refined
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $color-danger
|
|
||||||
|
|
||||||
span
|
|
||||||
&:before
|
|
||||||
/* x icon */
|
|
||||||
content: '\e84b'
|
|
||||||
font-family: 'pillar-font'
|
|
||||||
span
|
|
||||||
&:before
|
|
||||||
/* circle with dot */
|
|
||||||
content: '\e82f'
|
|
||||||
font-family: 'pillar-font'
|
|
||||||
position: relative
|
|
||||||
left: -7px
|
|
||||||
font-size: .9em
|
|
||||||
|
|
||||||
span
|
|
||||||
&:before
|
|
||||||
/* empty circle */
|
|
||||||
content: '\e82c'
|
|
||||||
font-family: 'pillar-font'
|
|
||||||
position: relative
|
|
||||||
left: -7px
|
|
||||||
font-size: .9em
|
|
||||||
.facet_count
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
|
|
||||||
|
|
||||||
.panel-title, .panel-heading
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
font:
|
|
||||||
size: 1em
|
|
||||||
weight: 500
|
|
||||||
|
|
||||||
.panel-body
|
|
||||||
padding-top: 0
|
|
||||||
|
|
||||||
.panel-title
|
|
||||||
position: relative
|
|
||||||
&:after
|
|
||||||
content: '\e83b'
|
|
||||||
font-family: 'pillar-font'
|
|
||||||
position: absolute
|
|
||||||
right: 0
|
|
||||||
color: $color-text-dark-primary
|
|
||||||
|
|
||||||
.collapsed
|
|
||||||
.panel-title:after
|
|
||||||
content: '\e838'
|
|
||||||
|
|
||||||
|
|
||||||
.search-list-stats
|
|
||||||
color: $color-text-dark-hint
|
|
||||||
padding: 10px 15px 0 15px
|
|
||||||
text-align: center
|
|
||||||
font-size: .9em
|
|
||||||
+clearfix
|
|
||||||
|
|
||||||
#pagination
|
|
||||||
ul.search-pagination
|
|
||||||
text-align: center
|
|
||||||
list-style-type: none
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
width: 100%
|
|
||||||
display: flex
|
|
||||||
+clearfix
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline-block
|
|
||||||
margin: 5px auto
|
|
||||||
|
|
||||||
&:last-child
|
|
||||||
border-color: transparent
|
|
||||||
|
|
||||||
a
|
|
||||||
font-weight: 500
|
|
||||||
padding: 5px 4px
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $color-text-dark-primary
|
|
||||||
|
|
||||||
&.disabled
|
|
||||||
opacity: .6
|
|
||||||
|
|
||||||
&.active a
|
|
||||||
color: $color-text-dark-primary
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
#search-list
|
|
||||||
width: 40%
|
|
||||||
height: 100%
|
|
||||||
padding: 0
|
|
||||||
position: relative
|
|
||||||
overflow-x: hidden
|
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
|
||||||
#hits
|
#error_container
|
||||||
position: relative
|
position: relative
|
||||||
width: 100%
|
background: white
|
||||||
|
padding: 20px
|
||||||
|
|
||||||
#no-hits
|
#search-error
|
||||||
padding: 10px 15px
|
display: none
|
||||||
color: $color-text-dark-secondary
|
margin: 20px auto
|
||||||
|
color: $color-danger
|
||||||
.search-hit
|
text-align: center
|
||||||
#search-loading
|
|
||||||
visibility: hidden
|
|
||||||
background-color: transparent
|
|
||||||
font:
|
|
||||||
size: 1.5em
|
|
||||||
weight: 600
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
bottom: 0
|
|
||||||
z-index: $z-index-base + 5
|
|
||||||
opacity: 0
|
|
||||||
cursor: default
|
|
||||||
transition: opacity 50ms ease-in-out
|
|
||||||
&.active
|
|
||||||
visibility: visible
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
.spinner
|
|
||||||
color: $color-background-nav
|
|
||||||
background-color: white
|
|
||||||
padding: 0
|
|
||||||
width: 20px
|
|
||||||
height: 20px
|
|
||||||
border-radius: 50%
|
|
||||||
position: absolute
|
|
||||||
top: 7px
|
|
||||||
right: 10px
|
|
||||||
span
|
|
||||||
padding: 5px
|
|
||||||
+pulse
|
|
||||||
|
|
||||||
#search-details
|
|
||||||
position: relative
|
|
||||||
width: 40%
|
|
||||||
border-left: 2px solid darken(white, 3%)
|
|
||||||
|
|
||||||
#search-hit-container
|
|
||||||
position: absolute // for scrollbars
|
|
||||||
width: 100%
|
|
||||||
overflow-y: auto
|
|
||||||
|
|
||||||
#error_container
|
|
||||||
position: relative
|
|
||||||
background: white
|
|
||||||
padding: 20px
|
|
||||||
|
|
||||||
#search-error
|
|
||||||
display: none
|
|
||||||
margin: 20px auto
|
|
||||||
color: $color-danger
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
|
#search-container
|
||||||
#node-container
|
#node-container
|
||||||
width: 100%
|
width: 100%
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
@@ -416,9 +248,7 @@ $search-hit-width_grid: 100px
|
|||||||
|
|
||||||
&.texture
|
&.texture
|
||||||
.texture-title
|
.texture-title
|
||||||
font:
|
font-size: 2em
|
||||||
size: 2em
|
|
||||||
family: $font-body
|
|
||||||
padding: 15px 10px 10px 15px
|
padding: 15px 10px 10px 15px
|
||||||
.node-row
|
.node-row
|
||||||
background: white
|
background: white
|
||||||
@@ -476,215 +306,88 @@ $search-hit-width_grid: 100px
|
|||||||
button
|
button
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.search-hit
|
.search-project
|
||||||
float: left
|
li.project
|
||||||
box-shadow: none
|
display: none
|
||||||
border: thin solid transparent
|
|
||||||
border-top-color: darken(white, 8%)
|
|
||||||
border-left: 3px solid transparent
|
|
||||||
|
|
||||||
color: $color-background-nav
|
#search-sidebar
|
||||||
|
.toggleRefine
|
||||||
width: 100%
|
display: block
|
||||||
position: relative
|
padding-left: 7px
|
||||||
margin: 0
|
color: $color-text-dark
|
||||||
padding: 7px 10px 7px 10px
|
text-transform: capitalize
|
||||||
+clearfix
|
|
||||||
|
|
||||||
&:first-child
|
|
||||||
border: thin solid transparent
|
|
||||||
border-left: 3px solid transparent
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
opacity: 1
|
|
||||||
text-decoration: none
|
|
||||||
cursor: default
|
|
||||||
color: darken($color-primary, 20%)
|
|
||||||
background-color: $color-background-light
|
|
||||||
|
|
||||||
& .search-hit-name i
|
|
||||||
color: darken($color-primary, 20%)
|
|
||||||
|
|
||||||
& .search-hit-thumbnail
|
|
||||||
& .search-hit-thumbnail-icon
|
|
||||||
transform: translate(-50%, -50%) scale(1.1)
|
|
||||||
|
|
||||||
.search-hit-name
|
|
||||||
text-decoration: none
|
|
||||||
&:hover
|
|
||||||
color: darken($color-primary, 10%)
|
|
||||||
|
|
||||||
.search-hit-thumbnail
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.search-hit-thumbnail-icon
|
|
||||||
transform: translate(-50%, -50%) scale(1)
|
|
||||||
|
|
||||||
&:active
|
|
||||||
background-color: rgba($color-background, .5)
|
|
||||||
opacity: .8
|
|
||||||
color: $color-primary
|
|
||||||
& .search-hit-name i
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
border-color: rgba($color-primary, .2)
|
|
||||||
|
|
||||||
/* Class that gets added when we click on the item */
|
|
||||||
&.active
|
|
||||||
background-color: lighten($color-background, 2%)
|
|
||||||
border-left: 3px solid $color-primary
|
|
||||||
|
|
||||||
.search-hit-name
|
|
||||||
color: darken($color-primary, 10%)
|
|
||||||
|
|
||||||
.search-hit-meta
|
|
||||||
span.when
|
|
||||||
display: none
|
|
||||||
span.context
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
.search-hit-thumbnail
|
|
||||||
position: relative
|
|
||||||
float: left
|
|
||||||
min-width: $search-hit-width_list * 1.49
|
|
||||||
max-width: $search-hit-width_list * 1.49
|
|
||||||
height: $search-hit-width_list
|
|
||||||
border-radius: 3px
|
|
||||||
background: $color-background
|
|
||||||
margin-right: 12px
|
|
||||||
text-align: center
|
|
||||||
overflow: hidden
|
|
||||||
+media-xs
|
|
||||||
display: none
|
|
||||||
+media-sm
|
|
||||||
min-width: $search-hit-width_list
|
|
||||||
max-width: $search-hit-width_list
|
|
||||||
|
|
||||||
img
|
|
||||||
height: $search-hit-width_list
|
|
||||||
width: auto
|
|
||||||
|
|
||||||
|
|
||||||
.pi-video:before, .pi-file:before,
|
|
||||||
.pi-group:before
|
|
||||||
font-family: 'pillar-font'
|
|
||||||
.pi-video:before
|
|
||||||
content: '\e81d'
|
|
||||||
.pi-file:before
|
|
||||||
content: '\e825'
|
|
||||||
.pi-group:before
|
|
||||||
content: '\e80d'
|
|
||||||
|
|
||||||
.search-hit-thumbnail-icon
|
|
||||||
position: absolute
|
|
||||||
top: 50%
|
|
||||||
left: 50%
|
|
||||||
transform: translate(-50%, -50%)
|
|
||||||
color: white
|
|
||||||
font-size: 1.2em
|
|
||||||
transition: none
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
|
|
||||||
.dark
|
|
||||||
text-shadow: none
|
|
||||||
font-size: 1.3em
|
|
||||||
|
|
||||||
.search-hit-name
|
|
||||||
position: relative
|
|
||||||
font-size: 1.1em
|
|
||||||
color: $color-text-dark-primary
|
|
||||||
background-color: initial
|
|
||||||
width: initial
|
|
||||||
max-width: initial
|
|
||||||
+text-overflow-ellipsis
|
|
||||||
padding-top: 5px
|
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
cursor: pointer
|
text-decoration: none
|
||||||
text-decoration: underline
|
color: $primary
|
||||||
|
|
||||||
em
|
&.refined
|
||||||
color: darken($color-primary, 15%)
|
color: $primary
|
||||||
font-style: normal
|
|
||||||
|
|
||||||
.search-hit-ribbon
|
&:hover
|
||||||
+ribbon
|
color: $color-danger
|
||||||
right: -30px
|
|
||||||
top: 5px
|
span
|
||||||
|
&:before
|
||||||
|
/* x icon */
|
||||||
|
content: '\e84b'
|
||||||
|
font-family: 'pillar-font'
|
||||||
|
span
|
||||||
|
&:before
|
||||||
|
/* circle with dot */
|
||||||
|
content: '\e82f'
|
||||||
|
font-family: 'pillar-font'
|
||||||
|
position: relative
|
||||||
|
left: -7px
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
span
|
span
|
||||||
font-size: 60%
|
&:before
|
||||||
margin: 1px 0
|
/* empty circle */
|
||||||
padding: 2px 35px
|
content: '\e82c'
|
||||||
|
font-family: 'pillar-font'
|
||||||
|
position: relative
|
||||||
|
left: -7px
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
.search-hit-meta
|
.search-list-stats
|
||||||
position: relative
|
color: $color-text-dark-hint
|
||||||
|
padding: 10px 15px 0 15px
|
||||||
|
text-align: center
|
||||||
font-size: .9em
|
font-size: .9em
|
||||||
color: $color-text-dark-secondary
|
+clearfix
|
||||||
background-color: initial
|
|
||||||
padding: 3px 0 0 0
|
|
||||||
text-decoration: none
|
|
||||||
+text-overflow-ellipsis
|
|
||||||
|
|
||||||
span
|
.search-pagination
|
||||||
&.project
|
text-align: center
|
||||||
color: $color-text-dark-secondary
|
list-style-type: none
|
||||||
margin-right: 3px
|
margin: 0
|
||||||
&.updated
|
padding: 0
|
||||||
color: $color-text-dark-hint
|
width: 100%
|
||||||
&.status
|
display: flex
|
||||||
font-size: .8em
|
+clearfix
|
||||||
color: $color-text-dark-secondary
|
|
||||||
border: thin solid $color-text-dark-hint
|
|
||||||
padding: 3px 8px
|
|
||||||
text-transform: uppercase
|
|
||||||
border-radius: 3px
|
|
||||||
margin-right: 5px
|
|
||||||
&.media, &.node_type
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
text-transform: capitalize
|
|
||||||
margin: 0 3px
|
|
||||||
|
|
||||||
&.when
|
li
|
||||||
margin: 0 3px
|
display: inline-block
|
||||||
float: right
|
margin: 5px auto
|
||||||
display: block
|
|
||||||
+media-lg
|
|
||||||
display: block
|
|
||||||
+media-md
|
|
||||||
display: block
|
|
||||||
+media-sm
|
|
||||||
display: none
|
|
||||||
+media-xs
|
|
||||||
display: none
|
|
||||||
|
|
||||||
&.context
|
&:last-child
|
||||||
margin: 0
|
border-color: transparent
|
||||||
float: right
|
|
||||||
display: none
|
|
||||||
|
|
||||||
&:hover
|
a
|
||||||
cursor: pointer
|
font-weight: 500
|
||||||
.search-hit-name-user
|
padding: 5px 4px
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
&.users
|
|
||||||
em
|
|
||||||
font-style: normal
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
.search-hit-name
|
|
||||||
font-size: 1.2em
|
|
||||||
|
|
||||||
small
|
|
||||||
margin-left: 5px
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
|
|
||||||
.search-hit-roles
|
|
||||||
font-size: .9em
|
|
||||||
color: $color-text-dark-secondary
|
color: $color-text-dark-secondary
|
||||||
margin-left: 15px
|
|
||||||
|
&:hover
|
||||||
|
color: $color-text-dark-primary
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: .6
|
||||||
|
|
||||||
|
&.active a
|
||||||
|
color: $color-text-dark-primary
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
|
||||||
.view-grid
|
.view-grid
|
||||||
display: flex
|
display: flex
|
||||||
@@ -706,13 +409,13 @@ $search-hit-width_grid: 100px
|
|||||||
transition: border-color 150ms ease-in-out
|
transition: border-color 150ms ease-in-out
|
||||||
|
|
||||||
&.active
|
&.active
|
||||||
background-color: $color-primary
|
background-color: $primary
|
||||||
border-color: $color-primary
|
border-color: $primary
|
||||||
|
|
||||||
.search-hit-name
|
.search-hit-name
|
||||||
font-weight: 500
|
font-weight: 500
|
||||||
color: white
|
color: white
|
||||||
background-color: $color-primary
|
background-color: $primary
|
||||||
|
|
||||||
.search-hit-name
|
.search-hit-name
|
||||||
font-size: .9em
|
font-size: .9em
|
||||||
@@ -776,5 +479,5 @@ $search-hit-width_grid: 100px
|
|||||||
|
|
||||||
&.active
|
&.active
|
||||||
color: white
|
color: white
|
||||||
background-color: $color-primary
|
background-color: $primary
|
||||||
border-color: transparent
|
border-color: transparent
|
||||||
|
@@ -67,138 +67,6 @@
|
|||||||
&:hover
|
&:hover
|
||||||
background-color: lighten($provider-color-google, 7%)
|
background-color: lighten($provider-color-google, 7%)
|
||||||
|
|
||||||
#settings
|
|
||||||
+media-xs
|
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
align-items: stretch
|
|
||||||
display: flex
|
|
||||||
margin: 25px auto
|
|
||||||
|
|
||||||
#settings-sidebar
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
+container-box
|
|
||||||
background-color: $color-background-light
|
|
||||||
color: $color-text
|
|
||||||
margin-right: 15px
|
|
||||||
width: 30%
|
|
||||||
|
|
||||||
.settings-content
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
ul
|
|
||||||
list-style: none
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
a
|
|
||||||
&:hover
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
li
|
|
||||||
background-color: lighten($color-background, 5%)
|
|
||||||
|
|
||||||
li
|
|
||||||
border-bottom: thin solid $color-background
|
|
||||||
border-left: thick solid transparent
|
|
||||||
margin: 0
|
|
||||||
padding: 25px
|
|
||||||
transition: all 100ms ease-in-out
|
|
||||||
|
|
||||||
i
|
|
||||||
font-size: 1.1em
|
|
||||||
padding-right: 15px
|
|
||||||
|
|
||||||
.active
|
|
||||||
li
|
|
||||||
background-color: lighten($color-background, 5%)
|
|
||||||
border-left: thick solid $color-info
|
|
||||||
|
|
||||||
|
|
||||||
#settings-container
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
+container-box
|
|
||||||
background-color: $color-background-light
|
|
||||||
width: 70%
|
|
||||||
|
|
||||||
.settings-header
|
|
||||||
background-color: $color-background
|
|
||||||
border-top-left-radius: 3px
|
|
||||||
border-top-right-radius: 3px
|
|
||||||
|
|
||||||
.settings-title
|
|
||||||
font:
|
|
||||||
size: 1.5em
|
|
||||||
weight: 300
|
|
||||||
padding: 10px 15px 10px 25px
|
|
||||||
|
|
||||||
.settings-content
|
|
||||||
padding: 25px
|
|
||||||
|
|
||||||
.settings-billing-info
|
|
||||||
font-size: 1.2em
|
|
||||||
|
|
||||||
.subscription-active
|
|
||||||
color: $color-success
|
|
||||||
padding-bottom: 20px
|
|
||||||
.subscription-demo
|
|
||||||
color: $color-info
|
|
||||||
margin-top: 0
|
|
||||||
.subscription-missing
|
|
||||||
color: $color-danger
|
|
||||||
margin-top: 0
|
|
||||||
|
|
||||||
.button-submit
|
|
||||||
clear: both
|
|
||||||
display: block
|
|
||||||
min-width: 200px
|
|
||||||
margin: 0 auto
|
|
||||||
+button($color-primary, 3px, true)
|
|
||||||
|
|
||||||
|
|
||||||
#settings-container
|
|
||||||
#settings-form
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
|
|
||||||
.settings-form
|
|
||||||
align-items: center
|
|
||||||
display: flex
|
|
||||||
justify-content: center
|
|
||||||
|
|
||||||
.left, .right
|
|
||||||
padding: 25px 0
|
|
||||||
|
|
||||||
.left
|
|
||||||
width: 60%
|
|
||||||
float: left
|
|
||||||
|
|
||||||
.right
|
|
||||||
width: 40%
|
|
||||||
float: right
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
label
|
|
||||||
color: $color-text
|
|
||||||
display: block
|
|
||||||
|
|
||||||
.settings-avatar
|
|
||||||
img
|
|
||||||
border-radius: 3px
|
|
||||||
|
|
||||||
span
|
|
||||||
display: block
|
|
||||||
padding: 15px 0
|
|
||||||
font:
|
|
||||||
size: .9em
|
|
||||||
|
|
||||||
.settings-password
|
|
||||||
color: $color-text-dark-primary
|
|
||||||
|
|
||||||
|
|
||||||
#user-edit-container
|
#user-edit-container
|
||||||
padding: 15px
|
padding: 15px
|
||||||
|
@@ -26,7 +26,6 @@
|
|||||||
display: inline-flex
|
display: inline-flex
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
font-family: $font-body
|
|
||||||
padding: 5px 12px
|
padding: 5px 12px
|
||||||
border-radius: $roundness
|
border-radius: $roundness
|
||||||
|
|
||||||
@@ -83,6 +82,15 @@
|
|||||||
text-shadow: none
|
text-shadow: none
|
||||||
|
|
||||||
|
|
||||||
|
=disabled-stripes
|
||||||
|
color: $color-text-dark
|
||||||
|
cursor: not-allowed
|
||||||
|
background: repeating-linear-gradient(-45deg, lighten($color-text-dark-hint, 15%), lighten($color-text-dark-hint, 15%) 10px, lighten($color-text-dark-hint, 5%) 10px, lighten($color-text-dark-hint, 5%) 20px)
|
||||||
|
border-color: darken($color-text-dark-hint, 5%)
|
||||||
|
pointer-events: none
|
||||||
|
opacity: .6
|
||||||
|
|
||||||
|
|
||||||
@mixin overlay($from-color, $from-percentage, $to-color, $to-percentage)
|
@mixin overlay($from-color, $from-percentage, $to-color, $to-percentage)
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0
|
top: 0
|
||||||
@@ -122,24 +130,16 @@
|
|||||||
transform: translate(-50%, -50%)
|
transform: translate(-50%, -50%)
|
||||||
|
|
||||||
=input-generic
|
=input-generic
|
||||||
padding: 5px 5px 5px 0
|
|
||||||
color: $color-text-dark
|
color: $color-text-dark
|
||||||
box-shadow: none
|
|
||||||
font-family: $font-body
|
|
||||||
border: thin solid transparent
|
|
||||||
border-radius: 0
|
|
||||||
border-bottom-color: $color-background-dark
|
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
transition: border-color 150ms ease-in-out, box-shadow 150ms ease-in-out
|
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
border-bottom-color: $color-background
|
border-bottom-color: $color-background
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
outline: 0
|
outline: 0
|
||||||
border: thin solid transparent
|
border-color: $primary
|
||||||
border-bottom-color: $color-primary
|
box-shadow: none
|
||||||
box-shadow: 0 1px 0 0 $color-primary
|
|
||||||
|
|
||||||
=label-generic
|
=label-generic
|
||||||
color: $color-text-dark-primary
|
color: $color-text-dark-primary
|
||||||
@@ -170,17 +170,25 @@
|
|||||||
/* Small but wide: phablets, iPads
|
/* Small but wide: phablets, iPads
|
||||||
** Menu is collapsed, columns stack, no brand */
|
** Menu is collapsed, columns stack, no brand */
|
||||||
=media-sm
|
=media-sm
|
||||||
@media (min-width: #{$screen-tablet}) and (max-width: #{$screen-desktop - 1px})
|
@include media-breakpoint-up(sm)
|
||||||
@content
|
@content
|
||||||
|
|
||||||
/* Tablets portrait.
|
/* Tablets portrait.
|
||||||
** Menu is expanded, but columns stack, brand is shown */
|
** Menu is expanded, but columns stack, brand is shown */
|
||||||
=media-md
|
=media-md
|
||||||
@media (min-width: #{$screen-desktop})
|
@include media-breakpoint-up(md)
|
||||||
@content
|
@content
|
||||||
|
|
||||||
=media-lg
|
=media-lg
|
||||||
@media (min-width: #{$screen-lg-desktop})
|
@include media-breakpoint-up(lg)
|
||||||
|
@content
|
||||||
|
|
||||||
|
=media-xl
|
||||||
|
@include media-breakpoint-up(xl)
|
||||||
|
@content
|
||||||
|
|
||||||
|
=media-xxl
|
||||||
|
@include media-breakpoint-up(xxl)
|
||||||
@content
|
@content
|
||||||
|
|
||||||
=media-print
|
=media-print
|
||||||
@@ -352,17 +360,15 @@
|
|||||||
|
|
||||||
=node-details-description
|
=node-details-description
|
||||||
+clearfix
|
+clearfix
|
||||||
color: darken($color-text-dark, 5%)
|
color: $color-text
|
||||||
font:
|
font-size: 1.25em
|
||||||
family: $font-body
|
|
||||||
weight: 300
|
|
||||||
size: 1.2em
|
|
||||||
|
|
||||||
word-break: break-word
|
word-break: break-word
|
||||||
|
|
||||||
+media-xs
|
+media-xs
|
||||||
font-size: 1.1em
|
font-size: 1.1em
|
||||||
|
|
||||||
|
/* Style links without a class. Usually regular
|
||||||
|
* links in a comment or node description. */
|
||||||
a:not([class])
|
a:not([class])
|
||||||
color: $color-text-dark-primary
|
color: $color-text-dark-primary
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
@@ -375,11 +381,6 @@
|
|||||||
line-height: 1.5em
|
line-height: 1.5em
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
padding:
|
|
||||||
top: 20px
|
|
||||||
right: 20px
|
|
||||||
|
|
||||||
blockquote
|
blockquote
|
||||||
background-color: lighten($color-background-light, 5%)
|
background-color: lighten($color-background-light, 5%)
|
||||||
box-shadow: inset 5px 0 0 $color-background
|
box-shadow: inset 5px 0 0 $color-background
|
||||||
@@ -400,10 +401,10 @@
|
|||||||
img,
|
img,
|
||||||
p img,
|
p img,
|
||||||
ul li img
|
ul li img
|
||||||
|
@extend .d-block
|
||||||
|
@extend .mx-auto
|
||||||
|
@extend .my-3
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
padding:
|
|
||||||
bottom: 25px
|
|
||||||
top: 25px
|
|
||||||
|
|
||||||
&.emoji
|
&.emoji
|
||||||
display: inline-block
|
display: inline-block
|
||||||
@@ -416,25 +417,13 @@
|
|||||||
font-size: 1.5em
|
font-size: 1.5em
|
||||||
|
|
||||||
/* e.g. YouTube embed */
|
/* e.g. YouTube embed */
|
||||||
iframe
|
iframe, video
|
||||||
height: auto
|
|
||||||
margin: 15px auto
|
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
min-height: 500px
|
@extend .mx-auto
|
||||||
width: 100%
|
|
||||||
|
|
||||||
+media-sm
|
.embed-responsive,
|
||||||
iframe
|
video
|
||||||
min-height: 314px
|
@extend .my-3
|
||||||
+media-xs
|
|
||||||
iframe
|
|
||||||
min-height: 314px
|
|
||||||
|
|
||||||
iframe[src^="https://www.youtube"]
|
|
||||||
+media-xs
|
|
||||||
iframe
|
|
||||||
min-height: 420px
|
|
||||||
min-height: 500px
|
|
||||||
|
|
||||||
iframe[src^="https://w.soundcloud"]
|
iframe[src^="https://w.soundcloud"]
|
||||||
min-height: auto
|
min-height: auto
|
||||||
@@ -507,28 +496,24 @@
|
|||||||
|
|
||||||
=ribbon
|
=ribbon
|
||||||
background-color: $color-success
|
background-color: $color-success
|
||||||
cursor: default
|
border: thin dashed rgba(white, .5)
|
||||||
|
color: white
|
||||||
|
pointer-events: none
|
||||||
|
font-size: 70%
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
white-space: nowrap
|
|
||||||
position: absolute
|
position: absolute
|
||||||
right: -40px
|
right: -40px
|
||||||
top: 10px
|
top: 10px
|
||||||
-webkit-transform: rotate(45deg)
|
|
||||||
-moz-transform: rotate(45deg)
|
|
||||||
-ms-transform: rotate(45deg)
|
|
||||||
-o-transform: rotate(45deg)
|
|
||||||
transform: rotate(45deg)
|
transform: rotate(45deg)
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
span
|
span
|
||||||
border: thin dashed rgba(white, .5)
|
|
||||||
color: white
|
|
||||||
display: block
|
display: block
|
||||||
font-size: 70%
|
|
||||||
margin: 1px 0
|
margin: 1px 0
|
||||||
padding: 3px 50px
|
padding: 3px 50px
|
||||||
text:
|
|
||||||
align: center
|
.ribbon
|
||||||
transform: uppercase
|
+ribbon
|
||||||
|
|
||||||
@mixin text-background($text-color, $background-color, $roundness, $padding)
|
@mixin text-background($text-color, $background-color, $roundness, $padding)
|
||||||
border-radius: $roundness
|
border-radius: $roundness
|
||||||
@@ -568,9 +553,7 @@
|
|||||||
|
|
||||||
/* Bootstrap's img-responsive class */
|
/* Bootstrap's img-responsive class */
|
||||||
=img-responsive
|
=img-responsive
|
||||||
display: block
|
@extend .img-fluid
|
||||||
max-width: 100%
|
|
||||||
height: auto
|
|
||||||
|
|
||||||
/* Set the color for a specified property
|
/* Set the color for a specified property
|
||||||
* 1: $property: e.g. background-color
|
* 1: $property: e.g. background-color
|
||||||
@@ -642,9 +625,7 @@
|
|||||||
#{$property}: $color-status-review
|
#{$property}: $color-status-review
|
||||||
|
|
||||||
=sidebar-button-active
|
=sidebar-button-active
|
||||||
background-color: $color-background-nav
|
color: $primary
|
||||||
box-shadow: inset 2px 0 0 $color-primary
|
|
||||||
color: white
|
|
||||||
|
|
||||||
.flash-on
|
.flash-on
|
||||||
background-color: lighten($color-success, 50%) !important
|
background-color: lighten($color-success, 50%) !important
|
||||||
@@ -660,3 +641,52 @@
|
|||||||
transition: all 1s ease-out
|
transition: all 1s ease-out
|
||||||
img
|
img
|
||||||
transition: all 1s ease-out
|
transition: all 1s ease-out
|
||||||
|
|
||||||
|
.cursor-pointer
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
.cursor-zoom-in
|
||||||
|
cursor: zoom-in
|
||||||
|
|
||||||
|
.user-select-none
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
.pointer-events-none
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
// Bootstrap has .img-fluid, a class to limit the width of an image to 100%.
|
||||||
|
// .imgs-fluid below is to be applied on a parent container when we can't add
|
||||||
|
// classes to the images themselves. e.g. the blog.
|
||||||
|
.imgs-fluid
|
||||||
|
img
|
||||||
|
// Just re-use Bootstrap's mixin here.
|
||||||
|
+img-fluid
|
||||||
|
|
||||||
|
.overflow-hidden
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
=text-gradient($color_from, $color_to)
|
||||||
|
background: linear-gradient(to right, $color_from, $color_to)
|
||||||
|
background-clip: text
|
||||||
|
-webkit-background-clip: text
|
||||||
|
-webkit-text-fill-color: transparent
|
||||||
|
|
||||||
|
=active-gradient
|
||||||
|
+text-gradient($primary-accent, $primary)
|
||||||
|
|
||||||
|
&:before
|
||||||
|
+text-gradient($primary-accent, $primary)
|
||||||
|
|
||||||
|
.title-underline
|
||||||
|
padding-bottom: 5px
|
||||||
|
position: relative
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
|
&:before
|
||||||
|
background-color: $primary
|
||||||
|
content: ' '
|
||||||
|
display: block
|
||||||
|
height: 2px
|
||||||
|
top: 125%
|
||||||
|
position: absolute
|
||||||
|
width: 50px
|
||||||
|
1290
src/styles/base.sass
1290
src/styles/base.sass
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,58 @@
|
|||||||
@import _normalize
|
// Bootstrap variables and utilities.
|
||||||
|
@import "../../node_modules/bootstrap/scss/functions"
|
||||||
|
@import "../../node_modules/bootstrap/scss/variables"
|
||||||
|
@import "../../node_modules/bootstrap/scss/mixins"
|
||||||
|
|
||||||
@import _config
|
@import _config
|
||||||
@import _utils
|
@import _utils
|
||||||
|
|
||||||
|
// Bootstrap components.
|
||||||
|
@import "../../node_modules/bootstrap/scss/root"
|
||||||
|
@import "../../node_modules/bootstrap/scss/reboot"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/type"
|
||||||
|
@import "../../node_modules/bootstrap/scss/images"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/code"
|
||||||
|
@import "../../node_modules/bootstrap/scss/grid"
|
||||||
|
@import "../../node_modules/bootstrap/scss/buttons"
|
||||||
|
@import "../../node_modules/bootstrap/scss/dropdown"
|
||||||
|
@import "../../node_modules/bootstrap/scss/custom-forms"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/nav"
|
||||||
|
@import "../../node_modules/bootstrap/scss/navbar"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/card"
|
||||||
|
@import "../../node_modules/bootstrap/scss/jumbotron"
|
||||||
|
@import "../../node_modules/bootstrap/scss/media"
|
||||||
|
@import "../../node_modules/bootstrap/scss/close"
|
||||||
|
@import "../../node_modules/bootstrap/scss/modal"
|
||||||
|
@import "../../node_modules/bootstrap/scss/tooltip"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/utilities"
|
||||||
|
|
||||||
|
// Pillar components.
|
||||||
|
@import "apps_base"
|
||||||
|
@import "components/base"
|
||||||
|
|
||||||
|
@import "components/card"
|
||||||
|
@import "components/jumbotron"
|
||||||
|
@import "components/navbar"
|
||||||
|
@import "components/dropdown"
|
||||||
|
@import "components/footer"
|
||||||
|
@import "components/shortcode"
|
||||||
|
@import "components/flyout"
|
||||||
|
@import "components/buttons"
|
||||||
|
@import "components/tooltip"
|
||||||
|
@import "components/overlay"
|
||||||
|
|
||||||
@import _comments
|
@import _comments
|
||||||
@import _error
|
@import _notifications
|
||||||
@import _search
|
|
||||||
|
|
||||||
.container-fluid.blog
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
#blog_container
|
|
||||||
+media-xs
|
|
||||||
flex-direction: column
|
|
||||||
padding-top: 0
|
|
||||||
display: flex
|
|
||||||
padding:
|
|
||||||
bottom: 15px
|
|
||||||
|
|
||||||
video
|
|
||||||
max-width: 100%
|
|
||||||
|
|
||||||
#blog_post-edit-form
|
#blog_post-edit-form
|
||||||
padding: 20px
|
padding: 20px
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
position: relative
|
|
||||||
margin: 0 auto 30px auto
|
|
||||||
font-family: $font-body
|
|
||||||
|
|
||||||
input, textarea, select
|
input, textarea, select
|
||||||
+input-generic
|
+input-generic
|
||||||
|
|
||||||
@@ -95,7 +120,6 @@
|
|||||||
margin-bottom: 15px
|
margin-bottom: 15px
|
||||||
border-top: thin solid $color-text-dark-hint
|
border-top: thin solid $color-text-dark-hint
|
||||||
|
|
||||||
|
|
||||||
.form-group.description,
|
.form-group.description,
|
||||||
.form-group.summary,
|
.form-group.summary,
|
||||||
.form-group.content
|
.form-group.content
|
||||||
@@ -163,14 +187,10 @@
|
|||||||
color: transparent
|
color: transparent
|
||||||
|
|
||||||
|
|
||||||
#blog_post-create-container,
|
|
||||||
#blog_post-edit-container
|
|
||||||
padding: 25px
|
|
||||||
|
|
||||||
#blog_index-container,
|
|
||||||
#blog_post-create-container,
|
#blog_post-create-container,
|
||||||
#blog_post-edit-container
|
#blog_post-edit-container
|
||||||
+container-box
|
+container-box
|
||||||
|
padding: 25px
|
||||||
width: 75%
|
width: 75%
|
||||||
|
|
||||||
+media-xs
|
+media-xs
|
||||||
@@ -185,133 +205,6 @@
|
|||||||
+media-lg
|
+media-lg
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.blog_index-header
|
|
||||||
border-top-left-radius: 3px
|
|
||||||
border-top-right-radius: 3px
|
|
||||||
display: block
|
|
||||||
overflow: hidden
|
|
||||||
position: relative
|
|
||||||
text-align: center
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
img
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
.blog_index-item
|
|
||||||
+media-lg
|
|
||||||
max-width: 780px
|
|
||||||
+media-md
|
|
||||||
max-width: 780px
|
|
||||||
+media-sm
|
|
||||||
max-width: 780px
|
|
||||||
|
|
||||||
margin: 15px auto
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
.item-info a
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
.item-picture
|
|
||||||
position: relative
|
|
||||||
width: 100%
|
|
||||||
max-height: 350px
|
|
||||||
min-height: 200px
|
|
||||||
height: auto
|
|
||||||
overflow: hidden
|
|
||||||
border-top-left-radius: 3px
|
|
||||||
border-top-right-radius: 3px
|
|
||||||
+clearfix
|
|
||||||
|
|
||||||
img
|
|
||||||
+position-center-translate
|
|
||||||
width: 100%
|
|
||||||
border-top-left-radius: 3px
|
|
||||||
border-top-right-radius: 3px
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
min-height: 150px
|
|
||||||
+media-sm
|
|
||||||
min-height: 150px
|
|
||||||
+media-md
|
|
||||||
min-height: 250px
|
|
||||||
+media-lg
|
|
||||||
min-height: 250px
|
|
||||||
|
|
||||||
.item-title
|
|
||||||
color: $color-text-dark
|
|
||||||
display: block
|
|
||||||
font:
|
|
||||||
family: $font-body
|
|
||||||
size: 1.8em
|
|
||||||
|
|
||||||
padding: 10px 25px 10px
|
|
||||||
|
|
||||||
ul.meta
|
|
||||||
+list-meta
|
|
||||||
font-size: .9em
|
|
||||||
padding: 0px 25px 5px
|
|
||||||
|
|
||||||
.item-content
|
|
||||||
+node-details-description
|
|
||||||
font-size: 1.3em
|
|
||||||
padding: 15px 25px 25px
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
padding:
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
|
|
||||||
img
|
|
||||||
display: block
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
.item-meta
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
padding:
|
|
||||||
left: 25px
|
|
||||||
right: 25px
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
padding:
|
|
||||||
left: 10px
|
|
||||||
right: 10px
|
|
||||||
|
|
||||||
.button-create,
|
|
||||||
.button-edit
|
|
||||||
+button($color-success, 3px, true)
|
|
||||||
|
|
||||||
.item-picture+.button-back+.button-edit
|
|
||||||
right: 20px
|
|
||||||
top: 20px
|
|
||||||
|
|
||||||
.comments-container
|
|
||||||
padding:
|
|
||||||
left: 20px
|
|
||||||
right: 20px
|
|
||||||
max-width: 680px
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
+media-lg
|
|
||||||
padding:
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
|
|
||||||
.comment-reply-container
|
|
||||||
background-color: transparent
|
|
||||||
|
|
||||||
.comment-reply-field
|
|
||||||
textarea, .comment-reply-meta
|
|
||||||
background-color: $color-background-light
|
|
||||||
|
|
||||||
&.filled
|
|
||||||
.comment-reply-meta
|
|
||||||
background-color: $color-success
|
|
||||||
|
|
||||||
.comment-reply-form
|
|
||||||
+media-xs
|
|
||||||
padding:
|
|
||||||
left: 0
|
|
||||||
|
|
||||||
#blog_post-edit-form
|
#blog_post-edit-form
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
@@ -346,294 +239,3 @@
|
|||||||
|
|
||||||
.form-upload-file-meta
|
.form-upload-file-meta
|
||||||
width: initial
|
width: initial
|
||||||
|
|
||||||
#blog_post-edit-title
|
|
||||||
padding: 0
|
|
||||||
color: $color-text
|
|
||||||
font:
|
|
||||||
size: 1.8em
|
|
||||||
weight: 300
|
|
||||||
margin: 0 20px 15px 0
|
|
||||||
|
|
||||||
#blog_index-sidebar
|
|
||||||
width: 25%
|
|
||||||
padding: 0 15px
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
clear: both
|
|
||||||
display: block
|
|
||||||
margin-top: 25px
|
|
||||||
+media-sm
|
|
||||||
width: 40%
|
|
||||||
+media-md
|
|
||||||
width: 30%
|
|
||||||
+media-lg
|
|
||||||
width: 25%
|
|
||||||
|
|
||||||
.button-create
|
|
||||||
display: block
|
|
||||||
width: 100%
|
|
||||||
+button($color-success, 6px)
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
.button-back
|
|
||||||
+button($color-info, 6px, true)
|
|
||||||
display: block
|
|
||||||
width: 100%
|
|
||||||
margin: 15px 0 0 0
|
|
||||||
|
|
||||||
#blog_post-edit-form
|
|
||||||
.form-group
|
|
||||||
.form-control
|
|
||||||
background-color: white
|
|
||||||
|
|
||||||
.blog_index-sidebar,
|
|
||||||
.blog_project-sidebar
|
|
||||||
+container-box
|
|
||||||
background-color: lighten($color-background, 5%)
|
|
||||||
padding: 20px
|
|
||||||
|
|
||||||
.blog_project-card
|
|
||||||
position: relative
|
|
||||||
width: 100%
|
|
||||||
border-radius: 3px
|
|
||||||
overflow: hidden
|
|
||||||
background-color: white
|
|
||||||
color: lighten($color-text, 10%)
|
|
||||||
box-shadow: 0 0 30px rgba(black, .2)
|
|
||||||
|
|
||||||
margin:
|
|
||||||
top: 0
|
|
||||||
bottom: 15px
|
|
||||||
left: auto
|
|
||||||
right: auto
|
|
||||||
|
|
||||||
|
|
||||||
a.item-header
|
|
||||||
position: relative
|
|
||||||
width: 100%
|
|
||||||
height: 100px
|
|
||||||
display: block
|
|
||||||
background-size: 100% 100%
|
|
||||||
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.overlay
|
|
||||||
z-index: 1
|
|
||||||
width: 100%
|
|
||||||
height: 100px
|
|
||||||
@include overlay(transparent, 0%, white, 100%)
|
|
||||||
|
|
||||||
|
|
||||||
img.background
|
|
||||||
width: 100%
|
|
||||||
transform: scale(1.4)
|
|
||||||
|
|
||||||
.card-thumbnail
|
|
||||||
position: absolute
|
|
||||||
z-index: 2
|
|
||||||
height: 90px
|
|
||||||
width: 90px
|
|
||||||
display: block
|
|
||||||
top: 35px
|
|
||||||
left: 50%
|
|
||||||
transform: translateX(-50%)
|
|
||||||
background-color: white
|
|
||||||
border-radius: 3px
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
img.thumb
|
|
||||||
opacity: .9
|
|
||||||
|
|
||||||
img.thumb
|
|
||||||
width: 100%
|
|
||||||
border-radius: 3px
|
|
||||||
transition: opacity 150ms ease-in-out
|
|
||||||
+position-center-translate
|
|
||||||
|
|
||||||
.item-info
|
|
||||||
padding: 10px 20px
|
|
||||||
background-color: white
|
|
||||||
border-bottom-left-radius: 3px
|
|
||||||
border-bottom-right-radius: 3px
|
|
||||||
|
|
||||||
a.item-title
|
|
||||||
display: inline-block
|
|
||||||
width: 100%
|
|
||||||
padding: 30px 0 15px 0
|
|
||||||
color: $color-text-dark
|
|
||||||
text-align: center
|
|
||||||
font:
|
|
||||||
size: 1.6em
|
|
||||||
weight: 300
|
|
||||||
|
|
||||||
transition: color 150ms ease-in-out
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration: none
|
|
||||||
color: $color-primary
|
|
||||||
|
|
||||||
|
|
||||||
#blog_container
|
|
||||||
&.cloud-blog
|
|
||||||
#blog_index-container,
|
|
||||||
#blog_post-create-container,
|
|
||||||
#blog_post-edit-container
|
|
||||||
width: 100%
|
|
||||||
padding: 25px 30px 20px 30px
|
|
||||||
|
|
||||||
#blog_index-container+#blog_index-sidebar
|
|
||||||
display: none
|
|
||||||
|
|
||||||
#blog_index-container,
|
|
||||||
&.cloud-blog #blog_index-container
|
|
||||||
+media-sm
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
padding: 0 0 50px 0
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
.blog_index-item
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
padding: 10px 25px
|
|
||||||
|
|
||||||
&.list
|
|
||||||
margin: 0 auto
|
|
||||||
padding: 15px 0
|
|
||||||
margin: 0 auto
|
|
||||||
border-bottom: thin solid $color-background
|
|
||||||
|
|
||||||
&:last-child
|
|
||||||
border-bottom: none
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
padding: 15px 10px
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
a.item-title
|
|
||||||
padding:
|
|
||||||
top: 0
|
|
||||||
bottom: 5px
|
|
||||||
font:
|
|
||||||
size: 1.6em
|
|
||||||
weight: 400
|
|
||||||
family: $font-body
|
|
||||||
|
|
||||||
.item-info
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
font-size: .9em
|
|
||||||
padding:
|
|
||||||
left: 25px
|
|
||||||
right: 25px
|
|
||||||
|
|
||||||
.item-header
|
|
||||||
width: 50px
|
|
||||||
height: 50px
|
|
||||||
position: absolute
|
|
||||||
top: 20px
|
|
||||||
border-radius: 3px
|
|
||||||
background-color: $color-background
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
img
|
|
||||||
+position-center-translate
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
i
|
|
||||||
+position-center-translate
|
|
||||||
font-size: 1.2em
|
|
||||||
color: $color-text-dark-hint
|
|
||||||
|
|
||||||
&.nothumb
|
|
||||||
border-radius: 50%
|
|
||||||
|
|
||||||
a.item-title, .item-info
|
|
||||||
padding-left: 70px
|
|
||||||
|
|
||||||
#blog_index-container
|
|
||||||
.blog_index-item
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
padding: 25px 0 20px 0
|
|
||||||
|
|
||||||
&.list
|
|
||||||
padding: 15px 10px
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
+media-xs
|
|
||||||
width: 100%
|
|
||||||
padding: 15px 10px
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
|
|
||||||
.blog-archive-navigation
|
|
||||||
+media-xs
|
|
||||||
font-size: 1em
|
|
||||||
max-width: initial
|
|
||||||
|
|
||||||
border-bottom: thin solid $color-background-dark
|
|
||||||
display: flex
|
|
||||||
font:
|
|
||||||
size: 1.2em
|
|
||||||
weight: 300
|
|
||||||
margin: 0 auto
|
|
||||||
max-width: 780px
|
|
||||||
text-align: center
|
|
||||||
+text-overflow-ellipsis
|
|
||||||
|
|
||||||
&:last-child
|
|
||||||
border: none
|
|
||||||
|
|
||||||
a, span
|
|
||||||
+media-xs
|
|
||||||
padding: 10px
|
|
||||||
|
|
||||||
flex: 1
|
|
||||||
padding: 25px 15px
|
|
||||||
|
|
||||||
span
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
.blog-action
|
|
||||||
display: flex
|
|
||||||
padding: 10px
|
|
||||||
position: absolute
|
|
||||||
right: 0
|
|
||||||
top: 0
|
|
||||||
z-index: 1
|
|
||||||
|
|
||||||
|
|
||||||
// Specific tweaks for blogs in the context of a project
|
|
||||||
#project_context
|
|
||||||
.blog_index-item
|
|
||||||
+media-xs
|
|
||||||
margin-left: 0
|
|
||||||
padding: 0
|
|
||||||
margin-left: 10px
|
|
||||||
|
|
||||||
&.list
|
|
||||||
margin-left: 35px !important
|
|
||||||
|
|
||||||
.item-title,
|
|
||||||
.item-info
|
|
||||||
+media-xs
|
|
||||||
padding-left: 0
|
|
||||||
padding-left: 25px
|
|
||||||
|
|
||||||
#blog_container
|
|
||||||
.comments-container
|
|
||||||
+media-sm
|
|
||||||
margin-left: 10px
|
|
||||||
margin-left: 30px
|
|
||||||
|
|
||||||
.blog-archive-navigation
|
|
||||||
margin-left: 35px
|
|
||||||
|
72
src/styles/components/_alerts.sass
Normal file
72
src/styles/components/_alerts.sass
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
.alert
|
||||||
|
margin-bottom: 0
|
||||||
|
text-align: center
|
||||||
|
padding: 10px 20px
|
||||||
|
z-index: 16
|
||||||
|
|
||||||
|
// overriden by alert types
|
||||||
|
color: $color-text-dark
|
||||||
|
background-color: $color-background
|
||||||
|
|
||||||
|
&.alert-danger,
|
||||||
|
&.alert-error
|
||||||
|
background-color: lighten($color-danger, 35%)
|
||||||
|
color: $color-danger
|
||||||
|
.alert-icon, .close
|
||||||
|
color: $color-danger
|
||||||
|
|
||||||
|
&.alert-warning
|
||||||
|
background-color: lighten($color-warning, 20%)
|
||||||
|
color: darken($color-warning, 20%)
|
||||||
|
.alert-icon, .close
|
||||||
|
color: darken($color-warning, 20%)
|
||||||
|
|
||||||
|
&.alert-success
|
||||||
|
background-color: lighten($color-success, 45%)
|
||||||
|
color: $color-success
|
||||||
|
|
||||||
|
.alert-icon, .close
|
||||||
|
color: $color-success
|
||||||
|
|
||||||
|
&.alert-info
|
||||||
|
background-color: lighten($color-info, 30%)
|
||||||
|
color: darken($color-info, 10%)
|
||||||
|
.alert-icon, .close
|
||||||
|
color: darken($color-info, 10%)
|
||||||
|
|
||||||
|
button.close
|
||||||
|
position: absolute
|
||||||
|
right: 10px
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: .8em
|
||||||
|
|
||||||
|
i.alert-icon
|
||||||
|
&:before
|
||||||
|
font-family: "pillar-font"
|
||||||
|
padding-right: 10px
|
||||||
|
|
||||||
|
&.success:before
|
||||||
|
content: '\e801'
|
||||||
|
&.info:before
|
||||||
|
content: "\e80c"
|
||||||
|
&.warning:before
|
||||||
|
content: "\e80b"
|
||||||
|
&.danger:before,
|
||||||
|
&.error:before
|
||||||
|
content: "\e83d"
|
||||||
|
|
||||||
|
|
||||||
|
/* When there's an alert, disable the fixed top */
|
||||||
|
.alert+.navbar-fixed-top
|
||||||
|
position: relative
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
|
&+.container
|
||||||
|
padding-top: 0
|
||||||
|
|
||||||
|
.alert+.navbar
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.alert+.navbar+.page-content
|
||||||
|
padding-top: 0
|
29
src/styles/components/_base.sass
Normal file
29
src/styles/components/_base.sass
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
body
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
+media-sm
|
||||||
|
width: 100%
|
||||||
|
max-width: 100%
|
||||||
|
min-width: auto
|
||||||
|
|
||||||
|
+media-xs
|
||||||
|
width: 100%
|
||||||
|
max-width: 100%
|
||||||
|
min-width: auto
|
||||||
|
|
||||||
|
.container
|
||||||
|
+media-xs
|
||||||
|
max-width: 100%
|
||||||
|
min-width: auto
|
||||||
|
padding:
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
|
||||||
|
&.box
|
||||||
|
+container-box
|
||||||
|
|
||||||
|
.page-content
|
||||||
|
background-color: $white
|
||||||
|
|
||||||
|
.container-box
|
||||||
|
+container-box
|
14
src/styles/components/_buttons.sass
Normal file
14
src/styles/components/_buttons.sass
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.btn-outline
|
||||||
|
background-color: transparent
|
||||||
|
border-width: 1px
|
||||||
|
transition: background-color .1s
|
||||||
|
|
||||||
|
&:focus, &:active
|
||||||
|
box-shadow: none
|
||||||
|
|
||||||
|
.btn-empty
|
||||||
|
background-color: transparent
|
||||||
|
border-color: transparent
|
||||||
|
|
||||||
|
&:focus, &:active
|
||||||
|
box-shadow: none
|
142
src/styles/components/_card.sass
Normal file
142
src/styles/components/_card.sass
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
.card-deck
|
||||||
|
// Custom, as of bootstrap 4.1.3 there is no way to do this.
|
||||||
|
&.card-deck-responsive
|
||||||
|
@extend .row
|
||||||
|
|
||||||
|
.card
|
||||||
|
@extend .col-md-4
|
||||||
|
|
||||||
|
+media-sm
|
||||||
|
flex: 1 0 50%
|
||||||
|
max-width: 50%
|
||||||
|
|
||||||
|
+media-md
|
||||||
|
flex: 1 0 33%
|
||||||
|
max-width: 33%
|
||||||
|
|
||||||
|
+media-lg
|
||||||
|
flex: 1 0 33%
|
||||||
|
max-width: 33%
|
||||||
|
|
||||||
|
+media-xl
|
||||||
|
flex: 1 0 25%
|
||||||
|
max-width: 25%
|
||||||
|
|
||||||
|
+media-xxl
|
||||||
|
flex: 1 0 20%
|
||||||
|
max-width: 20%
|
||||||
|
|
||||||
|
&.card-3-columns .card
|
||||||
|
+media-xxl
|
||||||
|
flex: 1 0 33%
|
||||||
|
max-width: 33%
|
||||||
|
|
||||||
|
&.card-deck-vertical
|
||||||
|
@extend .flex-column
|
||||||
|
flex-wrap: initial
|
||||||
|
|
||||||
|
.card
|
||||||
|
@extend .w-100
|
||||||
|
@extend .flex-row
|
||||||
|
@extend .p-0
|
||||||
|
|
||||||
|
flex: initial
|
||||||
|
flex-wrap: wrap
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
|
.card-img-top
|
||||||
|
@extend .rounded-0
|
||||||
|
|
||||||
|
.embed-responsive
|
||||||
|
@extend .mr-2
|
||||||
|
max-width: 120px
|
||||||
|
|
||||||
|
&.asset
|
||||||
|
&.free
|
||||||
|
&:after
|
||||||
|
+ribbon
|
||||||
|
content: 'FREE'
|
||||||
|
font-size: .6rem
|
||||||
|
left: -40px
|
||||||
|
padding: 1px 45px
|
||||||
|
right: initial
|
||||||
|
transform: rotate(-45deg)
|
||||||
|
|
||||||
|
.card-body
|
||||||
|
@extend .overflow-hidden
|
||||||
|
flex-basis: 0
|
||||||
|
|
||||||
|
.card-padless
|
||||||
|
.card
|
||||||
|
@extend .border-0
|
||||||
|
|
||||||
|
.card-body
|
||||||
|
@extend .px-0
|
||||||
|
|
||||||
|
|
||||||
|
.card-image-fade
|
||||||
|
&:hover
|
||||||
|
.card-img-top
|
||||||
|
opacity: .9
|
||||||
|
|
||||||
|
|
||||||
|
.card.asset
|
||||||
|
color: $color-text
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
&.free
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:after
|
||||||
|
+ribbon
|
||||||
|
content: 'FREE'
|
||||||
|
padding: 2px 50px
|
||||||
|
|
||||||
|
.card-body
|
||||||
|
position: relative // for placing the progress
|
||||||
|
|
||||||
|
.card-text
|
||||||
|
font-size: $font-size-xs
|
||||||
|
|
||||||
|
.card-img-top
|
||||||
|
background-color: $color-background
|
||||||
|
background-size: cover
|
||||||
|
background-position: center
|
||||||
|
|
||||||
|
|
||||||
|
$card-progress-height: 5px
|
||||||
|
.progress
|
||||||
|
height: $card-progress-height
|
||||||
|
position: absolute
|
||||||
|
top: -$card-progress-height
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.card-img-top
|
||||||
|
&.card-icon
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
font-size: 2em
|
||||||
|
|
||||||
|
i
|
||||||
|
opacity: .2
|
||||||
|
|
||||||
|
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
||||||
|
.card-label
|
||||||
|
background-color: rgba($black, .5)
|
||||||
|
border-radius: 3px
|
||||||
|
color: $white
|
||||||
|
display: block
|
||||||
|
font-size: $font-size-xxs
|
||||||
|
left: 5px
|
||||||
|
top: -27px // enough to be above the progress-bar
|
||||||
|
position: absolute
|
||||||
|
padding: 1px 5px
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
.card
|
||||||
|
&.active
|
||||||
|
.card-title
|
||||||
|
color: $primary
|
8
src/styles/components/_checkbox.sass
Normal file
8
src/styles/components/_checkbox.sass
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.checkbox label label
|
||||||
|
padding-left: 0
|
||||||
|
|
||||||
|
.checkbox label input[type=checkbox] + label
|
||||||
|
transition: color 100ms ease-in-out
|
||||||
|
|
||||||
|
.checkbox label input[type=checkbox]:checked + label
|
||||||
|
color: $color-success !important
|
44
src/styles/components/_dropdown.sass
Normal file
44
src/styles/components/_dropdown.sass
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Global, we want all menus to look like this.
|
||||||
|
ul.dropdown-menu
|
||||||
|
box-shadow: $dropdown-box-shadow
|
||||||
|
top: 95% // So there is less gap between the dropdown and the item.
|
||||||
|
|
||||||
|
> li
|
||||||
|
&:first-child > a
|
||||||
|
padding-top: ($dropdown-item-padding-y * 1.5)
|
||||||
|
&:last-child > a
|
||||||
|
padding-bottom: ($dropdown-item-padding-y * 1.5)
|
||||||
|
|
||||||
|
> a
|
||||||
|
padding-top: $dropdown-item-padding-y
|
||||||
|
padding-bottom: $dropdown-item-padding-y
|
||||||
|
|
||||||
|
.dropdown-divider
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.dropdown-item:last-child
|
||||||
|
border-bottom-left-radius: $border-radius
|
||||||
|
border-bottom-right-radius: $border-radius
|
||||||
|
|
||||||
|
// Open dropdown on mouse hover dropdowns in the navbar.
|
||||||
|
nav .dropdown:hover
|
||||||
|
ul.dropdown-menu
|
||||||
|
display: block
|
||||||
|
|
||||||
|
nav .dropdown.large:hover
|
||||||
|
.dropdown-menu
|
||||||
|
@extend .d-flex
|
||||||
|
|
||||||
|
.dropdown.large.show
|
||||||
|
@extend .d-flex
|
||||||
|
|
||||||
|
.dropdown-menu.show
|
||||||
|
@extend .d-flex
|
||||||
|
|
||||||
|
.dropdown-menu-tab
|
||||||
|
display: none
|
||||||
|
min-width: 100px
|
||||||
|
|
||||||
|
|
||||||
|
&.show // .dropdown-menu-tab.show
|
||||||
|
@extend .d-flex
|
25
src/styles/components/_flyout.sass
Normal file
25
src/styles/components/_flyout.sass
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/* Flyouts (only used on notifications for now) */
|
||||||
|
.flyout
|
||||||
|
background-color: $color-background
|
||||||
|
border-radius: 3px
|
||||||
|
border: thin solid darken($color-background, 3%)
|
||||||
|
box-shadow: 1px 2px 2px rgba(black, .2)
|
||||||
|
display: block
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
& .flyout-title
|
||||||
|
cursor: default
|
||||||
|
display: block
|
||||||
|
float: left
|
||||||
|
font-size: 1.1em
|
||||||
|
font-weight: 600
|
||||||
|
padding: 8px 10px 5px 10px
|
||||||
|
|
||||||
|
&.notifications
|
||||||
|
max-height: 1000%
|
||||||
|
overflow-x: hidden
|
||||||
|
position: absolute
|
||||||
|
right: 0
|
||||||
|
top: 40px
|
||||||
|
width: 420px
|
||||||
|
z-index: 9999
|
117
src/styles/components/_footer.sass
Normal file
117
src/styles/components/_footer.sass
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/* FOOTER */
|
||||||
|
.footer-wrapper
|
||||||
|
background-color: $color-background
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-color: $color-background
|
||||||
|
bottom: 0
|
||||||
|
content: ''
|
||||||
|
position: fixed
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
top: 0
|
||||||
|
pointer-events: none
|
||||||
|
z-index: -1
|
||||||
|
|
||||||
|
/* Footer Navigation */
|
||||||
|
footer
|
||||||
|
font-size: .75em
|
||||||
|
padding: 0 0 10px 0
|
||||||
|
|
||||||
|
a
|
||||||
|
color: $color-text-dark-primary
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-primary
|
||||||
|
|
||||||
|
ul.links
|
||||||
|
float: left
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
list-style-type: none
|
||||||
|
|
||||||
|
li
|
||||||
|
padding: 0 15px 0 0
|
||||||
|
margin: 0
|
||||||
|
float: left
|
||||||
|
|
||||||
|
#hop
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
visibility: hidden
|
||||||
|
position: fixed
|
||||||
|
right: 25px
|
||||||
|
bottom: 25px
|
||||||
|
z-index: 999
|
||||||
|
cursor: pointer
|
||||||
|
opacity: 0
|
||||||
|
background: $color-background-light
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
border-radius: 50%
|
||||||
|
color: $color-text-dark-secondary
|
||||||
|
font-size: 2em
|
||||||
|
box-shadow: 0 0 15px rgba(black, .2)
|
||||||
|
transform: scale(0.5)
|
||||||
|
transition: all 150ms ease-in-out
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: scale(1.2)
|
||||||
|
background-color: $color-background-nav
|
||||||
|
|
||||||
|
&.active
|
||||||
|
visibility: visible
|
||||||
|
opacity: 1
|
||||||
|
transform: scale(1)
|
||||||
|
|
||||||
|
|
||||||
|
.footer-navigation
|
||||||
|
font-size: .85em
|
||||||
|
margin-bottom: 5px
|
||||||
|
color: lighten($color-text, 30%)
|
||||||
|
border-top: thick solid lighten($color-text, 60%)
|
||||||
|
padding:
|
||||||
|
top: 15px
|
||||||
|
bottom: 15px
|
||||||
|
|
||||||
|
a
|
||||||
|
color: lighten($color-text, 35%)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-primary
|
||||||
|
|
||||||
|
.footer-links
|
||||||
|
i
|
||||||
|
font-size: 80%
|
||||||
|
position: absolute
|
||||||
|
left: -14px
|
||||||
|
top: 20%
|
||||||
|
|
||||||
|
.special
|
||||||
|
padding:
|
||||||
|
top: 10px
|
||||||
|
bottom: 15px
|
||||||
|
font-size: .9em
|
||||||
|
border-left: thin solid darken($color-background, 20%)
|
||||||
|
|
||||||
|
|
||||||
|
img
|
||||||
|
max-width: 100%
|
||||||
|
opacity: .6
|
||||||
|
|
||||||
|
ul.footer-social
|
||||||
|
width: 100%
|
||||||
|
text-align:center
|
||||||
|
margin: 0 auto
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: space-around
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline-block
|
||||||
|
padding: 30px 0
|
||||||
|
|
||||||
|
i
|
||||||
|
font-size: 3em
|
132
src/styles/components/_forms.sass
Normal file
132
src/styles/components/_forms.sass
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/* File Upload forms */
|
||||||
|
.fieldlist
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
margin: 10px 0 0 0
|
||||||
|
|
||||||
|
li.fieldlist-item
|
||||||
|
background-color: $color-background-light
|
||||||
|
border: thin solid $color-background
|
||||||
|
border-left: 3px solid $color-primary
|
||||||
|
border-top-right-radius: 3px
|
||||||
|
border-bottom-right-radius: 3px
|
||||||
|
|
||||||
|
margin-bottom: 10px
|
||||||
|
padding: 10px
|
||||||
|
+clearfix
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
margin-bottom: 0 !important // override bs
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
input.form-control
|
||||||
|
background-color: white !important
|
||||||
|
padding: 0 10px !important
|
||||||
|
border: thin solid $color-background-dark !important
|
||||||
|
|
||||||
|
div[class$="slug"]
|
||||||
|
width: 50%
|
||||||
|
float: left
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
label
|
||||||
|
margin-right: 10px
|
||||||
|
|
||||||
|
.fieldlist-action-button
|
||||||
|
+button($color-success, 3px)
|
||||||
|
margin: 0 0 0 10px
|
||||||
|
padding: 5px 10px
|
||||||
|
text-transform: initial
|
||||||
|
|
||||||
|
.form-upload-file
|
||||||
|
margin-bottom: 10px
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.form-upload-progress
|
||||||
|
margin-top: 10px
|
||||||
|
|
||||||
|
.form-upload-progress-bar
|
||||||
|
margin-top: 5px
|
||||||
|
background-color: $color-success
|
||||||
|
height: 5px
|
||||||
|
min-width: 0
|
||||||
|
border-radius: 3px
|
||||||
|
|
||||||
|
&.progress-uploading
|
||||||
|
background-color: hsl(hue($color-success), 80%, 65%) !important
|
||||||
|
|
||||||
|
&.progress-processing
|
||||||
|
+stripes($color-success, lighten($color-success, 15%), -45deg, 25px)
|
||||||
|
+stripes-animate
|
||||||
|
animation-duration: 1s
|
||||||
|
|
||||||
|
&.progress-error
|
||||||
|
background-color: $color-danger !important
|
||||||
|
|
||||||
|
.preview-thumbnail
|
||||||
|
width: 50px
|
||||||
|
height: 50px
|
||||||
|
min-width: 50px
|
||||||
|
min-height: 50px
|
||||||
|
margin-right: 10px
|
||||||
|
margin-top: 5px
|
||||||
|
border-radius: 3px
|
||||||
|
background-color: $color-background
|
||||||
|
|
||||||
|
.form-upload-file-meta-container
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.form-upload-file-meta
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
width: 100%
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
flex: 1
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline-block
|
||||||
|
padding: 5px 10px
|
||||||
|
|
||||||
|
&:first-child
|
||||||
|
padding-left: 0
|
||||||
|
|
||||||
|
&.dimensions, &.size
|
||||||
|
color: $color-text-dark-secondary
|
||||||
|
|
||||||
|
&.delete
|
||||||
|
margin-left: auto
|
||||||
|
|
||||||
|
&.name
|
||||||
|
+text-overflow-ellipsis
|
||||||
|
|
||||||
|
.file_delete
|
||||||
|
color: $color-danger
|
||||||
|
|
||||||
|
.form-upload-file-actions
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline-block
|
||||||
|
padding: 5px 10px
|
||||||
|
|
||||||
|
.file_delete
|
||||||
|
color: $color-danger
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
&.error
|
||||||
|
.form-control, input
|
||||||
|
border-color: $color-danger !important
|
||||||
|
|
||||||
|
ul.error
|
||||||
|
padding: 5px 0 0 0
|
||||||
|
margin: 0
|
||||||
|
color: $color-danger
|
||||||
|
list-style-type: none
|
38
src/styles/components/_inputs.sass
Normal file
38
src/styles/components/_inputs.sass
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* Inputs */
|
||||||
|
input, input.form-control,
|
||||||
|
textarea, textarea.form-control,
|
||||||
|
select, select.form-control
|
||||||
|
+input-generic
|
||||||
|
|
||||||
|
label, label.control-label
|
||||||
|
+label-generic
|
||||||
|
|
||||||
|
select, select.form-control
|
||||||
|
border-top-left-radius: 3px
|
||||||
|
border-top-right-radius: 3px
|
||||||
|
background-color: $color-background-light
|
||||||
|
|
||||||
|
option
|
||||||
|
background-color: white
|
||||||
|
|
||||||
|
input.fileupload
|
||||||
|
background-color: transparent
|
||||||
|
display: block
|
||||||
|
margin-top: 10px
|
||||||
|
|
||||||
|
textarea
|
||||||
|
resize: vertical
|
||||||
|
|
||||||
|
button, .btn
|
||||||
|
&.disabled
|
||||||
|
opacity: .5 !important
|
||||||
|
pointer-events: none !important
|
||||||
|
text-shadow: none !important
|
||||||
|
user-select: none !important
|
||||||
|
|
||||||
|
.input-group-flex
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.input-group-separator
|
||||||
|
margin: 10px 0
|
||||||
|
border-top: thin solid $color-background
|
47
src/styles/components/_jumbotron.sass
Normal file
47
src/styles/components/_jumbotron.sass
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Mainly overrides bootstrap jumbotron settings
|
||||||
|
.jumbotron
|
||||||
|
@extend .d-flex
|
||||||
|
@extend .mb-0
|
||||||
|
@extend .rounded-0
|
||||||
|
background:
|
||||||
|
position: center
|
||||||
|
repeat: no-repeat
|
||||||
|
size: cover
|
||||||
|
margin-bottom: 0
|
||||||
|
padding-top: 10em
|
||||||
|
padding-bottom: 10em
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-color: rgba(black, .5)
|
||||||
|
bottom: 0
|
||||||
|
content: ''
|
||||||
|
display: none
|
||||||
|
left: 0
|
||||||
|
position: absolute
|
||||||
|
right: 0
|
||||||
|
top: 0
|
||||||
|
visibility: hidden
|
||||||
|
|
||||||
|
// Black-transparent gradient from left to right to better read the overlay text.
|
||||||
|
&.jumbotron-overlay
|
||||||
|
*
|
||||||
|
z-index: 1
|
||||||
|
&:after
|
||||||
|
display: block
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
|
&.jumbotron-overlay-gradient
|
||||||
|
*
|
||||||
|
z-index: 1
|
||||||
|
&:after
|
||||||
|
background-color: transparent
|
||||||
|
background-image: linear-gradient(45deg, rgba(black, .5) 25%, transparent 50%)
|
||||||
|
display: block
|
||||||
|
visibility: visible
|
||||||
|
|
||||||
|
h2, p
|
||||||
|
text-shadow: 1px 1px rgba(black, .2), 1px 1px 25px rgba(black, .5)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
text-decoration: none
|
255
src/styles/components/_navbar.sass
Normal file
255
src/styles/components/_navbar.sass
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/* Top level navigation bar. */
|
||||||
|
.navbar
|
||||||
|
box-shadow: 0 2px $color-background
|
||||||
|
|
||||||
|
.nav
|
||||||
|
border: none
|
||||||
|
color: $color-text-dark-secondary
|
||||||
|
padding: 0
|
||||||
|
z-index: $z-index-base + 5
|
||||||
|
|
||||||
|
nav
|
||||||
|
margin-left: auto
|
||||||
|
margin-right: 0
|
||||||
|
|
||||||
|
.navbar-nav
|
||||||
|
margin-right: 0
|
||||||
|
+media-xs
|
||||||
|
margin: 0
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
li
|
||||||
|
user-select: none
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
img.gravatar
|
||||||
|
height: 28px
|
||||||
|
position: relative
|
||||||
|
width: 28px
|
||||||
|
|
||||||
|
.special
|
||||||
|
background-color: white
|
||||||
|
border-radius: 999em
|
||||||
|
box-shadow: 1px 1px 1px rgba(black, .2)
|
||||||
|
display: inline-block
|
||||||
|
font-size: 1.2em
|
||||||
|
height: 18px
|
||||||
|
left: 28px
|
||||||
|
position: absolute
|
||||||
|
top: 3px
|
||||||
|
width: 18px
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
&.subscriber
|
||||||
|
background-color: $color-success
|
||||||
|
color: white
|
||||||
|
font-size: .6em
|
||||||
|
|
||||||
|
&.demo
|
||||||
|
background-color: $color-info
|
||||||
|
color: white
|
||||||
|
font-size: .6em
|
||||||
|
|
||||||
|
&.none
|
||||||
|
color: $color-danger
|
||||||
|
|
||||||
|
i
|
||||||
|
+position-center-translate
|
||||||
|
|
||||||
|
.dropdown
|
||||||
|
.navbar-item
|
||||||
|
&:hover
|
||||||
|
box-shadow: none // Remove the blue underline usually on navbar, from dropdown items.
|
||||||
|
|
||||||
|
ul.dropdown-menu
|
||||||
|
li
|
||||||
|
a
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.subitem // e.g. "Not Sintel? Log out"
|
||||||
|
font-size: .8em
|
||||||
|
text-transform: initial
|
||||||
|
|
||||||
|
i
|
||||||
|
width: 30px
|
||||||
|
|
||||||
|
&.subscription-status
|
||||||
|
a, a:hover
|
||||||
|
color: $white
|
||||||
|
|
||||||
|
&.none
|
||||||
|
background-color: $color-danger
|
||||||
|
|
||||||
|
&.subscriber
|
||||||
|
background-color: $color-success
|
||||||
|
|
||||||
|
&.demo
|
||||||
|
background-color: $color-info
|
||||||
|
|
||||||
|
span.info
|
||||||
|
display: block
|
||||||
|
|
||||||
|
span.renew
|
||||||
|
display: block
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
|
||||||
|
.nav-link
|
||||||
|
@extend .d-flex
|
||||||
|
|
||||||
|
.nav-title
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.navbar-item
|
||||||
|
align-items: center
|
||||||
|
display: flex
|
||||||
|
user-select: none
|
||||||
|
color: inherit
|
||||||
|
|
||||||
|
+media-sm
|
||||||
|
padding-left: 10px
|
||||||
|
padding-right: 10px
|
||||||
|
|
||||||
|
&:hover, &:focus
|
||||||
|
color: $primary
|
||||||
|
background-color: transparent
|
||||||
|
box-shadow: inset 0 -3px 0 $primary
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
box-shadow: inset 0 -3px 0 $primary
|
||||||
|
|
||||||
|
|
||||||
|
/* Secondary navigation. */
|
||||||
|
$nav-secondary-bar-size: -2px
|
||||||
|
.nav-secondary
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.nav-link
|
||||||
|
color: $color-text
|
||||||
|
cursor: pointer
|
||||||
|
margin-bottom: 2px
|
||||||
|
transition: color 150ms ease-in-out
|
||||||
|
|
||||||
|
span
|
||||||
|
position: relative
|
||||||
|
top: 2px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-color: transparent
|
||||||
|
bottom: 0
|
||||||
|
content: ''
|
||||||
|
height: 2px
|
||||||
|
position: absolute
|
||||||
|
right: 0
|
||||||
|
left: 0
|
||||||
|
width: 0
|
||||||
|
transition: width 150ms ease-in-out
|
||||||
|
|
||||||
|
.nav-link:hover,
|
||||||
|
.nav-link.active,
|
||||||
|
.nav-item.dropdown.show > .nav-link
|
||||||
|
// Blue bar on the bottom.
|
||||||
|
&:after
|
||||||
|
background-color: $primary-accent
|
||||||
|
background-image: linear-gradient(to right, $primary-accent 70%, $primary)
|
||||||
|
height: 2px
|
||||||
|
width: 100%
|
||||||
|
bottom: -2px
|
||||||
|
|
||||||
|
span
|
||||||
|
+active-gradient
|
||||||
|
|
||||||
|
i
|
||||||
|
color: $primary-accent
|
||||||
|
|
||||||
|
.nav-link.active
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
&.nav-secondary-vertical
|
||||||
|
align-items: flex-start
|
||||||
|
flex-direction: column
|
||||||
|
box-shadow: none // Last item on the list already has a box-shadow.
|
||||||
|
|
||||||
|
> li
|
||||||
|
width: 100% // span across the whole width.
|
||||||
|
|
||||||
|
// Blue bar on the side.
|
||||||
|
.nav-link
|
||||||
|
&:hover,
|
||||||
|
&.active
|
||||||
|
color: $primary
|
||||||
|
@extend .bg-white
|
||||||
|
|
||||||
|
&:after
|
||||||
|
background-image: linear-gradient($primary-accent 70%, $primary)
|
||||||
|
height: 100%
|
||||||
|
left: initial
|
||||||
|
top: 0
|
||||||
|
width: 3px
|
||||||
|
|
||||||
|
// Big navigation dropdown.
|
||||||
|
.nav-main
|
||||||
|
min-width: initial
|
||||||
|
|
||||||
|
.nav-secondary
|
||||||
|
.nav-link
|
||||||
|
@extend .pr-5
|
||||||
|
box-shadow: none
|
||||||
|
|
||||||
|
&.nav-see-more
|
||||||
|
color: $primary
|
||||||
|
|
||||||
|
i, span
|
||||||
|
+active-gradient
|
||||||
|
|
||||||
|
.navbar-overlay
|
||||||
|
+media-lg
|
||||||
|
display: block
|
||||||
|
bottom: 0
|
||||||
|
display: none
|
||||||
|
left: 0
|
||||||
|
height: 100%
|
||||||
|
position: absolute
|
||||||
|
right: 0
|
||||||
|
top: 0
|
||||||
|
transition: background-color 350ms ease-in-out
|
||||||
|
width: 100%
|
||||||
|
z-index: 0
|
||||||
|
|
||||||
|
&.is-active
|
||||||
|
background-color: $color-background-nav
|
||||||
|
text-shadow: none
|
||||||
|
|
||||||
|
nav.navbar
|
||||||
|
.navbar-collapse
|
||||||
|
> ul > li > .navbar-item
|
||||||
|
padding: $navbar-nav-link-padding-x
|
||||||
|
height: $nav-link-height
|
||||||
|
|
||||||
|
|
||||||
|
.navbar-backdrop-container
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
|
||||||
|
img
|
||||||
|
display: none
|
||||||
|
position: fixed
|
||||||
|
width: 100%
|
||||||
|
align-self: flex-start
|
||||||
|
+media-md
|
||||||
|
display: block
|
||||||
|
+media-lg
|
||||||
|
display: block
|
||||||
|
|
||||||
|
|
||||||
|
.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu
|
||||||
|
margin-top: 0
|
||||||
|
|
||||||
|
.navbar+.page-content
|
||||||
|
padding-top: $nav-link-height
|
75
src/styles/components/_overlay.sass
Normal file
75
src/styles/components/_overlay.sass
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#page-overlay
|
||||||
|
background-color: rgba(black, .8)
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
left: 0
|
||||||
|
z-index: $z-index-base + 15
|
||||||
|
visibility: hidden
|
||||||
|
opacity: 0
|
||||||
|
transition: opacity 150ms ease-in-out
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
img
|
||||||
|
user-select: none
|
||||||
|
display: block
|
||||||
|
max-height: 96%
|
||||||
|
max-width: 96%
|
||||||
|
z-index: 0
|
||||||
|
|
||||||
|
box-shadow: 0 0 15px rgba(black, .2), 0 0 100px rgba(black, .5)
|
||||||
|
p.caption
|
||||||
|
position: absolute
|
||||||
|
bottom: 1%
|
||||||
|
|
||||||
|
&.active
|
||||||
|
visibility: visible
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
.no-preview
|
||||||
|
user-select: none
|
||||||
|
z-index: 0
|
||||||
|
color: $color-text-light-secondary
|
||||||
|
|
||||||
|
.nav-prev, .nav-next
|
||||||
|
display: block
|
||||||
|
font:
|
||||||
|
family: 'pillar-font'
|
||||||
|
size: 2em
|
||||||
|
height: 80%
|
||||||
|
width: 50px
|
||||||
|
cursor: pointer
|
||||||
|
color: $color-text-light-secondary
|
||||||
|
z-index: 1
|
||||||
|
+position-center-translate
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: $color-text-light
|
||||||
|
|
||||||
|
&:before, &:after
|
||||||
|
+position-center-translate
|
||||||
|
|
||||||
|
.nav-prev
|
||||||
|
left: 50px
|
||||||
|
&:before
|
||||||
|
content: '\e839'
|
||||||
|
|
||||||
|
.nav-next
|
||||||
|
left: initial
|
||||||
|
right: 0
|
||||||
|
&:before
|
||||||
|
content: '\e83a'
|
||||||
|
|
||||||
|
|
||||||
|
&.video
|
||||||
|
.video-embed
|
||||||
|
+position-center-translate
|
||||||
|
position: fixed
|
||||||
|
|
||||||
|
iframe
|
||||||
|
width: 853px
|
||||||
|
height: 480px
|
26
src/styles/components/_popover.sass
Normal file
26
src/styles/components/_popover.sass
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.popover
|
||||||
|
background-color: lighten($color-background-nav, 5%)
|
||||||
|
border-radius: 3px
|
||||||
|
box-shadow: 1px 1px 2px rgba(black, .2)
|
||||||
|
border: thin solid lighten($color-background-nav, 10%)
|
||||||
|
|
||||||
|
&.in
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
.popover-title
|
||||||
|
background-color: lighten($color-background-nav, 10%)
|
||||||
|
border-bottom: thin solid lighten($color-background-nav, 3%)
|
||||||
|
color: $color-text-light-primary
|
||||||
|
|
||||||
|
.popover-content
|
||||||
|
color: $color-text-light
|
||||||
|
font-size: .9em
|
||||||
|
|
||||||
|
&.top .arrow:after
|
||||||
|
border-top-color: lighten($color-background-nav, 5%)
|
||||||
|
&.bottom .arrow:after
|
||||||
|
border-bottom-color: lighten($color-background-nav, 5%)
|
||||||
|
&.left .arrow:after
|
||||||
|
border-left-color: lighten($color-background-nav, 5%)
|
||||||
|
&.right .arrow:after
|
||||||
|
border-right-color: lighten($color-background-nav, 5%)
|
87
src/styles/components/_search.sass
Normal file
87
src/styles/components/_search.sass
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#search-overlay
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
pointer-events: none
|
||||||
|
visibility: hidden
|
||||||
|
opacity: 0
|
||||||
|
z-index: $z-index-base + 4
|
||||||
|
transition: opacity 150ms ease-in-out
|
||||||
|
|
||||||
|
&.active
|
||||||
|
opacity: 1
|
||||||
|
visibility: visible
|
||||||
|
background-color: rgba($color-background-nav, .7)
|
||||||
|
|
||||||
|
.search-input
|
||||||
|
+media-lg
|
||||||
|
max-width: 350px
|
||||||
|
+media-md
|
||||||
|
max-width: 350px
|
||||||
|
+media-sm
|
||||||
|
max-width: 120px
|
||||||
|
+media-xs
|
||||||
|
display: block
|
||||||
|
margin: 0 10px
|
||||||
|
position: absolute
|
||||||
|
z-index: $z-index-base
|
||||||
|
right: 5px
|
||||||
|
position: relative
|
||||||
|
float: left
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.search-icon
|
||||||
|
position: absolute
|
||||||
|
top: 4px
|
||||||
|
left: 10px
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:after
|
||||||
|
@extend .tooltip-inner
|
||||||
|
|
||||||
|
content: 'Use advanced search...'
|
||||||
|
font-size: .85em
|
||||||
|
font-style: normal
|
||||||
|
left: -10px
|
||||||
|
opacity: 0
|
||||||
|
pointer-events: none
|
||||||
|
position: absolute
|
||||||
|
top: 30px
|
||||||
|
transition: top 150ms ease-in-out, opacity 150ms ease-in-out
|
||||||
|
width: 150px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
&:after
|
||||||
|
opacity: 1
|
||||||
|
top: 35px
|
||||||
|
|
||||||
|
|
||||||
|
#cloud-search, .tt-hint
|
||||||
|
+text-overflow-ellipsis
|
||||||
|
border: thin solid $color-background
|
||||||
|
border-radius: 3px
|
||||||
|
font:
|
||||||
|
size: 1em
|
||||||
|
weight: 400
|
||||||
|
margin: 0
|
||||||
|
min-height: 32px
|
||||||
|
outline: none
|
||||||
|
padding: 0 20px 0 40px
|
||||||
|
transition: border 100ms ease-in-out
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
box-shadow: none
|
||||||
|
border: none
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: rgba($color-text, .5)
|
||||||
|
transition: color 150ms ease-in-out
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
&::placeholder
|
||||||
|
color: rgba($color-text, .6)
|
6
src/styles/components/_shortcode.sass
Normal file
6
src/styles/components/_shortcode.sass
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
p.shortcode.nocap
|
||||||
|
padding: 0.6em 3em
|
||||||
|
font-size: .8em
|
||||||
|
color: $color-text-dark-primary
|
||||||
|
background-color: $color-background-dark
|
||||||
|
border-radius: 3px
|
21
src/styles/components/_statusbar.sass
Normal file
21
src/styles/components/_statusbar.sass
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#status-bar
|
||||||
|
opacity: 0
|
||||||
|
transition: all 250ms ease-in-out
|
||||||
|
|
||||||
|
i
|
||||||
|
margin-right: 5px
|
||||||
|
|
||||||
|
&.info
|
||||||
|
color: $color-info
|
||||||
|
&.error
|
||||||
|
color: $color-danger
|
||||||
|
&.warning
|
||||||
|
color: $color-warning
|
||||||
|
&.success
|
||||||
|
color: $color-success
|
||||||
|
&.default
|
||||||
|
color: $color-text-light
|
||||||
|
|
||||||
|
&.active
|
||||||
|
opacity: 1
|
||||||
|
|
5
src/styles/components/_tooltip.sass
Normal file
5
src/styles/components/_tooltip.sass
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.tooltip
|
||||||
|
transition: none
|
||||||
|
|
||||||
|
.tooltip-inner
|
||||||
|
white-space: nowrap
|
@@ -1,9 +1,12 @@
|
|||||||
/* So it's possible to override the path before importing font-pillar.sass */
|
/* SCROLL TO READ ABOUT UPDATING THIS FILE FROM FONTELLO */
|
||||||
|
|
||||||
|
/* Makes it possible to override the path before importing font-pillar.sass */
|
||||||
$pillar-font-path: "../font" !default
|
$pillar-font-path: "../font" !default
|
||||||
|
|
||||||
|
/* Font properties. */
|
||||||
@font-face
|
@font-face
|
||||||
font-family: 'pillar-font'
|
font-family: 'pillar-font'
|
||||||
src: url('#{$pillar-font-path}/pillar-font.woff?55726379') format("woff"), url('#{$pillar-font-path}/pillar-font.woff2?55726379') format("woff2")
|
src: url('#{$pillar-font-path}/pillar-font.woff?54788822') format("woff"), url('#{$pillar-font-path}/pillar-font.woff2?54788822') format("woff2")
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
font-style: normal
|
font-style: normal
|
||||||
|
|
||||||
@@ -17,23 +20,99 @@ $pillar-font-path: "../font" !default
|
|||||||
width: 1em
|
width: 1em
|
||||||
margin-right: .2em
|
margin-right: .2em
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
/* opacity: .8;
|
|
||||||
/* For safety - reset parent styles, that can break glyph codes
|
|
||||||
font-variant: normal
|
font-variant: normal
|
||||||
text-transform: none
|
text-transform: none
|
||||||
/* fix buttons height, for twitter bootstrap
|
|
||||||
line-height: 1em
|
line-height: 1em
|
||||||
/* Animation center compensation - margins should be symmetric
|
|
||||||
/* remove if not needed
|
|
||||||
margin-left: .2em
|
margin-left: .2em
|
||||||
/* you can be more comfortable with increased icons size
|
|
||||||
/* font-size: 120%;
|
|
||||||
/* Font smoothing. That was taken from TWBS
|
|
||||||
-webkit-font-smoothing: antialiased
|
-webkit-font-smoothing: antialiased
|
||||||
-moz-osx-font-smoothing: grayscale
|
-moz-osx-font-smoothing: grayscale
|
||||||
/* Uncomment for 3D effect
|
|
||||||
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3);
|
/* Icon aliases. */
|
||||||
|
/* Empty icons, multiple names for the same/unasigned icon, etc. */
|
||||||
|
.pi, .pi-blank
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
font-family: "pillar-font"
|
||||||
|
font-style: normal
|
||||||
|
font-weight: normal
|
||||||
|
speak: none
|
||||||
|
display: inline-block
|
||||||
|
text-decoration: inherit
|
||||||
|
width: 1em
|
||||||
|
text-align: center
|
||||||
|
font-variant: normal
|
||||||
|
text-transform: none
|
||||||
|
line-height: 1em
|
||||||
|
-webkit-font-smoothing: antialiased
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:before
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.pi-svnman:before
|
||||||
|
content: '\f1c0'
|
||||||
|
|
||||||
|
/* Assets */
|
||||||
|
.pi-group
|
||||||
|
@extend .pi-folder
|
||||||
|
.pi-video
|
||||||
|
@extend .pi-film-thick
|
||||||
|
.pi-file
|
||||||
|
@extend .pi-file-archive
|
||||||
|
.pi-asset
|
||||||
|
@extend .pi-file-archive
|
||||||
|
.pi-group_texture
|
||||||
|
@extend .pi-folder-texture
|
||||||
|
.pi-post
|
||||||
|
@extend .pi-newspaper
|
||||||
|
.pi-page
|
||||||
|
@extend .pi-document
|
||||||
|
|
||||||
|
/* License */
|
||||||
|
.pi-license-cc-zero:before
|
||||||
|
content: '\e85a'
|
||||||
|
.pi-license-cc-sa:before
|
||||||
|
content: '\e858'
|
||||||
|
top: 1px
|
||||||
|
.pi-license-cc-nd:before
|
||||||
|
content: '\e859'
|
||||||
|
.pi-license-cc-nc:before
|
||||||
|
content: '\e857'
|
||||||
|
|
||||||
|
.pi-license-cc-0
|
||||||
|
@extend .pi-license-cc-zero
|
||||||
|
position: relative
|
||||||
|
top: 1px
|
||||||
|
.pi-license-cc-by-sa
|
||||||
|
@extend .pi-license-cc-sa
|
||||||
|
.pi-license-cc-by-nd
|
||||||
|
@extend .pi-license-cc-nd
|
||||||
|
.pi-license-cc-by-nc
|
||||||
|
@extend .pi-license-cc-nc
|
||||||
|
|
||||||
|
.pi-license-cc-by-sa,
|
||||||
|
.pi-license-cc-by-nd,
|
||||||
|
.pi-license-cc-by-nc
|
||||||
|
@extend .pi
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: '\e807'
|
||||||
|
left: -27px
|
||||||
|
|
||||||
|
&:before
|
||||||
|
left: 27px
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
* Here begins the CSS code generated by fontello.com by using *
|
||||||
|
* the config.json file in /pillar/web/static/assets/font *
|
||||||
|
* Just convert the icon classes from pillar-font.css to Sass *
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
* When adding icons, only add/overwrite icon classes e.g. .pi-bla *
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*/
|
||||||
|
|
||||||
.pi-collection-plus:before
|
.pi-collection-plus:before
|
||||||
content: '\e800'
|
content: '\e800'
|
||||||
@@ -430,6 +509,11 @@ $pillar-font-path: "../font" !default
|
|||||||
|
|
||||||
/* ''
|
/* ''
|
||||||
|
|
||||||
|
.pi-speed:before
|
||||||
|
content: '\e84f'
|
||||||
|
|
||||||
|
/* ''
|
||||||
|
|
||||||
.pi-attention:before
|
.pi-attention:before
|
||||||
content: '\e850'
|
content: '\e850'
|
||||||
|
|
||||||
@@ -580,11 +664,6 @@ $pillar-font-path: "../font" !default
|
|||||||
|
|
||||||
/* ''
|
/* ''
|
||||||
|
|
||||||
.pi-users:before
|
|
||||||
content: '\e86e'
|
|
||||||
|
|
||||||
/* ''
|
|
||||||
|
|
||||||
.pi-flamenco:before
|
.pi-flamenco:before
|
||||||
content: '\e86f'
|
content: '\e86f'
|
||||||
|
|
||||||
@@ -605,6 +684,11 @@ $pillar-font-path: "../font" !default
|
|||||||
|
|
||||||
/* ''
|
/* ''
|
||||||
|
|
||||||
|
.pi-users:before
|
||||||
|
content: '\e873'
|
||||||
|
|
||||||
|
/* ''
|
||||||
|
|
||||||
.pi-pause:before
|
.pi-pause:before
|
||||||
content: '\f00e'
|
content: '\f00e'
|
||||||
|
|
||||||
@@ -640,6 +724,16 @@ $pillar-font-path: "../font" !default
|
|||||||
|
|
||||||
/* ''
|
/* ''
|
||||||
|
|
||||||
|
.pi-social-instagram:before
|
||||||
|
content: '\f16d'
|
||||||
|
|
||||||
|
/* ''
|
||||||
|
|
||||||
|
.pi-database:before
|
||||||
|
content: '\f1c0'
|
||||||
|
|
||||||
|
/* ''
|
||||||
|
|
||||||
.pi-newspaper:before
|
.pi-newspaper:before
|
||||||
content: '\f1ea'
|
content: '\f1ea'
|
||||||
|
|
||||||
|
@@ -1,15 +1,84 @@
|
|||||||
@import _normalize
|
// Bootstrap variables and utilities.
|
||||||
|
@import "../../node_modules/bootstrap/scss/functions"
|
||||||
|
@import "../../node_modules/bootstrap/scss/variables"
|
||||||
|
@import "../../node_modules/bootstrap/scss/mixins"
|
||||||
|
|
||||||
@import _config
|
@import _config
|
||||||
@import _utils
|
@import _utils
|
||||||
|
|
||||||
|
// Bootstrap components.
|
||||||
|
@import "../../node_modules/bootstrap/scss/root"
|
||||||
|
@import "../../node_modules/bootstrap/scss/reboot"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/type"
|
||||||
|
@import "../../node_modules/bootstrap/scss/images"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/code"
|
||||||
|
@import "../../node_modules/bootstrap/scss/grid"
|
||||||
|
@import "../../node_modules/bootstrap/scss/tables"
|
||||||
|
@import "../../node_modules/bootstrap/scss/forms"
|
||||||
|
@import "../../node_modules/bootstrap/scss/buttons"
|
||||||
|
@import "../../node_modules/bootstrap/scss/transitions"
|
||||||
|
@import "../../node_modules/bootstrap/scss/dropdown"
|
||||||
|
@import "../../node_modules/bootstrap/scss/button-group"
|
||||||
|
@import "../../node_modules/bootstrap/scss/input-group"
|
||||||
|
@import "../../node_modules/bootstrap/scss/custom-forms"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/nav"
|
||||||
|
@import "../../node_modules/bootstrap/scss/navbar"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/card"
|
||||||
|
@import "../../node_modules/bootstrap/scss/breadcrumb"
|
||||||
|
@import "../../node_modules/bootstrap/scss/pagination"
|
||||||
|
@import "../../node_modules/bootstrap/scss/badge"
|
||||||
|
@import "../../node_modules/bootstrap/scss/jumbotron"
|
||||||
|
@import "../../node_modules/bootstrap/scss/alert"
|
||||||
|
@import "../../node_modules/bootstrap/scss/progress"
|
||||||
|
@import "../../node_modules/bootstrap/scss/media"
|
||||||
|
@import "../../node_modules/bootstrap/scss/list-group"
|
||||||
|
@import "../../node_modules/bootstrap/scss/close"
|
||||||
|
@import "../../node_modules/bootstrap/scss/modal"
|
||||||
|
@import "../../node_modules/bootstrap/scss/tooltip"
|
||||||
|
@import "../../node_modules/bootstrap/scss/popover"
|
||||||
|
@import "../../node_modules/bootstrap/scss/carousel"
|
||||||
|
|
||||||
|
@import "../../node_modules/bootstrap/scss/utilities"
|
||||||
|
@import "../../node_modules/bootstrap/scss/print"
|
||||||
|
|
||||||
|
// Pillar components.
|
||||||
|
@import "apps_base"
|
||||||
|
@import "components/base"
|
||||||
|
|
||||||
|
@import "components/jumbotron"
|
||||||
|
@import "components/alerts"
|
||||||
|
@import "components/navbar"
|
||||||
|
@import "components/dropdown"
|
||||||
|
@import "components/footer"
|
||||||
|
@import "components/shortcode"
|
||||||
|
@import "components/statusbar"
|
||||||
|
@import "components/search"
|
||||||
|
|
||||||
|
@import "components/flyout"
|
||||||
|
@import "components/forms"
|
||||||
|
@import "components/inputs"
|
||||||
|
@import "components/buttons"
|
||||||
|
@import "components/popover"
|
||||||
|
@import "components/tooltip"
|
||||||
|
@import "components/checkbox"
|
||||||
|
@import "components/overlay"
|
||||||
|
@import "components/card"
|
||||||
|
|
||||||
|
|
||||||
/* Generic styles (comments, notifications, etc) come from base.css */
|
/* Generic styles (comments, notifications, etc) come from base.css */
|
||||||
|
@import _notifications
|
||||||
|
@import _comments
|
||||||
|
|
||||||
@import _project
|
@import _project
|
||||||
@import _project-sharing
|
@import _project-sharing
|
||||||
@import _project-dashboard
|
@import _project-dashboard
|
||||||
@import _user
|
@import _user
|
||||||
@import _search
|
|
||||||
@import _organizations
|
@import _organizations
|
||||||
|
@import _search
|
||||||
|
|
||||||
/* services, about, etc */
|
/* services, about, etc */
|
||||||
@import _pages
|
@import _pages
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
/* jsTree overrides */
|
/* jsTree overrides */
|
||||||
|
|
||||||
$tree-color-text: $color-text-dark-primary
|
$tree-color-text: $color-text-dark-primary
|
||||||
$tree-color-highlight: hsl(hue($color-background-active), 40%, 50%)
|
$tree-color-highlight: $color-primary-accent
|
||||||
$tree-color-highlight-background: hsl(hue($color-background-active), 40%, 50%)
|
$tree-color-highlight-background: $white
|
||||||
$tree-color-highlight-background-text: white
|
$tree-color-highlight-background-text: $primary
|
||||||
|
|
||||||
.jstree-default
|
.jstree-default
|
||||||
/* list item */
|
/* list item */
|
||||||
@@ -34,11 +33,10 @@ $tree-color-highlight-background-text: white
|
|||||||
|
|
||||||
&[data-node-type="page"],
|
&[data-node-type="page"],
|
||||||
&[data-node-type="blog"]
|
&[data-node-type="blog"]
|
||||||
color: darken($tree-color-highlight, 5%)
|
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
.jstree-anchor
|
.jstree-anchor
|
||||||
padding: 5px 8px 1px 8px
|
padding: 0 6px
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
top: 3px !important
|
top: 3px !important
|
||||||
@@ -63,49 +61,48 @@ $tree-color-highlight-background-text: white
|
|||||||
&.jstree-open
|
&.jstree-open
|
||||||
/* Text of children for an open tree (like a folder) */
|
/* Text of children for an open tree (like a folder) */
|
||||||
.jstree-children > .jstree-node
|
.jstree-children > .jstree-node
|
||||||
padding-left: 15px !important
|
padding-left: 16px !important
|
||||||
|
|
||||||
.jstree-icon:empty
|
.jstree-icon:empty
|
||||||
left: 20px !important
|
left: 20px !important
|
||||||
|
|
||||||
// Tweaks for specific icons
|
// Tweaks for specific icons
|
||||||
&.pi-file-archive
|
&.pi-file-archive
|
||||||
left: 22px !important
|
left: 25px !important
|
||||||
&.pi-folder
|
&.pi-folder
|
||||||
left: 21px !important
|
left: 20px !important
|
||||||
font-size: .9em !important
|
font-size: .9em !important
|
||||||
&.pi-film-thick
|
&.pi-splay
|
||||||
left: 22px !important
|
left: 20px !important
|
||||||
font-size: .85em !important
|
font-size: .85em !important
|
||||||
|
|
||||||
.jstree-anchor
|
.jstree-anchor
|
||||||
box-shadow: inset 1px 0 0 0 rgba($tree-color-text, .2)
|
// box-shadow: inset 1px 0 0 0 $color-background
|
||||||
|
|
||||||
/* Closed Folder */
|
/* Closed Folder */
|
||||||
// &.jstree-closed
|
// &.jstree-closed
|
||||||
|
|
||||||
&.jstree-open .jstree-icon.jstree-ocl,
|
&.jstree-open .jstree-icon.jstree-ocl,
|
||||||
&.jstree-closed .jstree-icon.jstree-ocl
|
&.jstree-closed .jstree-icon.jstree-ocl
|
||||||
|
float: left
|
||||||
|
min-width: 30px
|
||||||
|
opacity: 0
|
||||||
position: absolute
|
position: absolute
|
||||||
z-index: 1
|
z-index: 1
|
||||||
opacity: 0
|
|
||||||
min-width: 30px
|
|
||||||
float: left
|
|
||||||
|
|
||||||
/* The text of the last level item */
|
/* The text of the last level item */
|
||||||
.jstree-anchor
|
.jstree-anchor
|
||||||
+media-xs
|
+media-xs
|
||||||
width: 98%
|
|
||||||
padding: 0 !important
|
padding: 0 !important
|
||||||
|
width: 98%
|
||||||
border: none
|
border: none
|
||||||
|
font-size: 13px
|
||||||
height: inherit
|
height: inherit
|
||||||
line-height: 26px
|
line-height: 24px
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
padding-left: 28px
|
padding-left: 28px
|
||||||
padding-right: 10px
|
padding-right: 10px
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis
|
||||||
transition: none
|
|
||||||
transition: color 50ms ease-in-out, background-color 100ms ease-in-out
|
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ $tree-color-highlight-background-text: white
|
|||||||
&:after
|
&:after
|
||||||
content: '\e83a' !important
|
content: '\e83a' !important
|
||||||
font-family: 'pillar-font'
|
font-family: 'pillar-font'
|
||||||
color: white
|
color: $tree-color-highlight-background-text
|
||||||
display: none
|
display: none
|
||||||
position: absolute
|
position: absolute
|
||||||
right: 7px
|
right: 7px
|
||||||
@@ -121,34 +118,29 @@ $tree-color-highlight-background-text: white
|
|||||||
|
|
||||||
// Icon, not selected
|
// Icon, not selected
|
||||||
.jstree-icon
|
.jstree-icon
|
||||||
color: $color-text-dark-secondary
|
color: $tree-color-text
|
||||||
font-size: 95% !important
|
|
||||||
margin: 0 !important
|
margin: 0 !important
|
||||||
|
|
||||||
/* Selected item */
|
/* Selected item */
|
||||||
&.jstree-clicked
|
&.jstree-clicked
|
||||||
background-color: $tree-color-highlight-background !important
|
color: $tree-color-highlight-background-text !important
|
||||||
color: white !important
|
font-weight: bold
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
display: block
|
display: block
|
||||||
color: white !important
|
color: $tree-color-highlight-background-text !important
|
||||||
|
|
||||||
.jstree-ocl,
|
.jstree-ocl,
|
||||||
.jstree-icon
|
.jstree-icon
|
||||||
color: white
|
color: $tree-color-highlight-background-text
|
||||||
|
|
||||||
/* hover an active item */
|
/* hover an active item */
|
||||||
&.jstree-hovered
|
&.jstree-hovered
|
||||||
background-color: lighten($tree-color-highlight-background, 10%) !important
|
|
||||||
box-shadow: none
|
box-shadow: none
|
||||||
color: white !important
|
color: $tree-color-highlight-background-text !important
|
||||||
|
|
||||||
&.jstree-hovered .jstree-icon
|
&.jstree-hovered .jstree-icon
|
||||||
color: white !important
|
color: $tree-color-highlight-background-text !important
|
||||||
|
|
||||||
.jstree-hovered
|
|
||||||
background-color: rgba($tree-color-highlight, .1) !important
|
|
||||||
|
|
||||||
.jstree-leaf .jstree-clicked
|
.jstree-leaf .jstree-clicked
|
||||||
width: 100% !important
|
width: 100% !important
|
||||||
@@ -184,8 +176,8 @@ $tree-color-highlight-background-text: white
|
|||||||
position: absolute
|
position: absolute
|
||||||
|
|
||||||
&:empty
|
&:empty
|
||||||
line-height: 26px
|
line-height: 24px
|
||||||
left: 5px
|
left: 3px
|
||||||
|
|
||||||
&.is_subscriber
|
&.is_subscriber
|
||||||
.jstree-node
|
.jstree-node
|
||||||
@@ -196,63 +188,6 @@ $tree-color-highlight-background-text: white
|
|||||||
&:after
|
&:after
|
||||||
display: none !important
|
display: none !important
|
||||||
|
|
||||||
&.blog
|
|
||||||
.jstree-anchor
|
|
||||||
padding: 6px 6px 6px 12px
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
color: $tree-color-highlight
|
|
||||||
|
|
||||||
&.post
|
|
||||||
border-bottom: thin solid $color-background-dark
|
|
||||||
|
|
||||||
&.jstree-clicked
|
|
||||||
&.post
|
|
||||||
background-color: transparent !important
|
|
||||||
|
|
||||||
&:after
|
|
||||||
top: 8px
|
|
||||||
color: $tree-color-highlight !important
|
|
||||||
|
|
||||||
.tree-item-info
|
|
||||||
color: $color-text
|
|
||||||
|
|
||||||
.tree-item-title
|
|
||||||
color: $tree-color-highlight
|
|
||||||
|
|
||||||
.tree-item
|
|
||||||
line-height: initial
|
|
||||||
padding-right: 10px
|
|
||||||
|
|
||||||
&-title
|
|
||||||
font-size: 1.2em
|
|
||||||
overflow: initial
|
|
||||||
text-overflow: initial
|
|
||||||
white-space: normal
|
|
||||||
|
|
||||||
&-info
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
display: block
|
|
||||||
font-size: .8em
|
|
||||||
padding: 5px
|
|
||||||
|
|
||||||
&-thumbnail
|
|
||||||
align-items: center
|
|
||||||
background-color: $color-background
|
|
||||||
border-radius: 3px
|
|
||||||
color: $color-text-dark-secondary
|
|
||||||
display: flex
|
|
||||||
float: left
|
|
||||||
height: 70px
|
|
||||||
justify-content: center
|
|
||||||
margin: 0 10px 0 -5px
|
|
||||||
width: 70px
|
|
||||||
|
|
||||||
img
|
|
||||||
height: 70px
|
|
||||||
width: 70px
|
|
||||||
|
|
||||||
|
|
||||||
.jstree-loading
|
.jstree-loading
|
||||||
padding: 5px
|
padding: 5px
|
||||||
color: $color-text-light-secondary
|
color: $color-text-light-secondary
|
||||||
@@ -269,7 +204,7 @@ $tree-color-highlight-background-text: white
|
|||||||
|
|
||||||
.jstree-default .jstree-node.jstree-closed .jstree-icon.jstree-ocl + .jstree-anchor,
|
.jstree-default .jstree-node.jstree-closed .jstree-icon.jstree-ocl + .jstree-anchor,
|
||||||
.jstree-default .jstree-node.jstree-open .jstree-icon.jstree-ocl + .jstree-anchor
|
.jstree-default .jstree-node.jstree-open .jstree-icon.jstree-ocl + .jstree-anchor
|
||||||
padding-left: 28px !important
|
padding-left: 24px !important
|
||||||
|
|
||||||
/* hovered text */
|
/* hovered text */
|
||||||
.jstree-default .jstree-hovered,
|
.jstree-default .jstree-hovered,
|
||||||
@@ -279,12 +214,10 @@ $tree-color-highlight-background-text: white
|
|||||||
|
|
||||||
|
|
||||||
a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked
|
a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked
|
||||||
background-color: rgba($tree-color-highlight-background, .8) !important
|
color: $tree-color-highlight-background-text !important
|
||||||
color: white !important
|
|
||||||
|
|
||||||
a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked
|
a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked+ul li a.jstree-anchor.jstree-clicked
|
||||||
background-color: rgba($tree-color-highlight-background, .8) !important
|
color: $tree-color-highlight-background-text !important
|
||||||
color: white !important
|
|
||||||
|
|
||||||
i.jstree-icon.jstree-ocl
|
i.jstree-icon.jstree-ocl
|
||||||
color: rgba($tree-color-text, .5) !important
|
color: rgba($tree-color-text, .5) !important
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
$videoplayer-controls-color: white
|
$videoplayer-controls-color: white
|
||||||
$videoplayer-background-color: $color-background-nav
|
$videoplayer-background-color: darken($primary, 10%)
|
||||||
|
|
||||||
|
$videoplayer-progress-bar-height: .5em
|
||||||
|
|
||||||
.video-js
|
.video-js
|
||||||
.vjs-big-play-button:before, .vjs-control:before, .vjs-modal-dialog
|
.vjs-big-play-button:before, .vjs-control:before, .vjs-modal-dialog
|
||||||
@@ -30,7 +32,6 @@ $videoplayer-background-color: $color-background-nav
|
|||||||
font-weight: normal
|
font-weight: normal
|
||||||
font-style: normal
|
font-style: normal
|
||||||
|
|
||||||
|
|
||||||
.vjs-icon-play
|
.vjs-icon-play
|
||||||
font-family: VideoJS
|
font-family: VideoJS
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
@@ -285,7 +286,6 @@ $videoplayer-background-color: $color-background-nav
|
|||||||
line-height: 1
|
line-height: 1
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
font-style: normal
|
font-style: normal
|
||||||
font-family: Arial, Helvetica, sans-serif
|
|
||||||
-webkit-user-select: none
|
-webkit-user-select: none
|
||||||
-moz-user-select: none
|
-moz-user-select: none
|
||||||
-ms-user-select: none
|
-ms-user-select: none
|
||||||
@@ -453,20 +453,22 @@ body.vjs-full-window
|
|||||||
list-style: none
|
list-style: none
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0.2em 0
|
padding: 0.2em 0
|
||||||
line-height: 1.4em
|
line-height: 1.8em
|
||||||
font-size: 1.2em
|
font-size: 1.1em
|
||||||
text-align: center
|
text-align: center
|
||||||
text-transform: lowercase
|
text-transform: lowercase
|
||||||
|
|
||||||
&:focus, &:hover
|
&:focus, &:hover
|
||||||
outline: 0
|
background-color: darken($primary, 20%)
|
||||||
background-color: #73859f
|
|
||||||
background-color: rgba(115, 133, 159, 0.5)
|
|
||||||
&.vjs-selected
|
&.vjs-selected
|
||||||
background-color: $videoplayer-controls-color
|
background-color: $videoplayer-controls-color
|
||||||
color: $videoplayer-background-color
|
color: $videoplayer-background-color
|
||||||
|
|
||||||
&:focus, &:hover
|
&:focus, &:hover
|
||||||
background-color: $videoplayer-controls-color
|
background-color: $videoplayer-controls-color
|
||||||
color: $videoplayer-background-color
|
color: $videoplayer-background-color
|
||||||
|
|
||||||
&.vjs-menu-title
|
&.vjs-menu-title
|
||||||
text-align: center
|
text-align: center
|
||||||
text-transform: uppercase
|
text-transform: uppercase
|
||||||
@@ -486,12 +488,13 @@ body.vjs-full-window
|
|||||||
height: 0em
|
height: 0em
|
||||||
margin-bottom: 1.5em
|
margin-bottom: 1.5em
|
||||||
border-top-color: $videoplayer-background-color
|
border-top-color: $videoplayer-background-color
|
||||||
|
|
||||||
.vjs-menu-content
|
.vjs-menu-content
|
||||||
background-color: $videoplayer-background-color
|
background-color: $videoplayer-background-color
|
||||||
position: absolute
|
position: absolute
|
||||||
width: 100%
|
width: 100%
|
||||||
bottom: 1.5em
|
bottom: 1.5em
|
||||||
max-height: 15em
|
max-height: 25em
|
||||||
|
|
||||||
.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, .vjs-menu-button-popup .vjs-menu.vjs-lock-showing
|
.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, .vjs-menu-button-popup .vjs-menu.vjs-lock-showing
|
||||||
display: block
|
display: block
|
||||||
@@ -655,12 +658,12 @@ body.vjs-full-window
|
|||||||
-moz-transition: all 0.2s
|
-moz-transition: all 0.2s
|
||||||
-o-transition: all 0.2s
|
-o-transition: all 0.2s
|
||||||
transition: all 0.2s
|
transition: all 0.2s
|
||||||
height: 0.3em
|
height: $videoplayer-progress-bar-height
|
||||||
|
|
||||||
.vjs-play-progress
|
.vjs-play-progress
|
||||||
position: absolute
|
position: absolute
|
||||||
display: block
|
display: block
|
||||||
height: 0.3em
|
height: $videoplayer-progress-bar-height
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
width: 0
|
width: 0
|
||||||
@@ -670,7 +673,7 @@ body.vjs-full-window
|
|||||||
.vjs-load-progress
|
.vjs-load-progress
|
||||||
position: absolute
|
position: absolute
|
||||||
display: block
|
display: block
|
||||||
height: 0.3em
|
height: $videoplayer-progress-bar-height
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
width: 0
|
width: 0
|
||||||
@@ -680,7 +683,7 @@ body.vjs-full-window
|
|||||||
div
|
div
|
||||||
position: absolute
|
position: absolute
|
||||||
display: block
|
display: block
|
||||||
height: 0.3em
|
height: $videoplayer-progress-bar-height
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
width: 0
|
width: 0
|
||||||
@@ -692,10 +695,11 @@ body.vjs-full-window
|
|||||||
|
|
||||||
.vjs-play-progress
|
.vjs-play-progress
|
||||||
background-color: $videoplayer-controls-color
|
background-color: $videoplayer-controls-color
|
||||||
|
border-radius: 999em
|
||||||
|
|
||||||
&:before
|
&:before
|
||||||
position: absolute
|
position: absolute
|
||||||
top: -0.333333333333333em
|
top: -($videoplayer-progress-bar-height / 2) // halfway the height of the progress bar
|
||||||
right: -0.5em
|
right: -0.5em
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
@@ -712,8 +716,8 @@ body.vjs-full-window
|
|||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
.vjs-time-tooltip
|
.vjs-time-tooltip
|
||||||
background-color: $videoplayer-background-color
|
|
||||||
color: $videoplayer-controls-color
|
color: $videoplayer-controls-color
|
||||||
|
background-color: $videoplayer-background-color
|
||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
@@ -735,9 +739,9 @@ body.vjs-full-window
|
|||||||
|
|
||||||
.vjs-time-tooltip
|
.vjs-time-tooltip
|
||||||
background-color: $videoplayer-controls-color
|
background-color: $videoplayer-controls-color
|
||||||
border-radius: 3px
|
border-radius: $border-radius
|
||||||
color: $videoplayer-background-color
|
color: $videoplayer-background-color
|
||||||
font-family: $font-body
|
font-family: $font-family-base
|
||||||
font-size: 1.2em
|
font-size: 1.2em
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
padding: 5px 8px
|
padding: 5px 8px
|
||||||
@@ -851,9 +855,9 @@ body.vjs-full-window
|
|||||||
font-size: 0.9em
|
font-size: 0.9em
|
||||||
|
|
||||||
.vjs-slider-horizontal .vjs-volume-level
|
.vjs-slider-horizontal .vjs-volume-level
|
||||||
height: 0.3em
|
height: $videoplayer-progress-bar-height
|
||||||
&:before
|
&:before
|
||||||
top: -0.3em
|
top: -$videoplayer-progress-bar-height
|
||||||
right: -0.5em
|
right: -0.5em
|
||||||
|
|
||||||
.vjs-menu-button-popup
|
.vjs-menu-button-popup
|
||||||
@@ -1022,14 +1026,15 @@ video::-webkit-media-text-track-display
|
|||||||
|
|
||||||
.vjs-playback-rate
|
.vjs-playback-rate
|
||||||
.vjs-playback-rate-value
|
.vjs-playback-rate-value
|
||||||
font-size: 1.5em
|
font-size: 1.25em
|
||||||
line-height: 2
|
line-height: 2
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0
|
top: 3px
|
||||||
left: 0
|
left: 0
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
.vjs-menu
|
.vjs-menu
|
||||||
width: 4em
|
width: 4em
|
||||||
left: 0em
|
left: 0em
|
||||||
@@ -1041,7 +1046,6 @@ video::-webkit-media-text-track-display
|
|||||||
&:before
|
&:before
|
||||||
color: $videoplayer-controls-color
|
color: $videoplayer-controls-color
|
||||||
content: 'X'
|
content: 'X'
|
||||||
font-family: Arial, Helvetica, sans-serif
|
|
||||||
font-size: 4em
|
font-size: 4em
|
||||||
left: 0
|
left: 0
|
||||||
line-height: 1
|
line-height: 1
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
@import _normalize
|
|
||||||
@import _config
|
|
||||||
@import _utils
|
|
||||||
|
|
||||||
@import _comments
|
|
||||||
@import _project
|
|
||||||
@import _project-sharing
|
|
||||||
@import _project-dashboard
|
|
||||||
@import _error
|
|
||||||
|
|
||||||
@import _search
|
|
||||||
|
|
||||||
@import plugins/_jstree
|
|
||||||
@import plugins/_js_select2
|
|
@@ -7,8 +7,7 @@ $color-theatre-background-dark: darken($color-theatre-background, 5%)
|
|||||||
|
|
||||||
$theatre-width: 350px
|
$theatre-width: 350px
|
||||||
|
|
||||||
body.theatre,
|
body.theatre
|
||||||
body.theatre .container-page
|
|
||||||
background-color: $color-theatre-background
|
background-color: $color-theatre-background
|
||||||
nav.navbar
|
nav.navbar
|
||||||
+media-lg
|
+media-lg
|
||||||
@@ -26,6 +25,7 @@ body.theatre .container-page
|
|||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
|
|
||||||
.page-body
|
.page-body
|
||||||
height: 100%
|
height: 100%
|
||||||
width: 100%
|
width: 100%
|
||||||
|
@@ -6,20 +6,22 @@
|
|||||||
| {% if node_type_name == 'group' %}
|
| {% if node_type_name == 'group' %}
|
||||||
| {% set node_type_name = 'folder' %}
|
| {% set node_type_name = 'folder' %}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
li(class="button-{{ node_type['name'] }}")
|
li
|
||||||
a.item_add_node(
|
a.dropdown-item(
|
||||||
href="#",
|
class="item_add_node",
|
||||||
title="{{ node_type['description'] }}",
|
href="#",
|
||||||
data-node-type-name="{{ node_type['name'] }}",
|
title="{{ node_type['description'] }}",
|
||||||
data-toggle="tooltip",
|
data-node-type-name="{{ node_type['name'] }}",
|
||||||
data-placement="left")
|
data-toggle="tooltip",
|
||||||
i.pi(class="icon-{{ node_type['name'] }}")
|
data-placement="left")
|
||||||
|
i.pi(class="pi-{{ node_type['name'] }}")
|
||||||
| {% if node_type_name == 'group_texture' %}
|
| {% if node_type_name == 'group_texture' %}
|
||||||
| Texture Folder
|
| Texture Folder
|
||||||
| {% elif node_type_name == 'group_hdri' %}
|
| {% elif node_type_name == 'group_hdri' %}
|
||||||
| HDRi Folder
|
| HDRi Folder
|
||||||
| {% else %}
|
| {% else %}
|
||||||
| {{ node_type_name }}
|
span.text-capitalize
|
||||||
|
|{{ node_type_name }}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% endfor %}
|
| {% endfor %}
|
||||||
|
52
src/templates/_macros/_asset_list_item.pug
Normal file
52
src/templates/_macros/_asset_list_item.pug
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
| {% macro asset_list_item(asset, current_user) %}
|
||||||
|
|
||||||
|
| {% set node_type = asset.properties.content_type if asset.properties.content_type else asset.node_type %}
|
||||||
|
|
||||||
|
a.card.asset.card-image-fade.pr-0.mx-0.mb-2(
|
||||||
|
class="js-item-open {% if asset.permissions.world %}free{% endif %}",
|
||||||
|
data-node_id="{{ asset._id }}",
|
||||||
|
title="{{ asset.name }}",
|
||||||
|
href='{{ url_for_node(node=asset) }}')
|
||||||
|
.embed-responsive.embed-responsive-16by9
|
||||||
|
| {% if asset.picture %}
|
||||||
|
.card-img-top.embed-responsive-item(style="background-image: url({{ asset.picture.thumbnail('m', api=api) }})")
|
||||||
|
| {% else %}
|
||||||
|
.card-img-top.card-icon.embed-responsive-item
|
||||||
|
i(class="pi-{{ node_type }}")
|
||||||
|
| {% endif %}
|
||||||
|
|
||||||
|
.card-body.py-2.d-flex.flex-column.text-truncate
|
||||||
|
.card-title.mb-1.font-weight-bold.text-truncate
|
||||||
|
| {{ asset.name | hide_none }}
|
||||||
|
|
||||||
|
ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto.mb-0.text-truncate
|
||||||
|
li.pr-2.font-weight-bold {{ node_type | undertitle | hide_none }}
|
||||||
|
li.pr-2.text-truncate {{ asset.project.name | hide_none }}
|
||||||
|
li.pr-2.text-truncate {{ asset.user.full_name | hide_none }}
|
||||||
|
li.text-truncate {{ asset._created | pretty_date | hide_none }}
|
||||||
|
|
||||||
|
| {% if asset.properties.content_type == 'video' %}
|
||||||
|
|
||||||
|
| {% set view_progress = current_user.nodes.view_progress %}
|
||||||
|
| {% if asset._id in view_progress %}
|
||||||
|
| {% set progress = current_user.nodes.view_progress[asset._id] %}
|
||||||
|
| {% set progress_in_percent = progress.progress_in_percent %}
|
||||||
|
| {% set progress_done = progress.done %}
|
||||||
|
| {% endif %}
|
||||||
|
|
||||||
|
| {% if progress %}
|
||||||
|
.progress.rounded-0
|
||||||
|
.progress-bar(
|
||||||
|
role="progressbar",
|
||||||
|
style="width: {{ progress_in_percent }}%;",
|
||||||
|
aria-valuenow="{{ progress_in_percent }}",
|
||||||
|
aria-valuemin="0",
|
||||||
|
aria-valuemax="100")
|
||||||
|
|
||||||
|
| {% if progress.done %}
|
||||||
|
.card-label WATCHED
|
||||||
|
| {% endif %}
|
||||||
|
| {% endif %} {# endif progress #}
|
||||||
|
| {% endif %} {# endif video #}
|
||||||
|
|
||||||
|
| {% endmacro %}
|
@@ -28,15 +28,15 @@
|
|||||||
span Add files...
|
span Add files...
|
||||||
input(type='file', name='file', multiple='')
|
input(type='file', name='file', multiple='')
|
||||||
|
|
||||||
button.btn.btn-primary.start(type='submit')
|
button.btn.btn-outline-primary.start(type='submit')
|
||||||
i.pi-upload
|
i.pi-upload
|
||||||
span Start upload
|
span Start Upload
|
||||||
|
|
||||||
button.btn.btn-warning.cancel(type='reset')
|
button.btn.btn-outline-warning.cancel(type='reset')
|
||||||
i.pi-cancel
|
i.pi-cancel
|
||||||
span Cancel upload
|
span Cancel Upload
|
||||||
|
|
||||||
button.btn.btn-danger.delete(type='button')
|
button.btn.btn-outline-danger.delete(type='button')
|
||||||
i.pi-trash
|
i.pi-trash
|
||||||
span Delete
|
span Delete
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ script#template-upload(type="text/x-tmpl").
|
|||||||
</button>
|
</button>
|
||||||
{% } %}
|
{% } %}
|
||||||
{% if (!i) { %}
|
{% if (!i) { %}
|
||||||
<button class="btn btn-warning cancel">
|
<button class="btn btn-outline-secondary cancel">
|
||||||
<i class="ion-close-round"></i>
|
<i class="ion-close-round"></i>
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +61,7 @@ script#template-download(type="text/x-tmpl").
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if (file.deleteUrl) { %}
|
{% if (file.deleteUrl) { %}
|
||||||
<button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
|
<button class="btn btn-outline-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
|
||||||
<i class="ion-trash-b"></i>
|
<i class="ion-trash-b"></i>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -71,7 +71,7 @@ script#template-download(type="text/x-tmpl").
|
|||||||
Create
|
Create
|
||||||
</div>
|
</div>
|
||||||
{% } else { %}
|
{% } else { %}
|
||||||
<button class="btn btn-warning cancel">
|
<button class="btn btn-outline-secondary cancel">
|
||||||
<i class="ion-close-round"></i>
|
<i class="ion-close-round"></i>
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
| {% macro navigation_tabs(title) %}
|
|
||||||
|
|
||||||
nav#nav-tabs
|
|
||||||
ul#nav-tabs__list
|
|
||||||
li.nav-tabs__list-tab(
|
|
||||||
class="{% if title == 'homepage' %}active{% endif %}")
|
|
||||||
a(href="{{ url_for('main.homepage') }}") Activity
|
|
||||||
|
|
||||||
li.nav-tabs__list-tab(
|
|
||||||
class="{% if title == 'home' %}active{% endif %}")
|
|
||||||
a(href="{{ url_for('projects.home_project') }}") Home
|
|
||||||
|
|
||||||
li.nav-tabs__list-tab(
|
|
||||||
class="{% if title == 'dashboard' %}active{% endif %}")
|
|
||||||
a(href="{{ url_for('projects.index') }}") My Projects
|
|
||||||
|
|
||||||
| {% if current_user.has_organizations() %}
|
|
||||||
li.nav-tabs__list-tab(
|
|
||||||
class="{% if title == 'organizations' %}active{% endif %}")
|
|
||||||
a(
|
|
||||||
href="{{ url_for('pillar.web.organizations.index') }}",
|
|
||||||
title="My Organizations")
|
|
||||||
| My Organizations
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
| {% endmacro %}
|
|
@@ -9,5 +9,5 @@
|
|||||||
.modal-body
|
.modal-body
|
||||||
| ...
|
| ...
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-default(type='button', data-dismiss='modal') Close
|
button.btn.btn-outline-secondary(type='button', data-dismiss='modal') Close
|
||||||
button.btn.btn-primary(type='button') Save changes
|
button.btn.btn-primary(type='button') Save changes
|
||||||
|
@@ -29,32 +29,20 @@ html(lang="en")
|
|||||||
meta(name="twitter:image", content="")
|
meta(name="twitter:image", content="")
|
||||||
| {% endblock %}
|
| {% endblock %}
|
||||||
|
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery-3.1.0.min.js')}}")
|
script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js') }}")
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js')}}")
|
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js')}}")
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/js.cookie-2.0.3.min.js')}}")
|
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/js.cookie-2.0.3.min.js')}}")
|
||||||
| {% if current_user.is_authenticated %}
|
| {% if current_user.is_authenticated %}
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
|
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
script.
|
|
||||||
|
|
||||||
!function(e){"use strict";e.loadCSS=function(t,n,o){var r,i=e.document,l=i.createElement("link");if(n)r=n;else{var d=(i.body||i.getElementsByTagName("head")[0]).childNodes;r=d[d.length-1]}var a=i.styleSheets;l.rel="stylesheet",l.href=t,l.media="only x",r.parentNode.insertBefore(l,n?r:r.nextSibling);var f=function(e){for(var t=l.href,n=a.length;n--;)if(a[n].href===t)return e();setTimeout(function(){f(e)})};return l.onloadcssdefined=f,f(function(){l.media=o||"all"}),l},"undefined"!=typeof module&&(module.exports=e.loadCSS)}(this);
|
|
||||||
|
|
||||||
loadCSS( "//fonts.googleapis.com/css?family=Roboto:300,400" );
|
|
||||||
|
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/markdown.min.js') }}")
|
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js') }}")
|
|
||||||
|
|
||||||
link(href="{{ url_for('static', filename='assets/img/favicon.png') }}", rel="shortcut icon")
|
link(href="{{ url_for('static', filename='assets/img/favicon.png') }}", rel="shortcut icon")
|
||||||
link(href="{{ url_for('static', filename='assets/img/apple-touch-icon-precomposed.png') }}", rel="icon apple-touch-icon-precomposed", sizes="192x192")
|
link(href="{{ url_for('static', filename='assets/img/apple-touch-icon-precomposed.png') }}", rel="icon apple-touch-icon-precomposed", sizes="192x192")
|
||||||
|
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/css/vendor/bootstrap.min.css') }}", rel="stylesheet")
|
|
||||||
|
|
||||||
| {% block head %}{% endblock %}
|
| {% block head %}{% endblock %}
|
||||||
|
|
||||||
| {% block css %}
|
| {% block css %}
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
|
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/css/base.css') }}", rel="stylesheet")
|
|
||||||
| {% if title == 'blog' %}
|
| {% if title == 'blog' %}
|
||||||
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
|
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
|
||||||
| {% else %}
|
| {% else %}
|
||||||
@@ -66,10 +54,9 @@ html(lang="en")
|
|||||||
| {% if not title %}{% set title="default" %}{% endif %}
|
| {% if not title %}{% set title="default" %}{% endif %}
|
||||||
|
|
||||||
body(class="{{ title }}")
|
body(class="{{ title }}")
|
||||||
.container-page
|
.page-content
|
||||||
.page-content
|
.page-body
|
||||||
.page-body
|
| {% block body %}{% endblock %}
|
||||||
| {% block body %}{% endblock %}
|
|
||||||
|
|
||||||
| {% block footer_container %}
|
| {% block footer_container %}
|
||||||
#footer-container
|
#footer-container
|
||||||
@@ -84,8 +71,6 @@ html(lang="en")
|
|||||||
| {% endblock footer %}
|
| {% endblock footer %}
|
||||||
| {% endblock footer_container%}
|
| {% endblock footer_container%}
|
||||||
|
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.bootstrap-3.3.7.min.js') }}")
|
|
||||||
|
|
||||||
| {% if current_user.is_authenticated %}
|
| {% if current_user.is_authenticated %}
|
||||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typewatch-3.0.0.min.js') }}")
|
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typewatch-3.0.0.min.js') }}")
|
||||||
script.
|
script.
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
| {% if current_user.is_authenticated %}
|
| {% if current_user.is_authenticated %}
|
||||||
|
|
||||||
li.nav-notifications
|
li.nav-notifications.nav-item
|
||||||
a.navbar-item#notifications-toggle(
|
a.nav-link.px-2(
|
||||||
title="Notifications",
|
id="notifications-toggle",
|
||||||
data-toggle="tooltip",
|
title="Notifications",
|
||||||
data-placement="bottom")
|
data-toggle="tooltip",
|
||||||
|
data-placement="bottom")
|
||||||
i.pi-notifications-none.nav-notifications-icon
|
i.pi-notifications-none.nav-notifications-icon
|
||||||
span#notifications-count
|
span#notifications-count
|
||||||
span
|
span
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
| {% block menu_body %}
|
| {% block menu_body %}
|
||||||
| {% if current_user.is_authenticated %}
|
| {% if current_user.is_authenticated %}
|
||||||
|
|
||||||
li(class="dropdown")
|
li.dropdown
|
||||||
| {% block menu_avatar %}
|
| {% block menu_avatar %}
|
||||||
a.navbar-item.dropdown-toggle(href="#", data-toggle="dropdown", title="{{ current_user.email }}")
|
a.navbar-item.dropdown-toggle(href="#", data-toggle="dropdown", title="{{ current_user.email }}")
|
||||||
img.gravatar(
|
img.gravatar(
|
||||||
@@ -9,58 +9,36 @@ li(class="dropdown")
|
|||||||
alt="Avatar")
|
alt="Avatar")
|
||||||
| {% endblock menu_avatar %}
|
| {% endblock menu_avatar %}
|
||||||
|
|
||||||
ul.dropdown-menu
|
ul.dropdown-menu.dropdown-menu-right
|
||||||
| {% if not current_user.has_role('protected') %}
|
| {% if not current_user.has_role('protected') %}
|
||||||
| {% block menu_list %}
|
| {% block menu_list %}
|
||||||
li
|
|
||||||
a.navbar-item(
|
|
||||||
href="{{ url_for('projects.home_project') }}"
|
|
||||||
title="Home")
|
|
||||||
i.pi-home
|
|
||||||
| Home
|
|
||||||
|
|
||||||
li
|
li
|
||||||
a.navbar-item(
|
a.navbar-item.px-2(
|
||||||
href="{{ url_for('projects.index') }}"
|
href="{{ url_for('settings.profile') }}"
|
||||||
title="My Projects")
|
title="Settings")
|
||||||
i.pi-star
|
| #[i.pi-cog] Settings
|
||||||
| My Projects
|
|
||||||
|
|
||||||
| {% if current_user.has_organizations() %}
|
|
||||||
li
|
|
||||||
a.navbar-item(
|
|
||||||
href="{{ url_for('pillar.web.organizations.index') }}"
|
|
||||||
title="My Organizations")
|
|
||||||
i.pi-users
|
|
||||||
| My Organizations
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
li
|
|
||||||
a.navbar-item(
|
|
||||||
href="{{ url_for('settings.profile') }}"
|
|
||||||
title="Settings")
|
|
||||||
i.pi-cog
|
|
||||||
| Settings
|
|
||||||
|
|
||||||
| {% endblock menu_list %}
|
| {% endblock menu_list %}
|
||||||
|
|
||||||
li.divider(role="separator")
|
li.dropdown-divider(role="separator")
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
li
|
li
|
||||||
a.navbar-item(
|
a.navbar-item(
|
||||||
href="{{ url_for('users.logout') }}")
|
href="{{ url_for('users.logout') }}")
|
||||||
i.pi-log-out(title="Log Out")
|
i.pi-log-out(title="Log Out")
|
||||||
| Log out
|
| Log out
|
||||||
a.navbar-item.subitem(
|
a.navbar-item.subitem.pt-0(
|
||||||
href="{{ url_for('users.switch') }}")
|
href="{{ url_for('users.switch') }}")
|
||||||
i.pi-blank
|
i.pi-blank
|
||||||
| Not {{ current_user.full_name }}?
|
| Not {{ current_user.full_name }}?
|
||||||
|
|
||||||
| {% else %}
|
| {% else %}
|
||||||
|
|
||||||
li.nav-item-sign-in
|
li.pr-1
|
||||||
a.navbar-item(href="{{ url_for('users.login') }}")
|
a.btn.btn-sm.btn-outline-primary.px-3(
|
||||||
| Log in
|
href="{{ url_for('users.login') }}")
|
||||||
|
| Log In
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
| {% endblock menu_body %}
|
| {% endblock menu_body %}
|
||||||
|
73
src/templates/mixins/components.pug
Normal file
73
src/templates/mixins/components.pug
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// {#
|
||||||
|
// Header of landing pages. title or text can be skipped:
|
||||||
|
// +jumbotron("{{ page_title }}", null, "{{ page_header_image }}")
|
||||||
|
// Any extra attributes added (in a separate group) will be passed as is:
|
||||||
|
// +jumbotron("{{ page_title }}", null, "{{ page_header_image }}")(data-node-id='{{ node._id }}')
|
||||||
|
// #}
|
||||||
|
mixin jumbotron(title, text, image, url)
|
||||||
|
if url
|
||||||
|
a.jumbotron.text-white(
|
||||||
|
style='background-image: url(' + image + ');',
|
||||||
|
href=url)&attributes(attributes)
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-md-9
|
||||||
|
if title
|
||||||
|
.display-4.text-uppercase.font-weight-bold
|
||||||
|
=title
|
||||||
|
if text
|
||||||
|
.lead
|
||||||
|
=text
|
||||||
|
else
|
||||||
|
.jumbotron.text-white(style='background-image: url(' + image + ');')&attributes(attributes)
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-md-9
|
||||||
|
if title
|
||||||
|
.display-4.text-uppercase.font-weight-bold
|
||||||
|
=title
|
||||||
|
if text
|
||||||
|
.lead
|
||||||
|
=text
|
||||||
|
|
||||||
|
// {# Secondary navigation.
|
||||||
|
// e.g. Workshops, Courses. #}
|
||||||
|
mixin nav-secondary(title)
|
||||||
|
ul.nav.nav-secondary&attributes(attributes)
|
||||||
|
if title
|
||||||
|
li.nav-item
|
||||||
|
span.nav-title.nav-link.font-weight-bold.pointer-events-none= title
|
||||||
|
|
||||||
|
if block
|
||||||
|
block
|
||||||
|
else
|
||||||
|
p No items defined.
|
||||||
|
|
||||||
|
mixin nav-secondary-link()
|
||||||
|
li.nav-item
|
||||||
|
a.nav-link&attributes(attributes)
|
||||||
|
block
|
||||||
|
|
||||||
|
mixin card-deck(max_columns)
|
||||||
|
.card-deck.card-padless.card-deck-responsive(class="card-" + max_columns + "-columns")&attributes(attributes)
|
||||||
|
if block
|
||||||
|
block
|
||||||
|
else
|
||||||
|
.p-3 No items.
|
||||||
|
|
||||||
|
// {#
|
||||||
|
// Passes all attributes to the card.
|
||||||
|
// You can do fun stuff in a loop even like:
|
||||||
|
// +card(data-url="{{ url_for('projects.view', project_url=project.url) }}", tabindex='{{ loop.index }}')
|
||||||
|
// #}
|
||||||
|
mixin card()
|
||||||
|
.card&attributes(attributes)
|
||||||
|
if block
|
||||||
|
block
|
||||||
|
else
|
||||||
|
p No card content defined.
|
||||||
|
|
||||||
|
mixin list-asset(name, url, image, type, date)
|
||||||
|
if block
|
||||||
|
block
|
||||||
|
|
@@ -5,21 +5,21 @@ section.node-preview-forbidden
|
|||||||
|
|
||||||
div
|
div
|
||||||
p Available to Blender Cloud subscribers
|
p Available to Blender Cloud subscribers
|
||||||
hr
|
hr.bg-white
|
||||||
| {% if current_user.has_cap('can-renew-subscription') %}
|
| {% if current_user.has_cap('can-renew-subscription') %}
|
||||||
p
|
p
|
||||||
small You have a subscription, it just needs to be renewed.
|
small You have a subscription, it just needs to be renewed.
|
||||||
a.btn(href="/renew")
|
a.btn.btn-light(href="/renew")
|
||||||
| #[i.pi-heart] Renew Subscription
|
| #[i.pi-heart] Renew Subscription
|
||||||
| {% else %}
|
| {% else %}
|
||||||
p
|
p
|
||||||
small Support Blender and get awesome stuff!
|
small Support Blender and get awesome stuff!
|
||||||
a.btn(href="{{ url_for('cloud.join') }}")
|
a.btn.btn-light(href="{{ url_for('cloud.join') }}")
|
||||||
| #[i.pi-heart] Get a Subscription
|
| #[i.pi-heart] Get a Subscription
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
|
||||||
| {% if current_user.is_anonymous %}
|
| {% if current_user.is_anonymous %}
|
||||||
p(style="margin-top: 15px")
|
p(style="margin-top: 15px")
|
||||||
small
|
small
|
||||||
a(href="{{ url_for('users.login') }}") Already a subscriber? Log in
|
a.text-white(href="{{ url_for('users.login') }}") Already a subscriber? Log in
|
||||||
| {% endif %}
|
| {% endif %}
|
||||||
|
@@ -33,8 +33,8 @@ script(type="text/javascript").
|
|||||||
} else if (node_type === 'group_hdri') {
|
} else if (node_type === 'group_hdri') {
|
||||||
node_type_str = 'HDRi Folder';
|
node_type_str = 'HDRi Folder';
|
||||||
}
|
}
|
||||||
$('a', '.button-edit').html('<i class="pi-edit button-edit-icon"></i> Edit ' + node_type_str);
|
|
||||||
$('a', '.button-delete').html('<i class="pi-trash button-delete-icon"></i>Delete ' + node_type_str);
|
$('a', '.button-delete').html('<i class="pr-2 pi-trash button-delete-icon"></i>Delete ' + node_type_str);
|
||||||
|
|
||||||
{% if parent %}
|
{% if parent %}
|
||||||
ProjectUtils.setProjectAttributes({parentNodeId: '{{parent._id}}'});
|
ProjectUtils.setProjectAttributes({parentNodeId: '{{parent._id}}'});
|
||||||
@@ -61,7 +61,7 @@ script(type="text/javascript").
|
|||||||
}
|
}
|
||||||
|
|
||||||
{% if node.has_method('PUT') %}
|
{% if node.has_method('PUT') %}
|
||||||
$('.project-mode-view').show();
|
$('.project-mode-view').displayAs('inline-block');
|
||||||
{% else %}
|
{% else %}
|
||||||
$('.project-mode-view').hide();
|
$('.project-mode-view').hide();
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -114,7 +114,6 @@ script(type="text/javascript").
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$(page_overlay).find('.nav-prev').click(function(e){
|
$(page_overlay).find('.nav-prev').click(function(e){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -133,10 +132,6 @@ script(type="text/javascript").
|
|||||||
$(this).removeClass('active').hide().html();
|
$(this).removeClass('active').hide().html();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof $().popover != 'undefined'){
|
|
||||||
$('#asset-license').popover();
|
|
||||||
}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
var $content_type = $(".js-type");
|
var $content_type = $(".js-type");
|
||||||
|
@@ -23,7 +23,7 @@ section.node-preview.video
|
|||||||
|
|
||||||
| {% block node_download %}
|
| {% block node_download %}
|
||||||
| {% if node.file_variations %}
|
| {% if node.file_variations %}
|
||||||
button.btn.btn-default.dropdown-toggle(
|
button.btn.btn-outline-primary.dropdown-toggle.px-3(
|
||||||
type="button",
|
type="button",
|
||||||
data-toggle="dropdown",
|
data-toggle="dropdown",
|
||||||
aria-haspopup="true",
|
aria-haspopup="true",
|
||||||
@@ -32,7 +32,7 @@ button.btn.btn-default.dropdown-toggle(
|
|||||||
| Download
|
| Download
|
||||||
i.pi-angle-down.icon-dropdown-menu
|
i.pi-angle-down.icon-dropdown-menu
|
||||||
|
|
||||||
ul.dropdown-menu
|
ul.dropdown-menu.dropdown-menu-right
|
||||||
| {% for variation in node.file_variations %}
|
| {% for variation in node.file_variations %}
|
||||||
li
|
li
|
||||||
a(href="{{ variation.link }}",
|
a(href="{{ variation.link }}",
|
||||||
@@ -52,25 +52,49 @@ script(type="text/javascript").
|
|||||||
{% if node.video_sources %}
|
{% if node.video_sources %}
|
||||||
|
|
||||||
var videoPlayer = document.getElementById('videoplayer');
|
var videoPlayer = document.getElementById('videoplayer');
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
controlBar: {
|
controlBar: {
|
||||||
volumePanel: { inline: false }
|
volumePanel: { inline: false }
|
||||||
}
|
},
|
||||||
|
playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4]
|
||||||
};
|
};
|
||||||
|
|
||||||
videojs.registerPlugin('analytics', function() {
|
videojs(videoPlayer, options).ready(function() {
|
||||||
this.ga({
|
this.ga({
|
||||||
'eventLabel' : '{{ node._id }} - {{ node.name }}',
|
'eventLabel' : '{{ node._id }} - {{ node.name }}',
|
||||||
'eventCategory' : '{{ node.project }}',
|
'eventCategory' : '{{ node.project }}',
|
||||||
'eventsToTrack' : ['start', 'error', 'percentsPlayed']
|
'eventsToTrack' : ['start', 'error', 'percentsPlayed']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.hotkeys({
|
||||||
|
enableVolumeScroll: false,
|
||||||
|
customKeys: {
|
||||||
|
KeyL: {
|
||||||
|
key: function(event) {
|
||||||
|
return (event.which === 76);
|
||||||
|
},
|
||||||
|
handler: function(player, options, event) {
|
||||||
|
videoPlayerToggleLoop(videoPlayer, videoPlayerLoopButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rememberVolumePlugin();
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
let fetch_progress_url = '{{ url_for("users_api.get_video_progress", video_id=node._id) }}';
|
||||||
|
let report_url = '{{ url_for("users_api.set_video_progress", video_id=node._id) }}';
|
||||||
|
|
||||||
|
this.progressPlugin({
|
||||||
|
'report_url': report_url,
|
||||||
|
'fetch_progress_url': fetch_progress_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
videojs(videoPlayer, options).ready(function() {
|
// Generic utility to add-buttons to the player.
|
||||||
this.hotkeys();
|
|
||||||
});
|
|
||||||
|
|
||||||
function addVideoPlayerButton(data) {
|
function addVideoPlayerButton(data) {
|
||||||
|
|
||||||
var controlBar,
|
var controlBar,
|
||||||
@@ -89,6 +113,7 @@ script(type="text/javascript").
|
|||||||
return newButton;
|
return newButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video loop stuff. TODO: Move it to video_plugins.js
|
||||||
var videoPlayerLoopButton = addVideoPlayerButton({
|
var videoPlayerLoopButton = addVideoPlayerButton({
|
||||||
player: videoPlayer,
|
player: videoPlayer,
|
||||||
class: 'vjs-loop-button',
|
class: 'vjs-loop-button',
|
||||||
@@ -96,15 +121,18 @@ script(type="text/javascript").
|
|||||||
title: 'Loop'
|
title: 'Loop'
|
||||||
});
|
});
|
||||||
|
|
||||||
videoPlayerLoopButton.onclick = function() {
|
function videoPlayerToggleLoop(videoPlayer, videoPlayerLoopButton) {
|
||||||
if (videoPlayer.loop){
|
if (videoPlayer.loop){
|
||||||
videoPlayer.loop = false;
|
videoPlayer.loop = false;
|
||||||
$(this).removeClass('vjs-control-active');
|
$(videoPlayerLoopButton).removeClass('vjs-control-active');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
videoPlayer.loop = true;
|
videoPlayer.loop = true;
|
||||||
$(this).addClass('vjs-control-active');
|
$(videoPlayerLoopButton).addClass('vjs-control-active');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPlayerLoopButton.onclick = function() {
|
||||||
|
videoPlayerToggleLoop(videoPlayer, videoPlayerLoopButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
{% endif %} // if node.video_sources
|
{% endif %} // if node.video_sources
|
||||||
|
@@ -1,142 +0,0 @@
|
|||||||
//- ******************************************************* -//
|
|
||||||
| {% import 'projects/_macros.html' as projectmacros %}
|
|
||||||
| {% macro render_blog_post(node, project=None, pages=None) %}
|
|
||||||
| {% if node.picture %}
|
|
||||||
a.blog_index-header(href="{{ node.url }}")
|
|
||||||
img(src="{{ node.picture.thumbnail('h', api=api) }}")
|
|
||||||
| {% endif %}
|
|
||||||
| {% if project and project._id != config.MAIN_PROJECT_ID %}
|
|
||||||
| {{ projectmacros.render_secondary_navigation(project, pages=pages) }}
|
|
||||||
| {% endif %}
|
|
||||||
.blog_index-item
|
|
||||||
a.item-title(
|
|
||||||
href="{{ node.url }}")
|
|
||||||
| {{ node.name }}
|
|
||||||
ul.meta
|
|
||||||
| {% if node.project.name %}
|
|
||||||
li {{ node.project.name }}
|
|
||||||
| {% endif %}
|
|
||||||
| {% if node.user.full_name%}
|
|
||||||
li.who
|
|
||||||
| by {{ node.user.full_name }}
|
|
||||||
| {% endif %}
|
|
||||||
li.when
|
|
||||||
a(href="{{ node.url }}",
|
|
||||||
title="Updated {{ node._updated | pretty_date }}")
|
|
||||||
| {{ node._created | pretty_date }}
|
|
||||||
li
|
|
||||||
a(href="{{ node.url }}#comments")
|
|
||||||
| comment
|
|
||||||
|
|
||||||
|
|
||||||
.item-content
|
|
||||||
| {{ node.properties | markdowned('content') }}
|
|
||||||
|
|
||||||
| {% endmacro %}
|
|
||||||
|
|
||||||
//- ******************************************************* -//
|
|
||||||
| {% macro render_blog_list_item(node) %}
|
|
||||||
.blog_index-item.list
|
|
||||||
|
|
||||||
| {% if node.picture %}
|
|
||||||
a.item-header(href="{{ node.url }}")
|
|
||||||
img.image(src="{{ node.picture.thumbnail('s', api=api) }}")
|
|
||||||
| {% else %}
|
|
||||||
a.item-header.nothumb(href="{{ node.url }}")
|
|
||||||
i.pi-document-text
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
a.item-title(
|
|
||||||
href="{{ node.url }}")
|
|
||||||
| {{node.name}}
|
|
||||||
|
|
||||||
.item-info.
|
|
||||||
#[span(title="{{node._created}}") {{node._created | pretty_date }}]
|
|
||||||
{% if node._created != node._updated %}
|
|
||||||
#[span(title="{{node._updated}}") (updated {{node._updated | pretty_date }})]
|
|
||||||
{% endif %}
|
|
||||||
{% if node.properties.category %} · {{node.properties.category}}{% endif %}
|
|
||||||
· {{node.user.full_name}}
|
|
||||||
{% if node.properties.status != 'published' %} · {{ node.properties.status}} {% endif %}
|
|
||||||
|
|
||||||
| {% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
//- ******************************************************* -//
|
|
||||||
| {% macro render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=None) %}
|
|
||||||
| {% if can_create_blog_posts %}
|
|
||||||
.blog-action
|
|
||||||
a.btn.btn-default.button-create(href="{{url_for('nodes.posts_create', project_id=project._id)}}")
|
|
||||||
i.pi-plus
|
|
||||||
| Create New Post
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
| {% if posts %}
|
|
||||||
| {{ render_blog_post(posts[0], project=project, pages=pages) }}
|
|
||||||
|
|
||||||
| {% for node in posts[1:] %}
|
|
||||||
| {% if loop.first %}
|
|
||||||
.blog-archive-navigation
|
|
||||||
span Blasts from the past
|
|
||||||
| {% endif %}
|
|
||||||
| {{ render_blog_list_item(node) }}
|
|
||||||
| {% endfor %}
|
|
||||||
|
|
||||||
| {% if more_posts_available %}
|
|
||||||
.blog-archive-navigation
|
|
||||||
a(href="{{ project.blog_archive_url }}")
|
|
||||||
| {{posts_meta.total - posts|length}} more blog posts over here
|
|
||||||
i.pi-angle-right
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
| {% else %}
|
|
||||||
|
|
||||||
.blog_index-item
|
|
||||||
.item-content No posts... yet!
|
|
||||||
|
|
||||||
| {% endif %} {# posts #}
|
|
||||||
| {% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
//- Macro for rendering the navigation buttons for prev/next pages -//
|
|
||||||
| {% macro render_archive_pagination(project) %}
|
|
||||||
.blog-archive-navigation
|
|
||||||
| {% if project.blog_archive_prev %}
|
|
||||||
a.archive-nav-button(
|
|
||||||
href="{{ project.blog_archive_prev }}", rel="prev")
|
|
||||||
i.pi-angle-left
|
|
||||||
| Previous page
|
|
||||||
| {% else %}
|
|
||||||
span.archive-nav-button
|
|
||||||
i.pi-angle-left
|
|
||||||
| Previous page
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
a.archive-nav-button(
|
|
||||||
href="{{ url_for('main.project_blog', project_url=project.url) }}")
|
|
||||||
| Blog Index
|
|
||||||
|
|
||||||
| {% if project.blog_archive_next %}
|
|
||||||
a.archive-nav-button(
|
|
||||||
href="{{ project.blog_archive_next }}", rel="next")
|
|
||||||
| Next page
|
|
||||||
i.pi-angle-right
|
|
||||||
| {% else %}
|
|
||||||
span.archive-nav-button
|
|
||||||
| Next page
|
|
||||||
i.pi-angle-right
|
|
||||||
| {% endif %}
|
|
||||||
|
|
||||||
| {% endmacro %}
|
|
||||||
|
|
||||||
| {% macro render_archive(project, posts, posts_meta) %}
|
|
||||||
|
|
||||||
| {{ render_archive_pagination(project) }}
|
|
||||||
|
|
||||||
| {% for node in posts %}
|
|
||||||
| {{ render_blog_list_item(node) }}
|
|
||||||
| {% endfor %}
|
|
||||||
|
|
||||||
| {{ render_archive_pagination(project) }}
|
|
||||||
|
|
||||||
| {% endmacro %}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user