Compare commits
	
		
			2 Commits
		
	
	
		
			tmp-video-
			...
			wip-fronte
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ec8f5032e9 | |||
| 7a5af9282c | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,7 +12,6 @@ config_local.py
 | 
			
		||||
 | 
			
		||||
/build
 | 
			
		||||
/.cache
 | 
			
		||||
/.pytest_cache/
 | 
			
		||||
/*.egg-info/
 | 
			
		||||
profile.stats
 | 
			
		||||
/dump/
 | 
			
		||||
@@ -27,7 +26,6 @@ profile.stats
 | 
			
		||||
 | 
			
		||||
pillar/web/static/assets/css/*.css
 | 
			
		||||
pillar/web/static/assets/js/*.min.js
 | 
			
		||||
pillar/web/static/assets/js/vendor/video.min.js
 | 
			
		||||
pillar/web/static/storage/
 | 
			
		||||
pillar/web/static/uploads/
 | 
			
		||||
pillar/web/templates/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								gulpfile.js
									
									
									
									
									
								
							@@ -12,16 +12,15 @@ var pug          = require('gulp-pug');
 | 
			
		||||
var rename       = require('gulp-rename');
 | 
			
		||||
var sass         = require('gulp-sass');
 | 
			
		||||
var sourcemaps   = require('gulp-sourcemaps');
 | 
			
		||||
var uglify       = require('gulp-uglify-es').default;
 | 
			
		||||
var uglify       = require('gulp-uglify');
 | 
			
		||||
 | 
			
		||||
var enabled = {
 | 
			
		||||
    uglify: argv.production,
 | 
			
		||||
    maps: !argv.production,
 | 
			
		||||
    maps: argv.production,
 | 
			
		||||
    failCheck: !argv.production,
 | 
			
		||||
    prettyPug: !argv.production,
 | 
			
		||||
    cachify: !argv.production,
 | 
			
		||||
    cleanup: argv.production,
 | 
			
		||||
    chmod: argv.production,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
var destination = {
 | 
			
		||||
@@ -30,11 +29,6 @@ var destination = {
 | 
			
		||||
    js: 'pillar/web/static/assets/js',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var source = {
 | 
			
		||||
    bootstrap: 'node_modules/bootstrap/',
 | 
			
		||||
    jquery: 'node_modules/jquery/',
 | 
			
		||||
    popper: 'node_modules/popper.js/'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* CSS */
 | 
			
		||||
gulp.task('styles', function() {
 | 
			
		||||
@@ -73,50 +67,36 @@ gulp.task('scripts', function() {
 | 
			
		||||
        .pipe(gulpif(enabled.uglify, uglify()))
 | 
			
		||||
        .pipe(rename({suffix: '.min'}))
 | 
			
		||||
        .pipe(gulpif(enabled.maps, sourcemaps.write(".")))
 | 
			
		||||
        .pipe(gulpif(enabled.chmod, chmod(644)))
 | 
			
		||||
        .pipe(chmod(644))
 | 
			
		||||
        .pipe(gulp.dest(destination.js))
 | 
			
		||||
        .pipe(gulpif(argv.livereload, livereload()));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* 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.
 | 
			
		||||
 * It also includes jQuery and Bootstrap (and its dependency popper), since
 | 
			
		||||
 * the site doesn't work without it anyway.*/
 | 
			
		||||
/* 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 */
 | 
			
		||||
gulp.task('scripts_concat_tutti', function() {
 | 
			
		||||
 | 
			
		||||
    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/tooltip.js',
 | 
			
		||||
        source.bootstrap + 'js/dist/dropdown.js',
 | 
			
		||||
        'src/scripts/tutti/**/*.js'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    gulp.src(toUglify)
 | 
			
		||||
    gulp.src('src/scripts/tutti/**/*.js')
 | 
			
		||||
        .pipe(gulpif(enabled.failCheck, plumber()))
 | 
			
		||||
        .pipe(gulpif(enabled.maps, sourcemaps.init()))
 | 
			
		||||
        .pipe(concat("tutti.min.js"))
 | 
			
		||||
        .pipe(gulpif(enabled.uglify, uglify()))
 | 
			
		||||
        .pipe(gulpif(enabled.maps, sourcemaps.write(".")))
 | 
			
		||||
        .pipe(gulpif(enabled.chmod, chmod(644)))
 | 
			
		||||
        .pipe(chmod(644))
 | 
			
		||||
        .pipe(gulp.dest(destination.js))
 | 
			
		||||
        .pipe(gulpif(argv.livereload, livereload()));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Simply move these vendor scripts from node_modules. */
 | 
			
		||||
gulp.task('scripts_move_vendor', function(done) {
 | 
			
		||||
 | 
			
		||||
    let toMove = [
 | 
			
		||||
    'node_modules/video.js/dist/video.min.js',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    gulp.src(toMove)
 | 
			
		||||
    .pipe(gulp.dest(destination.js + '/vendor/'));
 | 
			
		||||
    done();
 | 
			
		||||
gulp.task('scripts_concat_markdown', function() {
 | 
			
		||||
    gulp.src('src/scripts/markdown/**/*.js')
 | 
			
		||||
        .pipe(gulpif(enabled.failCheck, plumber()))
 | 
			
		||||
        .pipe(gulpif(enabled.maps, sourcemaps.init()))
 | 
			
		||||
        .pipe(concat("markdown.min.js"))
 | 
			
		||||
        .pipe(gulpif(enabled.uglify, uglify()))
 | 
			
		||||
        .pipe(gulpif(enabled.maps, sourcemaps.write(".")))
 | 
			
		||||
        .pipe(chmod(644))
 | 
			
		||||
        .pipe(gulp.dest(destination.js))
 | 
			
		||||
        .pipe(gulpif(argv.livereload, livereload()));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -131,9 +111,9 @@ gulp.task('watch',function() {
 | 
			
		||||
    gulp.watch('src/templates/**/*.pug',['templates']);
 | 
			
		||||
    gulp.watch('src/scripts/*.js',['scripts']);
 | 
			
		||||
    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.
 | 
			
		||||
gulp.task('cleanup', function() {
 | 
			
		||||
    var paths = [];
 | 
			
		||||
@@ -156,5 +136,5 @@ gulp.task('default', tasks.concat([
 | 
			
		||||
    'templates',
 | 
			
		||||
    'scripts',
 | 
			
		||||
    'scripts_concat_tutti',
 | 
			
		||||
    'scripts_move_vendor',
 | 
			
		||||
    'scripts_concat_markdown',
 | 
			
		||||
]));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1879
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1879
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										36
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								package.json
									
									
									
									
									
								
							@@ -4,29 +4,23 @@
 | 
			
		||||
	"author": "Blender Institute",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
    "url": "git://git.blender.org/pillar.git"
 | 
			
		||||
		"url": "https://github.com/armadillica/pillar.git"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
    "gulp": "^3.9.1",
 | 
			
		||||
    "gulp-autoprefixer": "^6.0.0",
 | 
			
		||||
    "gulp-cached": "^1.1.1",
 | 
			
		||||
    "gulp-chmod": "^2.0.0",
 | 
			
		||||
    "gulp-concat": "^2.6.1",
 | 
			
		||||
    "gulp-if": "^2.0.2",
 | 
			
		||||
    "gulp-git": "^2.8.0",
 | 
			
		||||
    "gulp-livereload": "^4.0.0",
 | 
			
		||||
    "gulp-plumber": "^1.2.0",
 | 
			
		||||
    "gulp-pug": "^4.0.1",
 | 
			
		||||
    "gulp-rename": "^1.4.0",
 | 
			
		||||
    "gulp-sass": "^4.0.1",
 | 
			
		||||
    "gulp-sourcemaps": "^2.6.4",
 | 
			
		||||
    "gulp-uglify-es": "^1.0.4",
 | 
			
		||||
		"gulp": "~3.9.1",
 | 
			
		||||
		"gulp-autoprefixer": "~2.3.1",
 | 
			
		||||
		"gulp-cached": "~1.1.0",
 | 
			
		||||
		"gulp-chmod": "~1.3.0",
 | 
			
		||||
		"gulp-concat": "~2.6.0",
 | 
			
		||||
		"gulp-if": "^2.0.1",
 | 
			
		||||
		"gulp-git": "~2.4.2",
 | 
			
		||||
		"gulp-livereload": "~3.8.1",
 | 
			
		||||
		"gulp-plumber": "~1.1.0",
 | 
			
		||||
		"gulp-pug": "~3.2.0",
 | 
			
		||||
		"gulp-rename": "~1.2.2",
 | 
			
		||||
		"gulp-sass": "~2.3.1",
 | 
			
		||||
		"gulp-sourcemaps": "~1.6.0",
 | 
			
		||||
		"gulp-uglify": "~1.5.3",
 | 
			
		||||
		"minimist": "^1.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "bootstrap": "^4.1.3",
 | 
			
		||||
    "jquery": "^3.3.1",
 | 
			
		||||
    "popper.js": "^1.14.4",
 | 
			
		||||
    "video.js": "^7.2.2"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,8 @@ class PillarServer(BlinkerCompatibleEve):
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        # disabled on all endpoints. More info at WTF_CSRF_CHECK_DEFAULT in config.py
 | 
			
		||||
        self.csrf = CSRFProtect(self)
 | 
			
		||||
@@ -278,7 +280,7 @@ class PillarServer(BlinkerCompatibleEve):
 | 
			
		||||
        self.encoding_service_client = Zencoder(self.config['ZENCODER_API_KEY'])
 | 
			
		||||
 | 
			
		||||
    def _config_caching(self):
 | 
			
		||||
        from flask_caching import Cache
 | 
			
		||||
        from flask_cache import Cache
 | 
			
		||||
        self.cache = Cache(self)
 | 
			
		||||
 | 
			
		||||
    def set_languages(self, translations_folder: pathlib.Path):
 | 
			
		||||
@@ -477,11 +479,10 @@ class PillarServer(BlinkerCompatibleEve):
 | 
			
		||||
 | 
			
		||||
        # Pillar-defined Celery task modules:
 | 
			
		||||
        celery_task_modules = [
 | 
			
		||||
            'pillar.celery.badges',
 | 
			
		||||
            'pillar.celery.email_tasks',
 | 
			
		||||
            'pillar.celery.file_link_tasks',
 | 
			
		||||
            'pillar.celery.search_index_tasks',
 | 
			
		||||
            'pillar.celery.tasks',
 | 
			
		||||
            'pillar.celery.search_index_tasks',
 | 
			
		||||
            'pillar.celery.file_link_tasks',
 | 
			
		||||
            'pillar.celery.email_tasks',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # Allow Pillar extensions from defining their own Celery tasks.
 | 
			
		||||
@@ -703,8 +704,6 @@ class PillarServer(BlinkerCompatibleEve):
 | 
			
		||||
    def finish_startup(self):
 | 
			
		||||
        self.log.info('Using MongoDB database %r', self.config['MONGO_DBNAME'])
 | 
			
		||||
 | 
			
		||||
        with self.app_context():
 | 
			
		||||
            self.setup_db_indices()
 | 
			
		||||
        self._config_celery()
 | 
			
		||||
 | 
			
		||||
        api.setup_app(self)
 | 
			
		||||
@@ -761,8 +760,6 @@ class PillarServer(BlinkerCompatibleEve):
 | 
			
		||||
        coll.create_index([('properties.status', pymongo.ASCENDING),
 | 
			
		||||
                           ('node_type', pymongo.ASCENDING),
 | 
			
		||||
                           ('_created', pymongo.DESCENDING)])
 | 
			
		||||
        # Used for asset tags
 | 
			
		||||
        coll.create_index([('properties.tags', pymongo.ASCENDING)])
 | 
			
		||||
 | 
			
		||||
        coll = db['projects']
 | 
			
		||||
        # This index is used for statistics, and for fetching public projects.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ with Blender ID.
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
import logging
 | 
			
		||||
from urllib.parse import urljoin
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from bson import tz_util
 | 
			
		||||
@@ -115,14 +114,13 @@ def validate_token(user_id, token, oauth_subclient_id):
 | 
			
		||||
        # We only want to accept Blender Cloud tokens.
 | 
			
		||||
        payload['client_id'] = current_app.config['OAUTH_CREDENTIALS']['blender-id']['id']
 | 
			
		||||
 | 
			
		||||
    blender_id_endpoint = current_app.config['BLENDER_ID_ENDPOINT']
 | 
			
		||||
    url = urljoin(blender_id_endpoint, 'u/validate_token')
 | 
			
		||||
    url = '{0}/u/validate_token'.format(current_app.config['BLENDER_ID_ENDPOINT'])
 | 
			
		||||
    log.debug('POSTing to %r', url)
 | 
			
		||||
 | 
			
		||||
    # Retry a few times when POSTing to BlenderID fails.
 | 
			
		||||
    # Source: http://stackoverflow.com/a/15431343/875379
 | 
			
		||||
    s = requests.Session()
 | 
			
		||||
    s.mount(blender_id_endpoint, HTTPAdapter(max_retries=5))
 | 
			
		||||
    s.mount(current_app.config['BLENDER_ID_ENDPOINT'], HTTPAdapter(max_retries=5))
 | 
			
		||||
 | 
			
		||||
    # POST to Blender ID, handling errors as negative verification results.
 | 
			
		||||
    try:
 | 
			
		||||
@@ -220,7 +218,7 @@ def fetch_blenderid_user() -> dict:
 | 
			
		||||
 | 
			
		||||
    my_log = log.getChild('fetch_blenderid_user')
 | 
			
		||||
 | 
			
		||||
    bid_url = urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'api/user')
 | 
			
		||||
    bid_url = '%s/api/user' % current_app.config['BLENDER_ID_ENDPOINT']
 | 
			
		||||
    my_log.debug('Fetching user info from %s', bid_url)
 | 
			
		||||
 | 
			
		||||
    credentials = current_app.config['OAUTH_CREDENTIALS']['blender-id']
 | 
			
		||||
@@ -265,7 +263,7 @@ def setup_app(app, url_prefix):
 | 
			
		||||
def switch_user_url(next_url: str) -> str:
 | 
			
		||||
    from urllib.parse import quote
 | 
			
		||||
 | 
			
		||||
    base_url = urljoin(current_app.config['BLENDER_ID_ENDPOINT'], 'switch')
 | 
			
		||||
    base_url = '%s/switch' % current_app.config['BLENDER_ID_ENDPOINT']
 | 
			
		||||
    if next_url:
 | 
			
		||||
        return '%s?next=%s' % (base_url, quote(next_url))
 | 
			
		||||
    return base_url
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import copy
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from bson import ObjectId, tz_util
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import cerberus.errors
 | 
			
		||||
from eve.io.mongo import Validator
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
@@ -12,31 +12,6 @@ log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ValidateCustomFields(Validator):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Will be reference to the actual document being validated, so that we can
 | 
			
		||||
        # modify it during validation.
 | 
			
		||||
        self.__real_document = None
 | 
			
		||||
 | 
			
		||||
    def validate(self, document, *args, **kwargs):
 | 
			
		||||
        # Keep a reference to the actual document, because Cerberus validates copies.
 | 
			
		||||
        self.__real_document = document
 | 
			
		||||
        result = super().validate(document, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Store the in-place modified document as self.document, so that Eve's post_internal
 | 
			
		||||
        # can actually pick it up as the validated document. We need to make a copy so that
 | 
			
		||||
        # further modifications (like setting '_etag' etc.) aren't done in-place.
 | 
			
		||||
        self.document = copy.deepcopy(document)
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def _get_child_validator(self, *args, **kwargs):
 | 
			
		||||
        child = super()._get_child_validator(*args, **kwargs)
 | 
			
		||||
        # Pass along our reference to the actual document.
 | 
			
		||||
        child.__real_document = self.__real_document
 | 
			
		||||
        return child
 | 
			
		||||
 | 
			
		||||
    # TODO: split this into a convert_property(property, schema) and call that from this function.
 | 
			
		||||
    def convert_properties(self, properties, node_schema):
 | 
			
		||||
        """Converts datetime strings and ObjectId strings to actual Python objects."""
 | 
			
		||||
@@ -98,11 +73,6 @@ class ValidateCustomFields(Validator):
 | 
			
		||||
            dict_property[key] = self.convert_properties(item_prop, item_schema)['item']
 | 
			
		||||
 | 
			
		||||
    def _validate_valid_properties(self, valid_properties, field, value):
 | 
			
		||||
        """Fake property that triggers node dynamic property validation.
 | 
			
		||||
 | 
			
		||||
        The rule's arguments are validated against this schema:
 | 
			
		||||
        {'type': 'boolean'}
 | 
			
		||||
        """
 | 
			
		||||
        from pillar.api.utils import project_get_node_type
 | 
			
		||||
 | 
			
		||||
        projects_collection = current_app.data.driver.db['projects']
 | 
			
		||||
@@ -137,8 +107,7 @@ class ValidateCustomFields(Validator):
 | 
			
		||||
        if val:
 | 
			
		||||
            # This ensures the modifications made by v's coercion rules are
 | 
			
		||||
            # visible to this validator's output.
 | 
			
		||||
            # TODO(fsiddi): this no longer works due to Cerberus internal changes.
 | 
			
		||||
            # self.current[field] = v.current
 | 
			
		||||
            self.current[field] = v.current
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        log.warning('Error validating properties for node %s: %s', self.document, v.errors)
 | 
			
		||||
@@ -149,9 +118,6 @@ class ValidateCustomFields(Validator):
 | 
			
		||||
 | 
			
		||||
        Combine "required_after_creation=True" with "required=False" to allow
 | 
			
		||||
        pre-insert hooks to set default values.
 | 
			
		||||
 | 
			
		||||
        The rule's arguments are validated against this schema:
 | 
			
		||||
        {'type': 'boolean'}
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if not required_after_creation:
 | 
			
		||||
@@ -159,14 +125,14 @@ class ValidateCustomFields(Validator):
 | 
			
		||||
            # validator at all.
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self.document_id is None:
 | 
			
		||||
        if self._id is None:
 | 
			
		||||
            # This is a creation call, in which case this validator shouldn't run.
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if not value:
 | 
			
		||||
            self._error(field, "Value is required once the document was created")
 | 
			
		||||
 | 
			
		||||
    def _validator_iprange(self, field_name: str, value: str):
 | 
			
		||||
    def _validate_type_iprange(self, field_name: str, value: str):
 | 
			
		||||
        """Ensure the field contains a valid IP address.
 | 
			
		||||
 | 
			
		||||
        Supports both IPv6 and IPv4 ranges. Requires the IPy module.
 | 
			
		||||
@@ -183,36 +149,40 @@ class ValidateCustomFields(Validator):
 | 
			
		||||
        if ip.prefixlen() == 0:
 | 
			
		||||
            self._error(field_name, 'Zero-length prefix is not allowed')
 | 
			
		||||
 | 
			
		||||
    def _validator_markdown(self, field, value):
 | 
			
		||||
        """Convert MarkDown.
 | 
			
		||||
    def _validate_type_binary(self, field_name: str, value: bytes):
 | 
			
		||||
        """Add support for binary type.
 | 
			
		||||
 | 
			
		||||
        This type was actually introduced in Cerberus 1.0, so we can drop
 | 
			
		||||
        support for this once Eve starts using that version (or newer).
 | 
			
		||||
        """
 | 
			
		||||
        my_log = log.getChild('_validator_markdown')
 | 
			
		||||
 | 
			
		||||
        # Find this field inside the original document
 | 
			
		||||
        my_subdoc = self._subdoc_in_real_document()
 | 
			
		||||
        if my_subdoc is None:
 | 
			
		||||
            # If self.update==True we are validating an update document, which
 | 
			
		||||
            # may not contain all fields, so then a missing field is fine.
 | 
			
		||||
            if not self.update:
 | 
			
		||||
                self._error(field, f'validator_markdown: unable to find sub-document '
 | 
			
		||||
                                   f'for path {self.document_path}')
 | 
			
		||||
            return
 | 
			
		||||
        if not isinstance(value, (bytes, bytearray)):
 | 
			
		||||
            self._error(field_name, f'wrong value type {type(value)}, expected bytes or bytearray')
 | 
			
		||||
 | 
			
		||||
        my_log.debug('validating field %r with value %r', field, value)
 | 
			
		||||
        save_to = pillar.markdown.cache_field_name(field)
 | 
			
		||||
    def _validate_coerce(self, coerce, field: str, value):
 | 
			
		||||
        """Override Cerberus' _validate_coerce method for richer features.
 | 
			
		||||
 | 
			
		||||
        This now supports named coercion functions (available in Cerberus 1.0+)
 | 
			
		||||
        and passes the field name to coercion functions as well.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(coerce, str):
 | 
			
		||||
            coerce = getattr(self, f'_normalize_coerce_{coerce}')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return coerce(field, value)
 | 
			
		||||
        except (TypeError, ValueError):
 | 
			
		||||
            self._error(field, cerberus.errors.ERROR_COERCION_FAILED.format(field))
 | 
			
		||||
 | 
			
		||||
    def _normalize_coerce_markdown(self, field: str, value):
 | 
			
		||||
        """Render Markdown from this field into {field}_html.
 | 
			
		||||
 | 
			
		||||
        The field name MUST NOT end in `_html`. The Markdown is read from this
 | 
			
		||||
        field and the rendered HTML is written to the field `{field}_html`.
 | 
			
		||||
        """
 | 
			
		||||
        html = pillar.markdown.markdown(value)
 | 
			
		||||
        my_log.debug('saving result to %r in doc with id %s', save_to, id(my_subdoc))
 | 
			
		||||
        my_subdoc[save_to] = html
 | 
			
		||||
 | 
			
		||||
    def _subdoc_in_real_document(self):
 | 
			
		||||
        """Return a reference to the current sub-document inside the real document.
 | 
			
		||||
 | 
			
		||||
        This allows modification of the document being validated.
 | 
			
		||||
        """
 | 
			
		||||
        my_subdoc = getattr(self, 'persisted_document') or self.__real_document
 | 
			
		||||
        for item in self.document_path:
 | 
			
		||||
            my_subdoc = my_subdoc[item]
 | 
			
		||||
        return my_subdoc
 | 
			
		||||
        field_name = pillar.markdown.cache_field_name(field)
 | 
			
		||||
        self.current[field_name] = html
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
@@ -220,12 +190,12 @@ if __name__ == '__main__':
 | 
			
		||||
 | 
			
		||||
    v = ValidateCustomFields()
 | 
			
		||||
    v.schema = {
 | 
			
		||||
        'foo': {'type': 'string', 'validator': 'markdown'},
 | 
			
		||||
        'foo': {'type': 'string', 'coerce': 'markdown'},
 | 
			
		||||
        'foo_html': {'type': 'string'},
 | 
			
		||||
        'nested': {
 | 
			
		||||
            'type': 'dict',
 | 
			
		||||
            'schema': {
 | 
			
		||||
                'bar': {'type': 'string', 'validator': 'markdown'},
 | 
			
		||||
                'bar': {'type': 'string', 'coerce': 'markdown'},
 | 
			
		||||
                'bar_html': {'type': 'string'},
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -121,43 +121,12 @@ users_schema = {
 | 
			
		||||
    'service': {
 | 
			
		||||
        'type': 'dict',
 | 
			
		||||
        '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.
 | 
			
		||||
        },
 | 
			
		||||
            'badger': {
 | 
			
		||||
                'type': 'list',
 | 
			
		||||
                'schema': {'type': 'string'}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    # Properties defined by extensions. Extensions should use their name (see the
 | 
			
		||||
@@ -186,7 +155,7 @@ organizations_schema = {
 | 
			
		||||
    'description': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'maxlength': 256,
 | 
			
		||||
        'validator': 'markdown',
 | 
			
		||||
        'coerce': 'markdown',
 | 
			
		||||
    },
 | 
			
		||||
    '_description_html': {'type': 'string'},
 | 
			
		||||
    'website': {
 | 
			
		||||
@@ -258,7 +227,7 @@ organizations_schema = {
 | 
			
		||||
                'start': {'type': 'binary', 'required': True},
 | 
			
		||||
                'end': {'type': 'binary', 'required': True},
 | 
			
		||||
                'prefix': {'type': 'integer', 'required': True},
 | 
			
		||||
                'human': {'type': 'string', 'required': True, 'validator': 'iprange'},
 | 
			
		||||
                'human': {'type': 'iprange', 'required': True},
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
@@ -323,7 +292,7 @@ nodes_schema = {
 | 
			
		||||
    },
 | 
			
		||||
    'description': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'validator': 'markdown',
 | 
			
		||||
        'coerce': 'markdown',
 | 
			
		||||
    },
 | 
			
		||||
    '_description_html': {'type': 'string'},
 | 
			
		||||
    'picture': _file_embedded_schema,
 | 
			
		||||
@@ -358,7 +327,7 @@ nodes_schema = {
 | 
			
		||||
    'properties': {
 | 
			
		||||
        'type': 'dict',
 | 
			
		||||
        'valid_properties': True,
 | 
			
		||||
        'required': True
 | 
			
		||||
        'required': True,
 | 
			
		||||
    },
 | 
			
		||||
    'permissions': {
 | 
			
		||||
        'type': 'dict',
 | 
			
		||||
@@ -376,11 +345,11 @@ tokens_schema = {
 | 
			
		||||
    },
 | 
			
		||||
    'token': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'required': True,
 | 
			
		||||
        'required': False,
 | 
			
		||||
    },
 | 
			
		||||
    'token_hashed': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'required': False,
 | 
			
		||||
        'required': True,
 | 
			
		||||
    },
 | 
			
		||||
    'expire_time': {
 | 
			
		||||
        'type': 'datetime',
 | 
			
		||||
@@ -399,13 +368,6 @@ tokens_schema = {
 | 
			
		||||
            'type': 'string',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    # OAuth scopes granted to this token.
 | 
			
		||||
    'oauth_scopes': {
 | 
			
		||||
        'type': 'list',
 | 
			
		||||
        'default': [],
 | 
			
		||||
        'schema': {'type': 'string'},
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
files_schema = {
 | 
			
		||||
@@ -577,7 +539,7 @@ projects_schema = {
 | 
			
		||||
    },
 | 
			
		||||
    'description': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'validator': 'markdown',
 | 
			
		||||
        'coerce': 'markdown',
 | 
			
		||||
    },
 | 
			
		||||
    '_description_html': {'type': 'string'},
 | 
			
		||||
    # Short summary for the project
 | 
			
		||||
@@ -871,9 +833,4 @@ UPSET_ON_PUT = False  # do not create new document on PUT of non-existant URL.
 | 
			
		||||
X_DOMAINS = '*'
 | 
			
		||||
X_ALLOW_CREDENTIALS = True
 | 
			
		||||
X_HEADERS = 'Authorization'
 | 
			
		||||
RENDERERS = ['eve.render.JSONRenderer']
 | 
			
		||||
 | 
			
		||||
# TODO(Sybren): this is a quick workaround to make /p/{url}/jstree work again.
 | 
			
		||||
# Apparently Eve is now stricter in checking against MONGO_QUERY_BLACKLIST, and
 | 
			
		||||
# blocks our use of $regex.
 | 
			
		||||
MONGO_QUERY_BLACKLIST = ['$where']
 | 
			
		||||
XML = False
 | 
			
		||||
 
 | 
			
		||||
@@ -94,10 +94,17 @@ def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
 | 
			
		||||
 | 
			
		||||
    # Use 'xy' as altargs to prevent + and / characters from appearing.
 | 
			
		||||
    # We never have to b64decode the string anyway.
 | 
			
		||||
    token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
 | 
			
		||||
    token_bytes = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
 | 
			
		||||
    token = token_bytes.decode('ascii')
 | 
			
		||||
 | 
			
		||||
    token_expiry = utcnow() + datetime.timedelta(days=days)
 | 
			
		||||
    return store_token(user_id, token.decode('ascii'), token_expiry)
 | 
			
		||||
    token_data = store_token(user_id, token, 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:
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ ATTACHMENT_SLUG_REGEX = r'[a-zA-Z0-9_\-]+'
 | 
			
		||||
attachments_embedded_schema = {
 | 
			
		||||
    'type': 'dict',
 | 
			
		||||
    # TODO: will be renamed to 'keyschema' in Cerberus 1.0
 | 
			
		||||
    'keyschema': {
 | 
			
		||||
    'propertyschema': {
 | 
			
		||||
        'type': 'string',
 | 
			
		||||
        'regex': '^%s$' % ATTACHMENT_SLUG_REGEX,
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ node_type_comment = {
 | 
			
		||||
            'type': 'string',
 | 
			
		||||
            'minlength': 5,
 | 
			
		||||
            'required': True,
 | 
			
		||||
            'validator': 'markdown',
 | 
			
		||||
            'coerce': 'markdown',
 | 
			
		||||
        },
 | 
			
		||||
        '_content_html': {'type': 'string'},
 | 
			
		||||
        'status': {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ node_type_group = {
 | 
			
		||||
    'description': 'Folder node type',
 | 
			
		||||
    'parent': ['group', 'project'],
 | 
			
		||||
    'dyn_schema': {
 | 
			
		||||
 | 
			
		||||
        # Used for sorting within the context of a group
 | 
			
		||||
        'order': {
 | 
			
		||||
            'type': 'integer'
 | 
			
		||||
        },
 | 
			
		||||
@@ -20,8 +20,7 @@ node_type_group = {
 | 
			
		||||
        'notes': {
 | 
			
		||||
            'type': 'string',
 | 
			
		||||
            'maxlength': 256,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    'form_schema': {
 | 
			
		||||
        'url': {'visible': False},
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ node_type_post = {
 | 
			
		||||
            'minlength': 5,
 | 
			
		||||
            'maxlength': 90000,
 | 
			
		||||
            'required': True,
 | 
			
		||||
            'validator': 'markdown',
 | 
			
		||||
            'coerce': 'markdown',
 | 
			
		||||
        },
 | 
			
		||||
        '_content_html': {'type': 'string'},
 | 
			
		||||
        'status': {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import base64
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import typing
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
import pymongo.errors
 | 
			
		||||
@@ -9,7 +8,6 @@ import werkzeug.exceptions as wz_exceptions
 | 
			
		||||
from bson import ObjectId
 | 
			
		||||
from flask import current_app, Blueprint, request
 | 
			
		||||
 | 
			
		||||
import pillar.markdown
 | 
			
		||||
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
 | 
			
		||||
@@ -90,48 +88,6 @@ def share_node(node_id):
 | 
			
		||||
    return jsonify(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."""
 | 
			
		||||
 | 
			
		||||
    # 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()
 | 
			
		||||
 | 
			
		||||
    return _tagged(tag)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 jsonify(list(agg))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_and_store_short_code(node):
 | 
			
		||||
    nodes_coll = current_app.data.driver.db['nodes']
 | 
			
		||||
    node_id = node['_id']
 | 
			
		||||
@@ -444,52 +400,7 @@ def textures_sort_files(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):
 | 
			
		||||
    global _tagged
 | 
			
		||||
 | 
			
		||||
    cached = app.cache.memoize(timeout=300)
 | 
			
		||||
    _tagged = cached(_tagged)
 | 
			
		||||
 | 
			
		||||
    from . import patch
 | 
			
		||||
    patch.setup_app(app, url_prefix=url_prefix)
 | 
			
		||||
 | 
			
		||||
@@ -497,14 +408,12 @@ def setup_app(app, url_prefix):
 | 
			
		||||
    app.on_fetched_resource_nodes += before_returning_nodes
 | 
			
		||||
 | 
			
		||||
    app.on_replace_nodes += before_replacing_node
 | 
			
		||||
    app.on_replace_nodes += parse_markdown
 | 
			
		||||
    app.on_replace_nodes += texture_sort_files
 | 
			
		||||
    app.on_replace_nodes += deduct_content_type
 | 
			
		||||
    app.on_replace_nodes += node_set_default_picture
 | 
			
		||||
    app.on_replaced_nodes += after_replacing_node
 | 
			
		||||
 | 
			
		||||
    app.on_insert_nodes += before_inserting_nodes
 | 
			
		||||
    app.on_insert_nodes += parse_markdowns
 | 
			
		||||
    app.on_insert_nodes += nodes_deduct_content_type
 | 
			
		||||
    app.on_insert_nodes += nodes_set_default_picture
 | 
			
		||||
    app.on_insert_nodes += textures_sort_files
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
"""Code for moving around nodes."""
 | 
			
		||||
 | 
			
		||||
import attr
 | 
			
		||||
import pymongo.database
 | 
			
		||||
import flask_pymongo.wrappers
 | 
			
		||||
from bson import ObjectId
 | 
			
		||||
 | 
			
		||||
from pillar import attrs_extra
 | 
			
		||||
@@ -10,7 +10,7 @@ import pillar.api.file_storage.moving
 | 
			
		||||
 | 
			
		||||
@attr.s
 | 
			
		||||
class NodeMover(object):
 | 
			
		||||
    db = attr.ib(validator=attr.validators.instance_of(pymongo.database.Database))
 | 
			
		||||
    db = attr.ib(validator=attr.validators.instance_of(flask_pymongo.wrappers.Database))
 | 
			
		||||
    skip_gcs = attr.ib(default=False, validator=attr.validators.instance_of(bool))
 | 
			
		||||
    _log = attrs_extra.log('%s.NodeMover' % __name__)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -71,19 +71,14 @@ def before_delete_project(document):
 | 
			
		||||
 | 
			
		||||
def after_delete_project(project: dict):
 | 
			
		||||
    """Perform delete on the project's files too."""
 | 
			
		||||
    from werkzeug.exceptions import NotFound
 | 
			
		||||
 | 
			
		||||
    from eve.methods.delete import delete
 | 
			
		||||
 | 
			
		||||
    pid = project['_id']
 | 
			
		||||
    log.info('Project %s was deleted, also deleting its files.', pid)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
    r, _, _, status = delete('files', {'project': pid})
 | 
			
		||||
    except NotFound:
 | 
			
		||||
        # There were no files, and that's fine.
 | 
			
		||||
        return
 | 
			
		||||
    if status != 204:
 | 
			
		||||
        # Will never happen because bloody Eve always returns 204 or raises an exception.
 | 
			
		||||
        log.warning('Unable to delete files of project %s: %s', pid, r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -142,7 +142,7 @@ def after_fetching_user(user):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Remove all fields except public ones.
 | 
			
		||||
    public_fields = {'full_name', 'username', 'email', 'extension_props_public', 'badges'}
 | 
			
		||||
    public_fields = {'full_name', 'username', 'email', 'extension_props_public'}
 | 
			
		||||
    for field in list(user.keys()):
 | 
			
		||||
        if field not in public_fields:
 | 
			
		||||
            del user[field]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,9 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from eve.methods.get import get
 | 
			
		||||
from flask import Blueprint, request
 | 
			
		||||
import werkzeug.exceptions as wz_exceptions
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
 | 
			
		||||
from pillar import current_app
 | 
			
		||||
from pillar.api import utils
 | 
			
		||||
from pillar.api.utils import jsonify
 | 
			
		||||
from pillar.api.utils.authorization import require_login
 | 
			
		||||
from pillar.auth import current_user
 | 
			
		||||
 | 
			
		||||
@@ -17,128 +15,7 @@ blueprint_api = Blueprint('users_api', __name__)
 | 
			
		||||
@require_login()
 | 
			
		||||
def my_info():
 | 
			
		||||
    eve_resp, _, _, status, _ = get('users', {'_id': current_user.user_id})
 | 
			
		||||
    resp = utils.jsonify(eve_resp['_items'][0], status=status)
 | 
			
		||||
    resp = jsonify(eve_resp['_items'][0], status=status)
 | 
			
		||||
    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,10 +245,4 @@ def random_etag() -> str:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def utcnow() -> datetime.datetime:
 | 
			
		||||
    """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
 | 
			
		||||
    return datetime.datetime.now(tz=bson.tz_util.utc)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ import logging
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
import bson
 | 
			
		||||
from flask import g, current_app, session
 | 
			
		||||
from flask import g, current_app
 | 
			
		||||
from flask import request
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_token(*, force=False) -> bool:
 | 
			
		||||
def validate_token(*, force=False):
 | 
			
		||||
    """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
 | 
			
		||||
    from it.
 | 
			
		||||
@@ -115,7 +115,7 @@ def validate_token(*, force=False) -> bool:
 | 
			
		||||
    :returns: True iff the user is logged in with a valid Blender ID token.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    import pillar.auth
 | 
			
		||||
    from pillar.auth import AnonymousUser
 | 
			
		||||
 | 
			
		||||
    # Trust a pre-existing g.current_user
 | 
			
		||||
    if not force:
 | 
			
		||||
@@ -133,22 +133,16 @@ def validate_token(*, force=False) -> bool:
 | 
			
		||||
        oauth_subclient = ''
 | 
			
		||||
    else:
 | 
			
		||||
        # Check the session, the user might be logged in through Flask-Login.
 | 
			
		||||
        from pillar import auth
 | 
			
		||||
 | 
			
		||||
        # 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()
 | 
			
		||||
        token = auth.get_blender_id_oauth_token()
 | 
			
		||||
        oauth_subclient = None
 | 
			
		||||
 | 
			
		||||
    if not token:
 | 
			
		||||
        # If no authorization headers are provided, we are getting a request
 | 
			
		||||
        # from a non logged in user. Proceed accordingly.
 | 
			
		||||
        log.debug('No authentication headers, so not logged in.')
 | 
			
		||||
        g.current_user = pillar.auth.AnonymousUser()
 | 
			
		||||
        g.current_user = AnonymousUser()
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return validate_this_token(token, oauth_subclient) is not None
 | 
			
		||||
@@ -200,7 +194,7 @@ def remove_token(token: str):
 | 
			
		||||
    tokens_coll = current_app.db('tokens')
 | 
			
		||||
    token_hashed = hash_auth_token(token)
 | 
			
		||||
 | 
			
		||||
    # TODO: remove matching on hashed tokens once all hashed tokens have expired.
 | 
			
		||||
    # TODO: remove matching on unhashed tokens once all tokens have been hashed.
 | 
			
		||||
    lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]}
 | 
			
		||||
    del_res = tokens_coll.delete_many(lookup)
 | 
			
		||||
    log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count)
 | 
			
		||||
@@ -212,7 +206,7 @@ def find_token(token, is_subclient_token=False, **extra_filters):
 | 
			
		||||
    tokens_coll = current_app.db('tokens')
 | 
			
		||||
    token_hashed = hash_auth_token(token)
 | 
			
		||||
 | 
			
		||||
    # TODO: remove matching on hashed tokens once all hashed tokens have expired.
 | 
			
		||||
    # TODO: remove matching on unhashed tokens once all tokens have been hashed.
 | 
			
		||||
    lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}],
 | 
			
		||||
              'is_subclient_token': True if is_subclient_token else {'$in': [False, None]},
 | 
			
		||||
              'expire_time': {"$gt": utcnow()}}
 | 
			
		||||
@@ -235,14 +229,8 @@ def hash_auth_token(token: str) -> str:
 | 
			
		||||
    return base64.b64encode(digest).decode('ascii')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def store_token(user_id,
 | 
			
		||||
                token: str,
 | 
			
		||||
                token_expiry,
 | 
			
		||||
                oauth_subclient_id=False,
 | 
			
		||||
                *,
 | 
			
		||||
                org_roles: typing.Set[str] = frozenset(),
 | 
			
		||||
                oauth_scopes: typing.Optional[typing.List[str]] = None,
 | 
			
		||||
                ):
 | 
			
		||||
def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
 | 
			
		||||
                org_roles: typing.Set[str] = frozenset()):
 | 
			
		||||
    """Stores an authentication token.
 | 
			
		||||
 | 
			
		||||
    :returns: the token document from MongoDB
 | 
			
		||||
@@ -252,15 +240,13 @@ def store_token(user_id,
 | 
			
		||||
 | 
			
		||||
    token_data = {
 | 
			
		||||
        'user': user_id,
 | 
			
		||||
        'token': token,
 | 
			
		||||
        'token_hashed': hash_auth_token(token),
 | 
			
		||||
        'expire_time': token_expiry,
 | 
			
		||||
    }
 | 
			
		||||
    if oauth_subclient_id:
 | 
			
		||||
        token_data['is_subclient_token'] = True
 | 
			
		||||
    if 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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import logging
 | 
			
		||||
import functools
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
from bson import ObjectId
 | 
			
		||||
from flask import g
 | 
			
		||||
@@ -13,9 +12,8 @@ CHECK_PERMISSIONS_IMPLEMENTED_FOR = {'projects', 'nodes', 'flamenco_jobs'}
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_permissions(collection_name: str, resource: dict, method: str,
 | 
			
		||||
                      append_allowed_methods=False,
 | 
			
		||||
                      check_node_type: typing.Optional[str] = None):
 | 
			
		||||
def check_permissions(collection_name, resource, method, append_allowed_methods=False,
 | 
			
		||||
                      check_node_type=None):
 | 
			
		||||
    """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.
 | 
			
		||||
    If there is not match, we raise 403.
 | 
			
		||||
@@ -95,9 +93,8 @@ def compute_allowed_methods(collection_name, resource, check_node_type=None):
 | 
			
		||||
    return allowed_methods
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_permissions(collection_name: str, resource: dict, method: str,
 | 
			
		||||
                    append_allowed_methods=False,
 | 
			
		||||
                    check_node_type: typing.Optional[str] = None):
 | 
			
		||||
def has_permissions(collection_name, resource, method, append_allowed_methods=False,
 | 
			
		||||
                    check_node_type=None):
 | 
			
		||||
    """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.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,6 @@ class UserClass(flask_login.UserMixin):
 | 
			
		||||
        self.groups: typing.List[str] = []  # NOTE: these are stringified object IDs.
 | 
			
		||||
        self.group_ids: typing.List[bson.ObjectId] = []
 | 
			
		||||
        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
 | 
			
		||||
        self._has_organizations: typing.Optional[bool] = None
 | 
			
		||||
@@ -58,12 +56,6 @@ class UserClass(flask_login.UserMixin):
 | 
			
		||||
        user.email = db_user.get('email') or ''
 | 
			
		||||
        user.username = db_user.get('username') 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
 | 
			
		||||
        user.objectid = str(user.user_id or '')
 | 
			
		||||
@@ -218,11 +210,6 @@ def login_user(oauth_token: str, *, load_from_db=False):
 | 
			
		||||
        user = _load_user(oauth_token)
 | 
			
		||||
    else:
 | 
			
		||||
        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)
 | 
			
		||||
    g.current_user = user
 | 
			
		||||
    user_authenticated.send(None)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import abc
 | 
			
		||||
import attr
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
import attr
 | 
			
		||||
from rauth import OAuth2Service
 | 
			
		||||
from flask import current_app, url_for, request, redirect, session, Response
 | 
			
		||||
 | 
			
		||||
@@ -16,8 +15,6 @@ class OAuthUserResponse:
 | 
			
		||||
 | 
			
		||||
    id = 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):
 | 
			
		||||
@@ -130,10 +127,8 @@ class OAuthSignIn(metaclass=abc.ABCMeta):
 | 
			
		||||
 | 
			
		||||
class BlenderIdSignIn(OAuthSignIn):
 | 
			
		||||
    provider_name = 'blender-id'
 | 
			
		||||
    scopes = ['email', 'badge']
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        from urllib.parse import urljoin
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        base_url = current_app.config['BLENDER_ID_ENDPOINT']
 | 
			
		||||
@@ -142,14 +137,14 @@ class BlenderIdSignIn(OAuthSignIn):
 | 
			
		||||
            name='blender-id',
 | 
			
		||||
            client_id=self.consumer_id,
 | 
			
		||||
            client_secret=self.consumer_secret,
 | 
			
		||||
            authorize_url=urljoin(base_url, 'oauth/authorize'),
 | 
			
		||||
            access_token_url=urljoin(base_url, 'oauth/token'),
 | 
			
		||||
            base_url=urljoin(base_url, 'api/'),
 | 
			
		||||
            authorize_url='%s/oauth/authorize' % base_url,
 | 
			
		||||
            access_token_url='%s/oauth/token' % base_url,
 | 
			
		||||
            base_url='%s/api/' % base_url
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def authorize(self):
 | 
			
		||||
        return redirect(self.service.get_authorize_url(
 | 
			
		||||
            scope=' '.join(self.scopes),
 | 
			
		||||
            scope='email',
 | 
			
		||||
            response_type='code',
 | 
			
		||||
            redirect_uri=self.get_callback_url())
 | 
			
		||||
        )
 | 
			
		||||
@@ -163,11 +158,7 @@ class BlenderIdSignIn(OAuthSignIn):
 | 
			
		||||
 | 
			
		||||
        session['blender_id_oauth_token'] = access_token
 | 
			
		||||
        me = oauth_session.get('user').json()
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
        return OAuthUserResponse(str(me['id']), me['email'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FacebookSignIn(OAuthSignIn):
 | 
			
		||||
@@ -197,7 +188,7 @@ class FacebookSignIn(OAuthSignIn):
 | 
			
		||||
        me = oauth_session.get('me?fields=id,email').json()
 | 
			
		||||
        # TODO handle case when user chooses not to disclose en email
 | 
			
		||||
        # 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):
 | 
			
		||||
@@ -225,4 +216,4 @@ class GoogleSignIn(OAuthSignIn):
 | 
			
		||||
        oauth_session = self.make_oauth_session()
 | 
			
		||||
 | 
			
		||||
        me = oauth_session.get('userinfo').json()
 | 
			
		||||
        return OAuthUserResponse(str(me['id']), me['email'], '', [])
 | 
			
		||||
        return OAuthUserResponse(str(me['id']), me['email'])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,183 +0,0 @@
 | 
			
		||||
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')
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
"""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,7 +13,6 @@ from pillar.cli.maintenance import manager_maintenance
 | 
			
		||||
from pillar.cli.operations import manager_operations
 | 
			
		||||
from pillar.cli.setup import manager_setup
 | 
			
		||||
from pillar.cli.elastic import manager_elastic
 | 
			
		||||
from . import badges
 | 
			
		||||
 | 
			
		||||
from pillar.cli import translations
 | 
			
		||||
 | 
			
		||||
@@ -25,4 +24,3 @@ manager.add_command("maintenance", manager_maintenance)
 | 
			
		||||
manager.add_command("setup", manager_setup)
 | 
			
		||||
manager.add_command("operations", manager_operations)
 | 
			
		||||
manager.add_command("elastic", manager_elastic)
 | 
			
		||||
manager.add_command("badges", badges.manager)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
@@ -684,7 +684,7 @@ def upgrade_attachment_schema(proj_url=None, all_projects=False, go=False):
 | 
			
		||||
                log_proj()
 | 
			
		||||
                log.info('Removed %d empty attachment dicts', res.modified_count)
 | 
			
		||||
        else:
 | 
			
		||||
            to_remove = nodes_coll.count_documents({'properties.attachments': {},
 | 
			
		||||
            to_remove = nodes_coll.count({'properties.attachments': {},
 | 
			
		||||
                                          'project': project['_id']})
 | 
			
		||||
            if to_remove:
 | 
			
		||||
                log_proj()
 | 
			
		||||
@@ -767,9 +767,7 @@ def iter_markdown(proj_node_types: dict, some_node: dict, callback: typing.Calla
 | 
			
		||||
                    continue
 | 
			
		||||
                to_visit.append((subdoc, definition['schema']))
 | 
			
		||||
                continue
 | 
			
		||||
            coerce = definition.get('coerce')  # Eve < 0.8
 | 
			
		||||
            validator = definition.get('validator')  # Eve >= 0.8
 | 
			
		||||
            if coerce != 'markdown' and validator != 'markdown':
 | 
			
		||||
            if definition.get('coerce') != 'markdown':
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            my_log.debug('I have to change %r of %s', key, doc)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
import datetime
 | 
			
		||||
import os.path
 | 
			
		||||
from os import getenv
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
import requests.certs
 | 
			
		||||
 | 
			
		||||
# Certificate file for communication with other systems.
 | 
			
		||||
@@ -31,11 +29,10 @@ DEBUG = False
 | 
			
		||||
SECRET_KEY = ''
 | 
			
		||||
 | 
			
		||||
# 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''
 | 
			
		||||
 | 
			
		||||
# Authentication settings
 | 
			
		||||
BLENDER_ID_ENDPOINT = 'http://id.local:8000/'
 | 
			
		||||
BLENDER_ID_ENDPOINT = 'http://id.local:8000'
 | 
			
		||||
 | 
			
		||||
CDN_USE_URL_SIGNING = True
 | 
			
		||||
CDN_SERVICE_DOMAIN_PROTOCOL = 'https'
 | 
			
		||||
@@ -206,18 +203,8 @@ CELERY_BEAT_SCHEDULE = {
 | 
			
		||||
        'schedule': 600,  # every N seconds
 | 
			
		||||
        '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.
 | 
			
		||||
USER_CAPABILITIES = defaultdict(**{
 | 
			
		||||
    'subscriber': {'subscriber', 'home-project'},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
import flask
 | 
			
		||||
import raven.breadcrumbs
 | 
			
		||||
from raven.contrib.flask import Sentry
 | 
			
		||||
 | 
			
		||||
from .auth import current_user
 | 
			
		||||
@@ -16,14 +14,16 @@ class PillarSentry(Sentry):
 | 
			
		||||
    def init_app(self, app, *args, **kwargs):
 | 
			
		||||
        super().init_app(app, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        flask.request_started.connect(self.__add_sentry_breadcrumbs, self)
 | 
			
		||||
        # We perform authentication of the user while handling the request,
 | 
			
		||||
        # so Sentry calls get_user_info() too early.
 | 
			
		||||
 | 
			
		||||
    def __add_sentry_breadcrumbs(self, sender, **extra):
 | 
			
		||||
        raven.breadcrumbs.record(
 | 
			
		||||
            message='Request started',
 | 
			
		||||
            category='http',
 | 
			
		||||
            data={'url': flask.request.url}
 | 
			
		||||
        )
 | 
			
		||||
    def get_user_context_again(self, ):
 | 
			
		||||
        from flask import request
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.client.user_context(self.get_user_info(request))
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            self.client.logger.exception(str(e))
 | 
			
		||||
 | 
			
		||||
    def get_user_info(self, request):
 | 
			
		||||
        user_info = super().get_user_info(request)
 | 
			
		||||
 
 | 
			
		||||
@@ -163,8 +163,11 @@ class YouTube:
 | 
			
		||||
            return html_module.escape('{youtube invalid YouTube ID/URL}')
 | 
			
		||||
 | 
			
		||||
        src = f'https://www.youtube.com/embed/{youtube_id}?rel=0'
 | 
			
		||||
        html = f'<iframe class="shortcode youtube" width="{width}" height="{height}" src="{src}"' \
 | 
			
		||||
               f' frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>'
 | 
			
		||||
        iframe_tag = f'<iframe class="shortcode youtube embed-responsive-item" width="{width}"' \
 | 
			
		||||
                     f' height="{height}" src="{src}" frameborder="0" allow="autoplay; encrypted-media"' \
 | 
			
		||||
                     f' allowfullscreen></iframe>'
 | 
			
		||||
        # Embed the iframe in a container, to allow easier styling
 | 
			
		||||
        html = f'<div class="embed-responsive embed-responsive-16by9">{iframe_tag}</div>'
 | 
			
		||||
        return html
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -230,7 +233,7 @@ class Attachment:
 | 
			
		||||
 | 
			
		||||
        from pillar.web import system_util
 | 
			
		||||
 | 
			
		||||
        attachments = node_properties.get('properties', {}).get('attachments', {})
 | 
			
		||||
        attachments = node_properties.get('attachments', {})
 | 
			
		||||
        attachment = attachments.get(slug)
 | 
			
		||||
        if not attachment:
 | 
			
		||||
            raise self.NoSuchSlug(slug)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
import base64
 | 
			
		||||
import contextlib
 | 
			
		||||
import copy
 | 
			
		||||
import datetime
 | 
			
		||||
import json
 | 
			
		||||
@@ -11,7 +10,11 @@ import pathlib
 | 
			
		||||
import sys
 | 
			
		||||
import typing
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +27,6 @@ from eve.tests import TestMinimal
 | 
			
		||||
import pymongo.collection
 | 
			
		||||
from flask.testing import FlaskClient
 | 
			
		||||
import flask.ctx
 | 
			
		||||
import flask.wrappers
 | 
			
		||||
import responses
 | 
			
		||||
 | 
			
		||||
import pillar
 | 
			
		||||
@@ -183,7 +185,7 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
        else:
 | 
			
		||||
            self.ensure_project_exists()
 | 
			
		||||
 | 
			
		||||
        with self.app.app_context():
 | 
			
		||||
        with self.app.test_request_context():
 | 
			
		||||
            files_collection = self.app.data.driver.db['files']
 | 
			
		||||
            assert isinstance(files_collection, pymongo.collection.Collection)
 | 
			
		||||
 | 
			
		||||
@@ -324,46 +326,15 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
    @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:
 | 
			
		||||
    def create_valid_auth_token(self, user_id, token='token'):
 | 
			
		||||
        from pillar.api.utils import utcnow
 | 
			
		||||
 | 
			
		||||
        future = utcnow() + datetime.timedelta(days=expire_in_days)
 | 
			
		||||
        future = utcnow() + datetime.timedelta(days=1)
 | 
			
		||||
 | 
			
		||||
        with self.app.test_request_context():
 | 
			
		||||
            from pillar.api.utils import authentication as auth
 | 
			
		||||
 | 
			
		||||
            token_data = auth.store_token(user_id, token, future, oauth_scopes=oauth_scopes)
 | 
			
		||||
            token_data = auth.store_token(user_id, token, future, None)
 | 
			
		||||
 | 
			
		||||
        return token_data
 | 
			
		||||
 | 
			
		||||
@@ -393,7 +364,7 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
 | 
			
		||||
        return user_id
 | 
			
		||||
 | 
			
		||||
    def create_node(self, node_doc) -> ObjectId:
 | 
			
		||||
    def create_node(self, node_doc):
 | 
			
		||||
        """Creates a node, returning its ObjectId. """
 | 
			
		||||
 | 
			
		||||
        with self.app.test_request_context():
 | 
			
		||||
@@ -435,7 +406,7 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
        """Sets up Responses to mock unhappy validation flow."""
 | 
			
		||||
 | 
			
		||||
        responses.add(responses.POST,
 | 
			
		||||
                      urljoin(self.app.config['BLENDER_ID_ENDPOINT'], 'u/validate_token'),
 | 
			
		||||
                      '%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
 | 
			
		||||
                      json={'status': 'fail'},
 | 
			
		||||
                      status=403)
 | 
			
		||||
 | 
			
		||||
@@ -443,7 +414,7 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
        """Sets up Responses to mock happy validation flow."""
 | 
			
		||||
 | 
			
		||||
        responses.add(responses.POST,
 | 
			
		||||
                      urljoin(self.app.config['BLENDER_ID_ENDPOINT'], 'u/validate_token'),
 | 
			
		||||
                      '%s/u/validate_token' % self.app.config['BLENDER_ID_ENDPOINT'],
 | 
			
		||||
                      json=BLENDER_ID_USER_RESPONSE,
 | 
			
		||||
                      status=200)
 | 
			
		||||
 | 
			
		||||
@@ -514,10 +485,11 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
 | 
			
		||||
    def client_request(self, method, path, qs=None, expected_status=200, auth_token=None, json=None,
 | 
			
		||||
                       data=None, headers=None, files=None, content_type=None, etag=None,
 | 
			
		||||
                       environ_overrides=None) -> flask.wrappers.Response:
 | 
			
		||||
                       environ_overrides=None):
 | 
			
		||||
        """Performs a HTTP request to the server."""
 | 
			
		||||
 | 
			
		||||
        from pillar.api.utils import dumps
 | 
			
		||||
        import json as mod_json
 | 
			
		||||
 | 
			
		||||
        headers = headers or {}
 | 
			
		||||
        environ_overrides = environ_overrides or {}
 | 
			
		||||
@@ -550,21 +522,29 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
                             expected_status, resp.status_code, resp.data
 | 
			
		||||
                         ))
 | 
			
		||||
 | 
			
		||||
        def get_json():
 | 
			
		||||
            if resp.mimetype != 'application/json':
 | 
			
		||||
                raise TypeError('Unable to load JSON from mimetype %r' % resp.mimetype)
 | 
			
		||||
            return mod_json.loads(resp.data)
 | 
			
		||||
 | 
			
		||||
        resp.json = get_json
 | 
			
		||||
        resp.get_json = get_json
 | 
			
		||||
 | 
			
		||||
        return resp
 | 
			
		||||
 | 
			
		||||
    def get(self, *args, **kwargs) -> flask.wrappers.Response:
 | 
			
		||||
    def get(self, *args, **kwargs):
 | 
			
		||||
        return self.client_request('GET', *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def post(self, *args, **kwargs) -> flask.wrappers.Response:
 | 
			
		||||
    def post(self, *args, **kwargs):
 | 
			
		||||
        return self.client_request('POST', *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def put(self, *args, **kwargs) -> flask.wrappers.Response:
 | 
			
		||||
    def put(self, *args, **kwargs):
 | 
			
		||||
        return self.client_request('PUT', *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs) -> flask.wrappers.Response:
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
        return self.client_request('DELETE', *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def patch(self, *args, **kwargs) -> flask.wrappers.Response:
 | 
			
		||||
    def patch(self, *args, **kwargs):
 | 
			
		||||
        return self.client_request('PATCH', *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def assertAllowsAccess(self,
 | 
			
		||||
@@ -581,7 +561,7 @@ class AbstractPillarTest(TestMinimal):
 | 
			
		||||
            raise TypeError('expected_user_id should be a string or ObjectId, '
 | 
			
		||||
                            f'but is {expected_user_id!r}')
 | 
			
		||||
 | 
			
		||||
        resp = self.get('/api/users/me', expected_status=200, auth_token=token).get_json()
 | 
			
		||||
        resp = self.get('/api/users/me', expected_status=200, auth_token=token).json()
 | 
			
		||||
 | 
			
		||||
        if expected_user_id:
 | 
			
		||||
            self.assertEqual(resp['_id'], str(expected_user_id))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
"""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'
 | 
			
		||||
PILLAR_SERVER_ENDPOINT = 'http://localhost.local/api/'
 | 
			
		||||
SERVER_NAME = 'localhost'
 | 
			
		||||
PILLAR_SERVER_ENDPOINT = 'http://localhost/api/'
 | 
			
		||||
 | 
			
		||||
MAIN_PROJECT_ID = '5672beecc0261b2005ed1a33'
 | 
			
		||||
 | 
			
		||||
@@ -44,5 +44,3 @@ ELASTIC_INDICES = {
 | 
			
		||||
 | 
			
		||||
# MUST be 8 characters long, see pillar.flask_extra.HashedPathConverter
 | 
			
		||||
STATIC_FILE_HASH = 'abcd1234'
 | 
			
		||||
 | 
			
		||||
CACHE_NO_NULL_WARNING = True
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
from pillar.api.eve_settings import *
 | 
			
		||||
 | 
			
		||||
MONGO_DBNAME = 'pillar_test'
 | 
			
		||||
MONGO_USERNAME = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def override_eve():
 | 
			
		||||
@@ -11,7 +10,5 @@ def override_eve():
 | 
			
		||||
    test_settings.MONGO_HOST = MONGO_HOST
 | 
			
		||||
    test_settings.MONGO_PORT = MONGO_PORT
 | 
			
		||||
    test_settings.MONGO_DBNAME = MONGO_DBNAME
 | 
			
		||||
    test_settings.MONGO1_USERNAME = MONGO_USERNAME
 | 
			
		||||
    tests.MONGO_HOST = MONGO_HOST
 | 
			
		||||
    tests.MONGO_DBNAME = MONGO_DBNAME
 | 
			
		||||
    tests.MONGO_USERNAME = MONGO_USERNAME
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ def attachment_form_group_create(schema_prop):
 | 
			
		||||
def _attachment_build_single_field(schema_prop):
 | 
			
		||||
    # Ugly hard-coded schema.
 | 
			
		||||
    fake_schema = {
 | 
			
		||||
        'slug': schema_prop['keyschema'],
 | 
			
		||||
        'slug': schema_prop['propertyschema'],
 | 
			
		||||
        'oid': schema_prop['valueschema']['schema']['oid'],
 | 
			
		||||
    }
 | 
			
		||||
    file_select_form_group = build_file_select_form(fake_schema)
 | 
			
		||||
 
 | 
			
		||||
@@ -94,16 +94,6 @@ def find_for_post(project, node):
 | 
			
		||||
                   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):
 | 
			
		||||
    """Fallback: Assets, textures, and other node types.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import typing
 | 
			
		||||
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from datetime import date
 | 
			
		||||
import pillarsdk
 | 
			
		||||
from flask import current_app
 | 
			
		||||
from flask_wtf import FlaskForm
 | 
			
		||||
from wtforms import StringField
 | 
			
		||||
from wtforms import DateField
 | 
			
		||||
@@ -18,8 +17,6 @@ from wtforms import DateTimeField
 | 
			
		||||
from wtforms import SelectMultipleField
 | 
			
		||||
from wtforms import FieldList
 | 
			
		||||
from wtforms.validators import DataRequired
 | 
			
		||||
 | 
			
		||||
from pillar import current_app
 | 
			
		||||
from pillar.web.utils import system_util
 | 
			
		||||
from pillar.web.utils.forms import FileSelectField
 | 
			
		||||
from pillar.web.utils.forms import CustomFormField
 | 
			
		||||
@@ -47,13 +44,6 @@ def iter_node_properties(node_type):
 | 
			
		||||
        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):
 | 
			
		||||
    """Add fields to a form based on the node and form schema provided.
 | 
			
		||||
    :type node_schema: dict
 | 
			
		||||
@@ -70,9 +60,7 @@ def add_form_properties(form_class, node_type):
 | 
			
		||||
        # Recursive call if detects a dict
 | 
			
		||||
        field_type = schema_prop['type']
 | 
			
		||||
 | 
			
		||||
        if prop_name == 'tags' and field_type == 'list':
 | 
			
		||||
            field = SelectMultipleField(choices=tag_choices())
 | 
			
		||||
        elif field_type == 'dict':
 | 
			
		||||
        if field_type == 'dict':
 | 
			
		||||
            assert prop_name == 'attachments'
 | 
			
		||||
            field = attachments.attachment_form_group_create(schema_prop)
 | 
			
		||||
        elif field_type == 'list':
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ from pillar import current_app
 | 
			
		||||
from pillar.api.utils import utcnow
 | 
			
		||||
from pillar.web import system_util
 | 
			
		||||
from pillar.web import utils
 | 
			
		||||
from pillar.web.nodes import finders
 | 
			
		||||
from pillar.web.utils.jstree import jstree_get_children
 | 
			
		||||
import pillar.extension
 | 
			
		||||
 | 
			
		||||
@@ -303,52 +302,6 @@ def view(project_url):
 | 
			
		||||
                                         '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})
 | 
			
		||||
 | 
			
		||||
    # 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})
 | 
			
		||||
 | 
			
		||||
    return links
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def render_project(project, api, extra_context=None, template_name=None):
 | 
			
		||||
    project.picture_square = utils.get_file(project.picture_square, api=api)
 | 
			
		||||
    project.picture_header = utils.get_file(project.picture_header, api=api)
 | 
			
		||||
@@ -417,8 +370,6 @@ def render_project(project, api, extra_context=None, template_name=None):
 | 
			
		||||
 | 
			
		||||
    extension_sidebar_links = current_app.extension_sidebar_links(project)
 | 
			
		||||
 | 
			
		||||
    navigation_links = project_navigation_links(project, api)
 | 
			
		||||
 | 
			
		||||
    return render_template(template_name,
 | 
			
		||||
                           api=api,
 | 
			
		||||
                           project=project,
 | 
			
		||||
@@ -427,7 +378,6 @@ def render_project(project, api, extra_context=None, template_name=None):
 | 
			
		||||
                           show_project=True,
 | 
			
		||||
                           og_picture=project.picture_header,
 | 
			
		||||
                           activity_stream=activity_stream,
 | 
			
		||||
                           navigation_links=navigation_links,
 | 
			
		||||
                           extension_sidebar_links=extension_sidebar_links,
 | 
			
		||||
                           **extra_context)
 | 
			
		||||
 | 
			
		||||
@@ -497,14 +447,16 @@ def view_node(project_url, node_id):
 | 
			
		||||
 | 
			
		||||
    # Append _theatre to load the proper template
 | 
			
		||||
    theatre = '_theatre' if theatre_mode else ''
 | 
			
		||||
    navigation_links = project_navigation_links(project, api)
 | 
			
		||||
 | 
			
		||||
    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',
 | 
			
		||||
                               api=api,
 | 
			
		||||
                               node=node,
 | 
			
		||||
                               project=project,
 | 
			
		||||
                               navigation_links=navigation_links,
 | 
			
		||||
                               pages=pages._items,
 | 
			
		||||
                               og_picture=og_picture,)
 | 
			
		||||
 | 
			
		||||
    extension_sidebar_links = current_app.extension_sidebar_links(project)
 | 
			
		||||
@@ -516,7 +468,6 @@ def view_node(project_url, node_id):
 | 
			
		||||
                           show_node=True,
 | 
			
		||||
                           show_project=False,
 | 
			
		||||
                           og_picture=og_picture,
 | 
			
		||||
                           navigation_links=navigation_links,
 | 
			
		||||
                           extension_sidebar_links=extension_sidebar_links)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -872,6 +872,12 @@
 | 
			
		||||
      "code": 61930,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "31972e4e9d080eaa796290349ae6c1fd",
 | 
			
		||||
      "css": "users",
 | 
			
		||||
      "code": 59502,
 | 
			
		||||
      "src": "fontawesome"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "uid": "c8585e1e5b0467f28b70bce765d5840c",
 | 
			
		||||
      "css": "clipboard-copy",
 | 
			
		||||
@@ -984,30 +990,6 @@
 | 
			
		||||
      "code": 59394,
 | 
			
		||||
      "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",
 | 
			
		||||
      "css": "log-in",
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										8
									
								
								pillar/web/static/assets/js/vendor/videojs-6.2.8.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								pillar/web/static/assets/js/vendor/videojs-6.2.8.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,109 +0,0 @@
 | 
			
		||||
(function(vjs) {
 | 
			
		||||
"use strict";  
 | 
			
		||||
  var
 | 
			
		||||
  extend = function(obj) {
 | 
			
		||||
    var arg, i, k;
 | 
			
		||||
    for (i = 1; i < arguments.length; i++) {
 | 
			
		||||
      arg = arguments[i];
 | 
			
		||||
      for (k in arg) {
 | 
			
		||||
        if (arg.hasOwnProperty(k)) {
 | 
			
		||||
          obj[k] = arg[k];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return obj;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  defaults = {
 | 
			
		||||
    count: 10,
 | 
			
		||||
    counter: "counter",
 | 
			
		||||
    countdown: "countdown",
 | 
			
		||||
    countdown_text: "Next video in:",
 | 
			
		||||
    endcard: "player-endcard",
 | 
			
		||||
    related: "related-content",
 | 
			
		||||
    next: "next-video",
 | 
			
		||||
    getRelatedContent: function(callback){callback();},
 | 
			
		||||
    getNextVid: function(callback){callback();}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  endcard = function(options) {
 | 
			
		||||
    var player = this;
 | 
			
		||||
    var el = this.el();
 | 
			
		||||
    var settings = extend({}, defaults, options || {});
 | 
			
		||||
 | 
			
		||||
    // set background
 | 
			
		||||
    var card = document.createElement('div');
 | 
			
		||||
    card.id = settings.endcard;
 | 
			
		||||
    card.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
    el.appendChild(card);
 | 
			
		||||
 | 
			
		||||
    settings.getRelatedContent(function(content) {
 | 
			
		||||
      if (content instanceof Array) {
 | 
			
		||||
        var related_content_div = document.createElement('div');
 | 
			
		||||
        related_content_div.id = settings.related;
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < content.length; i++) {
 | 
			
		||||
          related_content_div.appendChild(content[i]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        card.appendChild(related_content_div);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        throw new TypeError("options.getRelatedContent must return an array");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    settings.getNextVid(function(next) {
 | 
			
		||||
      if (typeof next !== "undefined") {
 | 
			
		||||
        var next_div = document.createElement('div');
 | 
			
		||||
        var counter = document.createElement('span');
 | 
			
		||||
        var countdown = document.createElement('div');
 | 
			
		||||
        counter.id = settings.counter;
 | 
			
		||||
        countdown.id = settings.countdown;
 | 
			
		||||
        next_div.id = settings.next;
 | 
			
		||||
 | 
			
		||||
        countdown.innerHTML = settings.countdown_text;
 | 
			
		||||
        countdown.appendChild(counter);
 | 
			
		||||
        next_div.appendChild(countdown);
 | 
			
		||||
        next_div.appendChild(next);
 | 
			
		||||
 | 
			
		||||
        card.appendChild(next_div);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    var counter_started = 0;
 | 
			
		||||
    player.on('ended', function() {
 | 
			
		||||
      card.style.display = 'block';
 | 
			
		||||
      var next = document.getElementById(settings.next);
 | 
			
		||||
      if (next !== null) {
 | 
			
		||||
        var href = next.getElementsByTagName("a")[0].href;
 | 
			
		||||
        var count = settings.count;
 | 
			
		||||
        counter.innerHTML = count;
 | 
			
		||||
 | 
			
		||||
        var interval = setInterval(function(){
 | 
			
		||||
          count--;
 | 
			
		||||
          if (count <= 0) {
 | 
			
		||||
            clearInterval(interval);
 | 
			
		||||
            window.location = href;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          counter.innerHTML = count;
 | 
			
		||||
        }, 1000);
 | 
			
		||||
      }
 | 
			
		||||
      if (counter_started === 0) {
 | 
			
		||||
        counter_started++;
 | 
			
		||||
        player.on('playing', function() {
 | 
			
		||||
          card.style.display = 'none';
 | 
			
		||||
          clearInterval(interval);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  vjs.plugin('endcard', endcard);
 | 
			
		||||
    
 | 
			
		||||
})(window.videojs);
 | 
			
		||||
@@ -33,8 +33,7 @@ def get_user_info(user_id):
 | 
			
		||||
    # TODO: put those fields into a config var or module-level global.
 | 
			
		||||
    return {'email': user.email,
 | 
			
		||||
            'full_name': user.full_name,
 | 
			
		||||
            'username': user.username,
 | 
			
		||||
            'badges_html': (user.badges and user.badges.html) or ''}
 | 
			
		||||
            'username': user.username}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup_app(app):
 | 
			
		||||
 
 | 
			
		||||
@@ -48,10 +48,6 @@ def oauth_authorize(provider):
 | 
			
		||||
 | 
			
		||||
@blueprint.route('/oauth/<provider>/authorized')
 | 
			
		||||
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:
 | 
			
		||||
        return redirect(url_for('main.homepage'))
 | 
			
		||||
 | 
			
		||||
@@ -69,16 +65,6 @@ def oauth_callback(provider):
 | 
			
		||||
    user_info = {'id': oauth_user.id, 'email': oauth_user.email, 'full_name': ''}
 | 
			
		||||
    db_user = find_user_in_db(user_info, provider=provider)
 | 
			
		||||
    db_id, status = upsert_user(db_user)
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ def jstree_get_children(node_id, project_id=None):
 | 
			
		||||
        'where': {
 | 
			
		||||
            '$and': [
 | 
			
		||||
                {'node_type': {'$regex': '^(?!attract_)'}},
 | 
			
		||||
                {'node_type': {'$not': {'$in': ['comment', 'post', 'blog', 'page']}}},
 | 
			
		||||
                {'node_type': {'$not': {'$in': ['comment', 'post']}}},
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,16 @@ algoliasearch==1.12.0
 | 
			
		||||
bcrypt==3.1.3
 | 
			
		||||
blinker==1.4
 | 
			
		||||
bleach==2.1.3
 | 
			
		||||
celery[redis]==4.2.1
 | 
			
		||||
celery[redis]==4.0.2
 | 
			
		||||
CommonMark==0.7.2
 | 
			
		||||
elasticsearch==6.1.1
 | 
			
		||||
elasticsearch-dsl==6.1.0
 | 
			
		||||
Eve==0.8
 | 
			
		||||
Flask==1.0.2
 | 
			
		||||
Eve==0.7.3
 | 
			
		||||
Flask==0.12
 | 
			
		||||
Flask-Babel==0.11.2
 | 
			
		||||
Flask-Caching==1.4.0
 | 
			
		||||
Flask-Cache==0.13.1
 | 
			
		||||
Flask-Script==2.0.6
 | 
			
		||||
Flask-Login==0.4.1
 | 
			
		||||
Flask-Login==0.3.2
 | 
			
		||||
Flask-WTF==0.14.2
 | 
			
		||||
gcloud==0.12.0
 | 
			
		||||
google-apitools==0.4.11
 | 
			
		||||
@@ -27,49 +27,37 @@ Pillow==4.1.1
 | 
			
		||||
python-dateutil==2.5.3
 | 
			
		||||
rauth==0.7.3
 | 
			
		||||
raven[flask]==6.3.0
 | 
			
		||||
requests==2.13.0
 | 
			
		||||
redis==2.10.5
 | 
			
		||||
shortcodes==2.5.0
 | 
			
		||||
WebOb==1.5.0
 | 
			
		||||
wheel==0.29.0
 | 
			
		||||
zencoder==0.6.5
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Secondary requirements
 | 
			
		||||
amqp==2.3.2
 | 
			
		||||
asn1crypto==0.24.0
 | 
			
		||||
Babel==2.6.0
 | 
			
		||||
billiard==3.5.0.4
 | 
			
		||||
Cerberus==1.2
 | 
			
		||||
cffi==1.10.0
 | 
			
		||||
click==6.7
 | 
			
		||||
cryptography==2.0.3
 | 
			
		||||
Events==0.3
 | 
			
		||||
future==0.16.0
 | 
			
		||||
googleapis-common-protos==1.5.3
 | 
			
		||||
html5lib==1.0.1
 | 
			
		||||
idna==2.5
 | 
			
		||||
ipaddress==1.0.22
 | 
			
		||||
amqp==2.1.4
 | 
			
		||||
billiard==3.5.0.2
 | 
			
		||||
Flask-PyMongo==0.4.1
 | 
			
		||||
-e git+https://github.com/armadillica/cerberus.git@sybren-0.9#egg=Cerberus
 | 
			
		||||
Events==0.2.2
 | 
			
		||||
future==0.15.2
 | 
			
		||||
html5lib==0.99999999
 | 
			
		||||
googleapis-common-protos==1.1.0
 | 
			
		||||
itsdangerous==0.24
 | 
			
		||||
Jinja2==2.10
 | 
			
		||||
kombu==4.2.1
 | 
			
		||||
oauth2client==4.1.2
 | 
			
		||||
oauthlib==2.1.0
 | 
			
		||||
olefile==0.45.1
 | 
			
		||||
protobuf==3.6.0
 | 
			
		||||
protorpc==0.12.0
 | 
			
		||||
pyasn1==0.4.4
 | 
			
		||||
pyasn1-modules==0.2.2
 | 
			
		||||
pycparser==2.17
 | 
			
		||||
pymongo==3.7.0
 | 
			
		||||
pyOpenSSL==16.2.0
 | 
			
		||||
pytz==2018.5
 | 
			
		||||
requests-oauthlib==1.0.0
 | 
			
		||||
Jinja2==2.9.6
 | 
			
		||||
kombu==4.0.2
 | 
			
		||||
oauth2client==2.0.2
 | 
			
		||||
oauthlib==2.0.1
 | 
			
		||||
olefile==0.44
 | 
			
		||||
protobuf==3.0.0b2.post2
 | 
			
		||||
protorpc==0.11.1
 | 
			
		||||
pyasn1-modules==0.0.8
 | 
			
		||||
pymongo==3.4.0
 | 
			
		||||
pytz==2017.2
 | 
			
		||||
requests-oauthlib==0.7.0
 | 
			
		||||
rsa==3.4.2
 | 
			
		||||
simplejson==3.16.0
 | 
			
		||||
simplejson==3.10.0
 | 
			
		||||
six==1.10.0
 | 
			
		||||
urllib3==1.22
 | 
			
		||||
vine==1.1.4
 | 
			
		||||
webencodings==0.5.1
 | 
			
		||||
Werkzeug==0.14.1
 | 
			
		||||
WTForms==2.2.1
 | 
			
		||||
vine==1.1.3
 | 
			
		||||
WTForms==2.1
 | 
			
		||||
Werkzeug==0.11.15
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							@@ -35,7 +35,7 @@ setuptools.setup(
 | 
			
		||||
    install_requires=[
 | 
			
		||||
        'Flask>=0.12',
 | 
			
		||||
        'Eve>=0.7.3',
 | 
			
		||||
        'Flask-Caching>=1.4.0',
 | 
			
		||||
        'Flask-Cache>=0.13.1',
 | 
			
		||||
        'Flask-Script>=2.0.5',
 | 
			
		||||
        'Flask-Login>=0.3.2',
 | 
			
		||||
        'Flask-OAuthlib>=0.9.3',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1622
									
								
								src/scripts/markdown/01_markdown-converter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1622
									
								
								src/scripts/markdown/01_markdown-converter.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										116
									
								
								src/scripts/markdown/02_markdown-sanitizer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/scripts/markdown/02_markdown-sanitizer.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
(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;
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										2295
									
								
								src/scripts/markdown/03_showdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2295
									
								
								src/scripts/markdown/03_showdown.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										874
									
								
								src/scripts/markdown/04_pagedown-extra.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										874
									
								
								src/scripts/markdown/04_pagedown-extra.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,874 @@
 | 
			
		||||
(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,13 +64,4 @@
 | 
			
		||||
		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));
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ var DocumentTitleAPI = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Status Bar * DEPRECATED * USE TOASTR INSTEAD */
 | 
			
		||||
/* Status Bar */
 | 
			
		||||
function statusBarClear(delay_class, delay_html){
 | 
			
		||||
	var statusBar = $("#status-bar");
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +54,6 @@ function statusBarClear(delay_class, delay_html){
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Status Bar * DEPRECATED - USE TOASTR INSTEAD * */
 | 
			
		||||
function statusBarSet(classes, html, icon_name, time){
 | 
			
		||||
	/* Utility to notify the user by temporarily flashing text on the project header
 | 
			
		||||
		 Usage:
 | 
			
		||||
 
 | 
			
		||||
@@ -66,9 +66,12 @@ function containerResizeY(window_height){
 | 
			
		||||
 | 
			
		||||
	var project_container = document.getElementById('project-container');
 | 
			
		||||
	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_wheader = window_height - container_offset;
 | 
			
		||||
	var window_height_minus_nav = window_height - container_offset;
 | 
			
		||||
	var container_height_wheader = window_height - container_offset.top - nav_header_height;
 | 
			
		||||
	var window_height_minus_nav = window_height - nav_header_height - 1; // 1 is border width
 | 
			
		||||
 | 
			
		||||
	$('#project_context-header').width($('#project_context-container').width());
 | 
			
		||||
 | 
			
		||||
	if ($(window).width() > 768) {
 | 
			
		||||
		$('#project-container').css(
 | 
			
		||||
@@ -76,14 +79,13 @@ function containerResizeY(window_height){
 | 
			
		||||
			 'height': window_height_minus_nav + 'px'}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		$('#project_nav-container, #project_tree').css(
 | 
			
		||||
			{'max-height': (window_height_minus_nav) + 'px',
 | 
			
		||||
			 'height': (window_height_minus_nav) + 'px'}
 | 
			
		||||
		$('#project_nav-container, #project_tree, .project_split').css(
 | 
			
		||||
			{'max-height': (window_height_minus_nav - 50) + 'px',
 | 
			
		||||
			 'height': (window_height_minus_nav - 50) + 'px'}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (container_height > parseInt($('#project-container').css("min-height"))) {
 | 
			
		||||
			if (typeof projectTree !== "undefined"){
 | 
			
		||||
 | 
			
		||||
				$(projectTree).css(
 | 
			
		||||
					{'max-height': container_height_wheader + 'px',
 | 
			
		||||
					 'height': container_height_wheader + 'px'}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,202 +0,0 @@
 | 
			
		||||
/* 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,17 +143,12 @@ nav.sidebar
 | 
			
		||||
	left: 0
 | 
			
		||||
	width: $sidebar-width
 | 
			
		||||
	height: 100%
 | 
			
		||||
	background-color: $color-background-nav
 | 
			
		||||
	display: flex
 | 
			
		||||
	flex-direction: column
 | 
			
		||||
 | 
			
		||||
	> ul > li > .navbar-item
 | 
			
		||||
		padding-top: 10px
 | 
			
		||||
		padding-bottom: 10px
 | 
			
		||||
		background: red
 | 
			
		||||
 | 
			
		||||
	.dropdown
 | 
			
		||||
		min-width: $sidebar-width
 | 
			
		||||
 | 
			
		||||
		.dropdown-menu
 | 
			
		||||
			top: initial
 | 
			
		||||
			bottom: 3px
 | 
			
		||||
@@ -164,7 +159,7 @@ nav.sidebar
 | 
			
		||||
			li a
 | 
			
		||||
				justify-content: flex-start
 | 
			
		||||
 | 
			
		||||
	> ul
 | 
			
		||||
	ul
 | 
			
		||||
		width: 100%
 | 
			
		||||
		margin: 0
 | 
			
		||||
		padding: 0
 | 
			
		||||
@@ -177,11 +172,25 @@ nav.sidebar
 | 
			
		||||
 | 
			
		||||
			a.navbar-item, button
 | 
			
		||||
				display: flex
 | 
			
		||||
				color: $color-text-light-hint
 | 
			
		||||
				font-size: 1.5em
 | 
			
		||||
				align-items: center
 | 
			
		||||
				justify-content: center
 | 
			
		||||
				padding: 10px 0
 | 
			
		||||
				background: transparent
 | 
			
		||||
				border: none
 | 
			
		||||
				width: 100%
 | 
			
		||||
				text-decoration: none
 | 
			
		||||
 | 
			
		||||
				&:hover
 | 
			
		||||
					color: $color-text-light-primary
 | 
			
		||||
				&:active
 | 
			
		||||
					outline: none
 | 
			
		||||
 | 
			
		||||
				&.cloud
 | 
			
		||||
					i
 | 
			
		||||
						position: relative
 | 
			
		||||
						left: -4px
 | 
			
		||||
 | 
			
		||||
			a.dropdown-toggle
 | 
			
		||||
				padding: 0
 | 
			
		||||
@@ -399,57 +408,3 @@ nav.sidebar
 | 
			
		||||
			top: -1px
 | 
			
		||||
			left: -19px
 | 
			
		||||
			z-index: 1
 | 
			
		||||
 | 
			
		||||
$loader-bar-width: 100px
 | 
			
		||||
$loader-bar-height: 2px
 | 
			
		||||
.loader-bar
 | 
			
		||||
	background-color: $color-background
 | 
			
		||||
	bottom: 0
 | 
			
		||||
	content: ''
 | 
			
		||||
	display: none
 | 
			
		||||
	height: 0
 | 
			
		||||
	overflow: hidden
 | 
			
		||||
	position: absolute
 | 
			
		||||
	visibility: hidden
 | 
			
		||||
	width: 100%
 | 
			
		||||
 | 
			
		||||
	&:before
 | 
			
		||||
		animation: none
 | 
			
		||||
		background-color: $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
 | 
			
		||||
 
 | 
			
		||||
@@ -453,17 +453,12 @@ $comments-width-max: 710px
 | 
			
		||||
		transition: background-color 150ms ease-in-out, color 150ms ease-in-out
 | 
			
		||||
		width: 100px
 | 
			
		||||
 | 
			
		||||
		// The actual button for submitting the comment.
 | 
			
		||||
		button.comment-action-submit
 | 
			
		||||
			align-items: center
 | 
			
		||||
			background: transparent
 | 
			
		||||
			border: none
 | 
			
		||||
			border-top-left-radius: 0
 | 
			
		||||
			border-bottom-left-radius: 0
 | 
			
		||||
			color: $color-success
 | 
			
		||||
			cursor: pointer
 | 
			
		||||
			display: flex
 | 
			
		||||
			justify-content: center
 | 
			
		||||
			flex-direction: column
 | 
			
		||||
			height: 100%
 | 
			
		||||
			position: relative
 | 
			
		||||
@@ -471,12 +466,8 @@ $comments-width-max: 710px
 | 
			
		||||
			white-space: nowrap
 | 
			
		||||
			width: 100%
 | 
			
		||||
 | 
			
		||||
			&:hover
 | 
			
		||||
				background: rgba($color-success, .1)
 | 
			
		||||
 | 
			
		||||
			&:focus
 | 
			
		||||
				background: lighten($color-success, 10%)
 | 
			
		||||
				color: $white
 | 
			
		||||
 | 
			
		||||
			&.submitting
 | 
			
		||||
				color: $color-info
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,9 @@ $color-background-active-dark: hsl(hue($color-background-active), 50%, 50%) !def
 | 
			
		||||
$font-body: 'Roboto' !default
 | 
			
		||||
$font-headings: 'Lato' !default
 | 
			
		||||
$font-size: 14px !default
 | 
			
		||||
$font-size-xs: .75rem
 | 
			
		||||
$font-size-xxs: .65rem
 | 
			
		||||
 | 
			
		||||
$color-text: #4d4e53 !default
 | 
			
		||||
 | 
			
		||||
$color-text-dark: $color-text !default
 | 
			
		||||
$color-text-dark-primary: #646469 !default
 | 
			
		||||
$color-text-dark-secondary: #9E9FA2 !default
 | 
			
		||||
@@ -26,7 +25,7 @@ $color-text-light-primary: rgba($color-text-light, .87) !default
 | 
			
		||||
$color-text-light-secondary: rgba($color-text-light, .54) !default
 | 
			
		||||
$color-text-light-hint: rgba($color-text-light, .38) !default
 | 
			
		||||
 | 
			
		||||
$color-primary: #009eff !default
 | 
			
		||||
$color-primary: #68B3C8 !default
 | 
			
		||||
$color-primary-light: hsl(hue($color-primary), 30%, 90%) !default
 | 
			
		||||
$color-primary-dark: hsl(hue($color-primary), 80%, 30%) !default
 | 
			
		||||
$color-primary-accent:  hsl(hue($color-primary), 100%, 50%) !default
 | 
			
		||||
@@ -97,16 +96,16 @@ $screen-xs-max: $screen-sm-min - 1 !default
 | 
			
		||||
$screen-sm-max: $screen-md-min - 1 !default
 | 
			
		||||
$screen-md-max: $screen-lg-min - 1 !default
 | 
			
		||||
 | 
			
		||||
$sidebar-width: 40px !default
 | 
			
		||||
$sidebar-width: 50px !default
 | 
			
		||||
 | 
			
		||||
/* Project specifics */
 | 
			
		||||
$project_nav-width: 275px !default
 | 
			
		||||
$project_nav-width-lg: $project_nav-width * 1.2 !default
 | 
			
		||||
$project_nav-width-md: $project_nav-width
 | 
			
		||||
$project_nav-width-sm: $project_nav-width * 0.8 !default
 | 
			
		||||
$project_nav-width-xs: 100% !default
 | 
			
		||||
$project-sidebar-width: 40px !default
 | 
			
		||||
$project_header-height: 40px !default
 | 
			
		||||
$project_nav-width: 250px !default
 | 
			
		||||
$project-sidebar-width: 50px !default
 | 
			
		||||
$project_header-height: 50px !default
 | 
			
		||||
$project_footer-height: 30px !default
 | 
			
		||||
 | 
			
		||||
$navbar-height: 50px !default
 | 
			
		||||
$navbar-backdrop-height: 600px !default
 | 
			
		||||
 | 
			
		||||
$node-type-asset_image: #e87d86 !default
 | 
			
		||||
$node-type-asset_file: #CC91C7 !default
 | 
			
		||||
@@ -126,33 +125,3 @@ $z-index-base: 13 !default
 | 
			
		||||
 | 
			
		||||
	@media (min-width: $screen-lg-min)
 | 
			
		||||
		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
 | 
			
		||||
 
 | 
			
		||||
@@ -60,13 +60,14 @@
 | 
			
		||||
 | 
			
		||||
#node-overlay
 | 
			
		||||
	#error-container
 | 
			
		||||
		align-items: flex-start
 | 
			
		||||
		position: fixed
 | 
			
		||||
		top: $nav-link-height
 | 
			
		||||
		top: $navbar-height
 | 
			
		||||
		align-items: flex-start
 | 
			
		||||
 | 
			
		||||
		#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-right-radius: 0
 | 
			
		||||
			box-shadow: 0 0 25px rgba(black, .1), 0 0 50px rgba(black, .1)
 | 
			
		||||
			position: relative
 | 
			
		||||
			width: 100%
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
		color: $color-primary
 | 
			
		||||
		cursor: pointer
 | 
			
		||||
		float: right
 | 
			
		||||
		font-family: $font-body
 | 
			
		||||
		height: initial
 | 
			
		||||
		margin: 0
 | 
			
		||||
		padding: 8px 10px 0 10px
 | 
			
		||||
@@ -45,10 +46,10 @@
 | 
			
		||||
		border-color: transparent transparent $color-background transparent
 | 
			
		||||
		border-style: solid
 | 
			
		||||
		border-width: 0 8px 8px 8px
 | 
			
		||||
		bottom: -10px
 | 
			
		||||
		bottom: -15px
 | 
			
		||||
		height: 0
 | 
			
		||||
		position: absolute
 | 
			
		||||
		right: 7px
 | 
			
		||||
		right: 22px
 | 
			
		||||
		visibility: hidden
 | 
			
		||||
		width: 0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,20 @@
 | 
			
		||||
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
 | 
			
		||||
		.organization
 | 
			
		||||
			label
 | 
			
		||||
 
 | 
			
		||||
@@ -409,6 +409,7 @@ a.page-card-cta
 | 
			
		||||
		display: block
 | 
			
		||||
		+position-center-translate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		+media-xs
 | 
			
		||||
			display: none
 | 
			
		||||
		+media-sm
 | 
			
		||||
@@ -418,6 +419,9 @@ a.page-card-cta
 | 
			
		||||
		+media-lg
 | 
			
		||||
			width: 100%
 | 
			
		||||
 | 
			
		||||
.services.navbar-backdrop-overlay
 | 
			
		||||
	background: rgba(black, .5)
 | 
			
		||||
 | 
			
		||||
.services
 | 
			
		||||
	.page-card-side
 | 
			
		||||
		max-width: 500px
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,22 @@
 | 
			
		||||
 | 
			
		||||
.dashboard-container
 | 
			
		||||
	section#home,
 | 
			
		||||
	section#projects
 | 
			
		||||
		background-color: $color-background
 | 
			
		||||
		border-bottom-left-radius: 3px
 | 
			
		||||
		border-bottom-right-radius: 3px
 | 
			
		||||
 | 
			
		||||
		nav#sub-nav-tabs.home,
 | 
			
		||||
		nav#sub-nav-tabs.projects
 | 
			
		||||
			background-color: white
 | 
			
		||||
			border-bottom: thin solid $color-background-dark
 | 
			
		||||
 | 
			
		||||
			li.nav-tabs__list-tab
 | 
			
		||||
				padding: 15px 20px 10px 20px
 | 
			
		||||
 | 
			
		||||
	section#home
 | 
			
		||||
		background-color: $color-background-dark
 | 
			
		||||
 | 
			
		||||
	nav.nav-tabs__tab
 | 
			
		||||
		display: none
 | 
			
		||||
		background-color: $color-background-light
 | 
			
		||||
@@ -281,8 +287,9 @@
 | 
			
		||||
				flex-direction: column
 | 
			
		||||
 | 
			
		||||
				.title
 | 
			
		||||
					color: $color-text-dark-primary
 | 
			
		||||
					font-size: 1.2em
 | 
			
		||||
					padding-bottom: 2px
 | 
			
		||||
					color: $color-text-dark-primary
 | 
			
		||||
 | 
			
		||||
				ul.meta
 | 
			
		||||
					font-size: .9em
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,19 @@ ul.sharing-users-list
 | 
			
		||||
					&:hover
 | 
			
		||||
						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
 | 
			
		||||
	.disabled
 | 
			
		||||
@@ -149,26 +162,24 @@ ul.list-generic
 | 
			
		||||
	list-style: none
 | 
			
		||||
 | 
			
		||||
	> li
 | 
			
		||||
		padding: 5px 0
 | 
			
		||||
		display: flex
 | 
			
		||||
		align-items: center
 | 
			
		||||
		border-top: thin solid $color-background
 | 
			
		||||
		display: flex
 | 
			
		||||
		padding: 5px 0
 | 
			
		||||
 | 
			
		||||
		&:first-child
 | 
			
		||||
			border-top: none
 | 
			
		||||
 | 
			
		||||
		&:hover .item a
 | 
			
		||||
			color: $primary
 | 
			
		||||
			color: $color-primary
 | 
			
		||||
 | 
			
		||||
		a
 | 
			
		||||
			flex: 1
 | 
			
		||||
 | 
			
		||||
			&.active
 | 
			
		||||
				color: $primary !important
 | 
			
		||||
				font-weight: bold
 | 
			
		||||
 | 
			
		||||
		.actions
 | 
			
		||||
			margin-left: auto
 | 
			
		||||
			.btn
 | 
			
		||||
				font-size: .7em
 | 
			
		||||
 | 
			
		||||
			span
 | 
			
		||||
				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
 | 
			
		||||
				font-weight: 400
 | 
			
		||||
				padding-top: 8px
 | 
			
		||||
				color: $primary
 | 
			
		||||
				color: $color-primary-dark
 | 
			
		||||
 | 
			
		||||
	.search-hit
 | 
			
		||||
		padding: 0
 | 
			
		||||
@@ -29,13 +29,14 @@ $search-hit-width_grid: 100px
 | 
			
		||||
		font:
 | 
			
		||||
			size: .9em
 | 
			
		||||
			weight: 400
 | 
			
		||||
			family: $font-body
 | 
			
		||||
			style: initial
 | 
			
		||||
		width: 100%
 | 
			
		||||
		+text-overflow-ellipsis
 | 
			
		||||
		+clearfix
 | 
			
		||||
 | 
			
		||||
		& em
 | 
			
		||||
			color: $primary
 | 
			
		||||
			color: $color-primary-dark
 | 
			
		||||
			font-style: normal
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
@@ -70,7 +71,7 @@ $search-hit-width_grid: 100px
 | 
			
		||||
		min-width: 350px
 | 
			
		||||
		border-bottom-left-radius: 3px
 | 
			
		||||
		border-bottom-right-radius: 3px
 | 
			
		||||
		border-top: 3px solid lighten($primary, 5%)
 | 
			
		||||
		border-top: 3px solid lighten($color-primary, 5%)
 | 
			
		||||
		overflow: hidden
 | 
			
		||||
 | 
			
		||||
		.tt-suggestion
 | 
			
		||||
@@ -92,37 +93,222 @@ $search-hit-width_grid: 100px
 | 
			
		||||
			&.tt-cursor:hover .search-hit
 | 
			
		||||
				background-color: lighten($color-background, 5%)
 | 
			
		||||
 | 
			
		||||
.search-list
 | 
			
		||||
	width: 30%
 | 
			
		||||
#search-container
 | 
			
		||||
	display: flex
 | 
			
		||||
	min-height: 600px
 | 
			
		||||
	background-color: white
 | 
			
		||||
 | 
			
		||||
	.card-deck.card-deck-horizontal
 | 
			
		||||
		.card .embed-responsive
 | 
			
		||||
			max-width: 80px
 | 
			
		||||
	+media-lg
 | 
			
		||||
		padding-left: 0
 | 
			
		||||
		padding-right: 0
 | 
			
		||||
 | 
			
		||||
	#search-sidebar
 | 
			
		||||
		width: 20%
 | 
			
		||||
		background-color: $color-background-light
 | 
			
		||||
 | 
			
		||||
		+media-lg
 | 
			
		||||
			border-top-left-radius: 3px
 | 
			
		||||
 | 
			
		||||
		input.search-field
 | 
			
		||||
			background-color: $color-background-nav-dark
 | 
			
		||||
			font-size: 1.1em
 | 
			
		||||
			color: white
 | 
			
		||||
			margin-bottom: 10px
 | 
			
		||||
			border: none
 | 
			
		||||
		border-bottom: 2px solid rgba($primary, .2)
 | 
			
		||||
			border-bottom: 2px solid rgba($color-primary, .2)
 | 
			
		||||
			border-radius: 0
 | 
			
		||||
			width: 100%
 | 
			
		||||
			padding: 5px 15px
 | 
			
		||||
			height: 50px
 | 
			
		||||
			transition: border 100ms ease-in-out
 | 
			
		||||
 | 
			
		||||
			&::placeholder
 | 
			
		||||
				color: $color-text-dark-secondary
 | 
			
		||||
			&:placeholder-shown
 | 
			
		||||
			border-bottom-color: $primary
 | 
			
		||||
				border-bottom-color: $color-primary
 | 
			
		||||
 | 
			
		||||
			&:focus
 | 
			
		||||
				outline: none
 | 
			
		||||
				border: none
 | 
			
		||||
			border-bottom: 2px solid lighten($primary, 5%)
 | 
			
		||||
				border-bottom: 2px solid lighten($color-primary, 5%)
 | 
			
		||||
 | 
			
		||||
.search-details
 | 
			
		||||
	width: 70%
 | 
			
		||||
		.search-list-filters
 | 
			
		||||
			padding:
 | 
			
		||||
				left: 10px
 | 
			
		||||
				right: 10px
 | 
			
		||||
 | 
			
		||||
#search-details
 | 
			
		||||
			.panel.panel-default
 | 
			
		||||
				margin-bottom: 10px
 | 
			
		||||
				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
 | 
			
		||||
 | 
			
		||||
		#hits
 | 
			
		||||
			position: relative
 | 
			
		||||
			width: 100%
 | 
			
		||||
 | 
			
		||||
		#no-hits
 | 
			
		||||
			padding: 10px 15px
 | 
			
		||||
			color: $color-text-dark-secondary
 | 
			
		||||
 | 
			
		||||
		.search-hit
 | 
			
		||||
			#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
 | 
			
		||||
@@ -136,7 +322,6 @@ $search-hit-width_grid: 100px
 | 
			
		||||
			color: $color-danger
 | 
			
		||||
			text-align: center
 | 
			
		||||
 | 
			
		||||
#search-container
 | 
			
		||||
	#node-container
 | 
			
		||||
		width: 100%
 | 
			
		||||
		max-width: 100%
 | 
			
		||||
@@ -231,7 +416,9 @@ $search-hit-width_grid: 100px
 | 
			
		||||
 | 
			
		||||
		&.texture
 | 
			
		||||
			.texture-title
 | 
			
		||||
				font-size: 2em
 | 
			
		||||
				font:
 | 
			
		||||
					size: 2em
 | 
			
		||||
					family: $font-body
 | 
			
		||||
				padding: 15px 10px 10px 15px
 | 
			
		||||
			.node-row
 | 
			
		||||
				background: white
 | 
			
		||||
@@ -289,118 +476,215 @@ $search-hit-width_grid: 100px
 | 
			
		||||
								button
 | 
			
		||||
									width: 100%
 | 
			
		||||
 | 
			
		||||
#project_sidebar+#search-sidebar,
 | 
			
		||||
#project_sidebar+#search-sidebar+#search-container
 | 
			
		||||
	padding-left: $sidebar-width
 | 
			
		||||
.search-hit
 | 
			
		||||
	float: left
 | 
			
		||||
	box-shadow: none
 | 
			
		||||
	border: thin solid transparent
 | 
			
		||||
	border-top-color: darken(white, 8%)
 | 
			
		||||
	border-left: 3px solid transparent
 | 
			
		||||
 | 
			
		||||
.search-project
 | 
			
		||||
	li.project
 | 
			
		||||
	color: $color-background-nav
 | 
			
		||||
 | 
			
		||||
	width: 100%
 | 
			
		||||
	position: relative
 | 
			
		||||
	margin: 0
 | 
			
		||||
	padding: 7px 10px 7px 10px
 | 
			
		||||
	+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
 | 
			
		||||
			cursor: pointer
 | 
			
		||||
			text-decoration: underline
 | 
			
		||||
 | 
			
		||||
		em
 | 
			
		||||
			color: darken($color-primary, 15%)
 | 
			
		||||
			font-style: normal
 | 
			
		||||
 | 
			
		||||
	.search-hit-ribbon
 | 
			
		||||
		+ribbon
 | 
			
		||||
		right: -30px
 | 
			
		||||
		top: 5px
 | 
			
		||||
 | 
			
		||||
		span
 | 
			
		||||
			font-size: 60%
 | 
			
		||||
			margin: 1px 0
 | 
			
		||||
			padding: 2px 35px
 | 
			
		||||
 | 
			
		||||
	.search-hit-meta
 | 
			
		||||
		position: relative
 | 
			
		||||
		font-size: .9em
 | 
			
		||||
		color: $color-text-dark-secondary
 | 
			
		||||
		background-color: initial
 | 
			
		||||
		padding: 3px 0 0 0
 | 
			
		||||
		text-decoration: none
 | 
			
		||||
		+text-overflow-ellipsis
 | 
			
		||||
 | 
			
		||||
		span
 | 
			
		||||
			&.project
 | 
			
		||||
				color: $color-text-dark-secondary
 | 
			
		||||
				margin-right: 3px
 | 
			
		||||
			&.updated
 | 
			
		||||
				color: $color-text-dark-hint
 | 
			
		||||
			&.status
 | 
			
		||||
				font-size: .8em
 | 
			
		||||
				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
 | 
			
		||||
				margin: 0 3px
 | 
			
		||||
				float: right
 | 
			
		||||
				display: block
 | 
			
		||||
				+media-lg
 | 
			
		||||
					display: block
 | 
			
		||||
				+media-md
 | 
			
		||||
					display: block
 | 
			
		||||
				+media-sm
 | 
			
		||||
					display: none
 | 
			
		||||
				+media-xs
 | 
			
		||||
					display: none
 | 
			
		||||
 | 
			
		||||
#search-sidebar
 | 
			
		||||
	.card
 | 
			
		||||
		margin-bottom: 10px
 | 
			
		||||
		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: $primary
 | 
			
		||||
 | 
			
		||||
		&.refined
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
	.card-title
 | 
			
		||||
		position: relative
 | 
			
		||||
		&:after
 | 
			
		||||
			content: '\e83b'
 | 
			
		||||
			font-family: 'pillar-font'
 | 
			
		||||
			position: absolute
 | 
			
		||||
			right: 0
 | 
			
		||||
			color: $color-text-dark-primary
 | 
			
		||||
 | 
			
		||||
	.collapsed
 | 
			
		||||
		.card-title:after
 | 
			
		||||
			content: '\e838'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	.search-list-stats
 | 
			
		||||
		color: $color-text-dark-hint
 | 
			
		||||
		padding: 10px 15px 0 15px
 | 
			
		||||
		text-align: center
 | 
			
		||||
		font-size: .9em
 | 
			
		||||
		+clearfix
 | 
			
		||||
 | 
			
		||||
.search-pagination
 | 
			
		||||
	text-align: center
 | 
			
		||||
	list-style-type: none
 | 
			
		||||
			&.context
 | 
			
		||||
				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
 | 
			
		||||
				float: right
 | 
			
		||||
				display: none
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
				color: $color-text-dark-primary
 | 
			
		||||
		cursor: pointer
 | 
			
		||||
		.search-hit-name-user
 | 
			
		||||
			color: $color-primary
 | 
			
		||||
 | 
			
		||||
		&.disabled
 | 
			
		||||
			opacity: .6
 | 
			
		||||
	&.users
 | 
			
		||||
		em
 | 
			
		||||
			font-style: normal
 | 
			
		||||
			color: $color-primary
 | 
			
		||||
 | 
			
		||||
		&.active a
 | 
			
		||||
			color: $color-text-dark-primary
 | 
			
		||||
			font-weight: bold
 | 
			
		||||
		.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
 | 
			
		||||
			margin-left: 15px
 | 
			
		||||
 | 
			
		||||
.view-grid
 | 
			
		||||
	display: flex
 | 
			
		||||
@@ -422,13 +706,13 @@ $search-hit-width_grid: 100px
 | 
			
		||||
		transition: border-color 150ms ease-in-out
 | 
			
		||||
 | 
			
		||||
		&.active
 | 
			
		||||
			background-color: $primary
 | 
			
		||||
			border-color: $primary
 | 
			
		||||
			background-color: $color-primary
 | 
			
		||||
			border-color: $color-primary
 | 
			
		||||
 | 
			
		||||
			.search-hit-name
 | 
			
		||||
				font-weight: 500
 | 
			
		||||
				color: white
 | 
			
		||||
				background-color: $primary
 | 
			
		||||
				background-color: $color-primary
 | 
			
		||||
 | 
			
		||||
		.search-hit-name
 | 
			
		||||
			font-size: .9em
 | 
			
		||||
@@ -492,5 +776,5 @@ $search-hit-width_grid: 100px
 | 
			
		||||
 | 
			
		||||
			&.active
 | 
			
		||||
				color: white
 | 
			
		||||
				background-color: $primary
 | 
			
		||||
				background-color: $color-primary
 | 
			
		||||
				border-color: transparent
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,13 @@
 | 
			
		||||
          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%
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@
 | 
			
		||||
	display: inline-flex
 | 
			
		||||
	align-items: center
 | 
			
		||||
	justify-content: center
 | 
			
		||||
	font-family: $font-body
 | 
			
		||||
	padding: 5px 12px
 | 
			
		||||
	border-radius: $roundness
 | 
			
		||||
 | 
			
		||||
@@ -82,15 +83,6 @@
 | 
			
		||||
			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)
 | 
			
		||||
	position: absolute
 | 
			
		||||
	top: 0
 | 
			
		||||
@@ -130,17 +122,24 @@
 | 
			
		||||
	transform: translate(-50%, -50%)
 | 
			
		||||
 | 
			
		||||
=input-generic
 | 
			
		||||
	// padding: 5px 5px 5px 0
 | 
			
		||||
	padding: 5px 5px 5px 0
 | 
			
		||||
	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
 | 
			
		||||
	transition: border-color 150ms ease-in-out, box-shadow 150ms ease-in-out
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		border-bottom-color: $color-background
 | 
			
		||||
 | 
			
		||||
	&:focus
 | 
			
		||||
		outline: 0
 | 
			
		||||
		border-color: $primary
 | 
			
		||||
		box-shadow: none
 | 
			
		||||
		border: thin solid transparent
 | 
			
		||||
		border-bottom-color: $color-primary
 | 
			
		||||
		box-shadow: 0 1px 0 0 $color-primary
 | 
			
		||||
 | 
			
		||||
=label-generic
 | 
			
		||||
	color: $color-text-dark-primary
 | 
			
		||||
@@ -355,6 +354,7 @@
 | 
			
		||||
	+clearfix
 | 
			
		||||
	color: darken($color-text-dark, 5%)
 | 
			
		||||
	font:
 | 
			
		||||
		family: $font-body
 | 
			
		||||
		weight: 300
 | 
			
		||||
		size: 1.2em
 | 
			
		||||
 | 
			
		||||
@@ -507,22 +507,28 @@
 | 
			
		||||
 | 
			
		||||
=ribbon
 | 
			
		||||
	background-color: $color-success
 | 
			
		||||
	border: thin dashed rgba(white, .5)
 | 
			
		||||
	color: white
 | 
			
		||||
	pointer-events: none
 | 
			
		||||
	font-size: 70%
 | 
			
		||||
	cursor: default
 | 
			
		||||
	overflow: hidden
 | 
			
		||||
	white-space: nowrap
 | 
			
		||||
	position: absolute
 | 
			
		||||
	right: -40px
 | 
			
		||||
	top: 10px
 | 
			
		||||
	-webkit-transform: rotate(45deg)
 | 
			
		||||
	-moz-transform: rotate(45deg)
 | 
			
		||||
	-ms-transform: rotate(45deg)
 | 
			
		||||
	-o-transform: rotate(45deg)
 | 
			
		||||
	transform: rotate(45deg)
 | 
			
		||||
	white-space: nowrap
 | 
			
		||||
 | 
			
		||||
	span
 | 
			
		||||
		border: thin dashed rgba(white, .5)
 | 
			
		||||
		color: white
 | 
			
		||||
		display: block
 | 
			
		||||
		font-size: 70%
 | 
			
		||||
		margin: 1px 0
 | 
			
		||||
		padding: 3px 50px
 | 
			
		||||
 | 
			
		||||
		text:
 | 
			
		||||
			align: center
 | 
			
		||||
			transform: uppercase
 | 
			
		||||
 | 
			
		||||
@mixin text-background($text-color, $background-color, $roundness, $padding)
 | 
			
		||||
	border-radius: $roundness
 | 
			
		||||
@@ -636,7 +642,9 @@
 | 
			
		||||
				#{$property}: $color-status-review
 | 
			
		||||
 | 
			
		||||
=sidebar-button-active
 | 
			
		||||
	color: $primary
 | 
			
		||||
	background-color: $color-background-nav
 | 
			
		||||
	box-shadow: inset 2px 0 0 $color-primary
 | 
			
		||||
	color: white
 | 
			
		||||
 | 
			
		||||
.flash-on
 | 
			
		||||
	background-color: lighten($color-success, 50%) !important
 | 
			
		||||
@@ -652,20 +660,3 @@
 | 
			
		||||
	transition: all 1s ease-out
 | 
			
		||||
	img
 | 
			
		||||
		transition: all 1s ease-out
 | 
			
		||||
 | 
			
		||||
.cursor-pointer
 | 
			
		||||
	cursor: pointer
 | 
			
		||||
 | 
			
		||||
.user-select-none
 | 
			
		||||
	user-select: 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1288
									
								
								src/styles/base.sass
									
									
									
									
									
								
							
							
						
						
									
										1288
									
								
								src/styles/base.sass
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,96 +1,21 @@
 | 
			
		||||
// Bootstrap variables and utilities.
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/functions"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/variables"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/mixins"
 | 
			
		||||
 | 
			
		||||
@import _normalize
 | 
			
		||||
@import _config
 | 
			
		||||
@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"
 | 
			
		||||
 | 
			
		||||
@import _comments
 | 
			
		||||
@import _error
 | 
			
		||||
@import _search
 | 
			
		||||
 | 
			
		||||
@import components/base
 | 
			
		||||
@import components/alerts
 | 
			
		||||
@import components/navbar
 | 
			
		||||
@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
 | 
			
		||||
.container-fluid.blog
 | 
			
		||||
	padding: 0
 | 
			
		||||
 | 
			
		||||
#blog_container
 | 
			
		||||
	+media-xs
 | 
			
		||||
		flex-direction: column
 | 
			
		||||
		padding-top: 0
 | 
			
		||||
	display: flex
 | 
			
		||||
	padding:
 | 
			
		||||
		bottom: 15px
 | 
			
		||||
 | 
			
		||||
	video
 | 
			
		||||
		max-width: 100%
 | 
			
		||||
@@ -99,6 +24,10 @@
 | 
			
		||||
	padding: 20px
 | 
			
		||||
 | 
			
		||||
	.form-group
 | 
			
		||||
		position: relative
 | 
			
		||||
		margin: 0 auto 30px auto
 | 
			
		||||
		font-family: $font-body
 | 
			
		||||
 | 
			
		||||
		input, textarea, select
 | 
			
		||||
			+input-generic
 | 
			
		||||
 | 
			
		||||
@@ -238,7 +167,50 @@
 | 
			
		||||
#blog_post-edit-container
 | 
			
		||||
	padding: 25px
 | 
			
		||||
 | 
			
		||||
.blog_index-item
 | 
			
		||||
#blog_index-container,
 | 
			
		||||
#blog_post-create-container,
 | 
			
		||||
#blog_post-edit-container
 | 
			
		||||
	+container-box
 | 
			
		||||
	width: 75%
 | 
			
		||||
 | 
			
		||||
	+media-xs
 | 
			
		||||
		border-radius: 0
 | 
			
		||||
		width: 100%
 | 
			
		||||
		clear: both
 | 
			
		||||
		display: block
 | 
			
		||||
	+media-sm
 | 
			
		||||
		width: 100%
 | 
			
		||||
	+media-md
 | 
			
		||||
		width: 100%
 | 
			
		||||
	+media-lg
 | 
			
		||||
		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%
 | 
			
		||||
@@ -265,8 +237,24 @@
 | 
			
		||||
			+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:
 | 
			
		||||
@@ -288,29 +276,42 @@
 | 
			
		||||
					left: 10px
 | 
			
		||||
					right: 10px
 | 
			
		||||
 | 
			
		||||
#blog_index-container,
 | 
			
		||||
#blog_post-create-container,
 | 
			
		||||
#blog_post-edit-container
 | 
			
		||||
	+container-box
 | 
			
		||||
	width: 75%
 | 
			
		||||
 | 
			
		||||
	+media-xs
 | 
			
		||||
		border-radius: 0
 | 
			
		||||
		width: 100%
 | 
			
		||||
		clear: both
 | 
			
		||||
		display: block
 | 
			
		||||
	+media-sm
 | 
			
		||||
		width: 100%
 | 
			
		||||
	+media-md
 | 
			
		||||
		width: 100%
 | 
			
		||||
	+media-lg
 | 
			
		||||
		width: 100%
 | 
			
		||||
 | 
			
		||||
	.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
 | 
			
		||||
	padding: 0
 | 
			
		||||
 | 
			
		||||
@@ -370,6 +371,12 @@
 | 
			
		||||
	+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
 | 
			
		||||
@@ -468,6 +475,105 @@
 | 
			
		||||
					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
 | 
			
		||||
@@ -497,7 +603,16 @@
 | 
			
		||||
		color: $color-text-dark-secondary
 | 
			
		||||
		pointer-events: none
 | 
			
		||||
 | 
			
		||||
// Specific tweaks for blogs in the context of a project.
 | 
			
		||||
.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
 | 
			
		||||
@@ -522,29 +637,3 @@
 | 
			
		||||
 | 
			
		||||
	.blog-archive-navigation
 | 
			
		||||
		margin-left: 35px
 | 
			
		||||
 | 
			
		||||
// Used on the blog.
 | 
			
		||||
.comments-compact
 | 
			
		||||
	.comments-list
 | 
			
		||||
		border: none
 | 
			
		||||
		padding: 0 0 15px 0
 | 
			
		||||
 | 
			
		||||
	.comments-container
 | 
			
		||||
		max-width: 680px
 | 
			
		||||
		margin: 0 auto
 | 
			
		||||
 | 
			
		||||
		.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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
.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
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
.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
 | 
			
		||||
@@ -1,116 +0,0 @@
 | 
			
		||||
.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-3
 | 
			
		||||
			+media-xs
 | 
			
		||||
				flex: 1 0 50%
 | 
			
		||||
				max-width: 50%
 | 
			
		||||
 | 
			
		||||
			+media-sm
 | 
			
		||||
				flex: 1 0 33%
 | 
			
		||||
				max-width: 33%
 | 
			
		||||
 | 
			
		||||
			+media-md
 | 
			
		||||
				flex: 1 0 25%
 | 
			
		||||
				max-width: 25%
 | 
			
		||||
 | 
			
		||||
			+media-lg
 | 
			
		||||
				flex: 1 0 20%
 | 
			
		||||
				max-width: 20%
 | 
			
		||||
 | 
			
		||||
	&.card-deck-horizontal
 | 
			
		||||
		@extend .flex-column
 | 
			
		||||
		flex-wrap: initial
 | 
			
		||||
 | 
			
		||||
		.card
 | 
			
		||||
			@extend .w-100
 | 
			
		||||
			@extend .flex-row
 | 
			
		||||
			flex: initial
 | 
			
		||||
			max-width: 100%
 | 
			
		||||
 | 
			
		||||
			.card-img-top
 | 
			
		||||
				@extend .rounded-0
 | 
			
		||||
 | 
			
		||||
			.embed-responsive
 | 
			
		||||
				@extend .mr-2
 | 
			
		||||
				max-width: 120px
 | 
			
		||||
 | 
			
		||||
		.card-body
 | 
			
		||||
			@extend .overflow-hidden
 | 
			
		||||
 | 
			
		||||
.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
 | 
			
		||||
 | 
			
		||||
.card-label
 | 
			
		||||
	background-color: rgba($black, .5)
 | 
			
		||||
	border-radius: 3px
 | 
			
		||||
	color: $white
 | 
			
		||||
	display: block
 | 
			
		||||
	font-size: $font-size-xxs
 | 
			
		||||
	left: 5px
 | 
			
		||||
	top: -25px
 | 
			
		||||
	position: absolute
 | 
			
		||||
	padding: 1px 5px
 | 
			
		||||
	z-index: 1
 | 
			
		||||
 | 
			
		||||
.card
 | 
			
		||||
	&.active
 | 
			
		||||
		.card-title
 | 
			
		||||
			color: $primary
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
.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
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
// 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
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
/* 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
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
/* 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
 | 
			
		||||
@@ -1,132 +0,0 @@
 | 
			
		||||
/* 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
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
/* 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
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
// Mainly overrides bootstrap jumbotron settings
 | 
			
		||||
.jumbotron
 | 
			
		||||
	background-size: cover
 | 
			
		||||
	border-radius: 0
 | 
			
		||||
	margin-bottom: 0
 | 
			
		||||
	padding-top: 10em
 | 
			
		||||
	padding-bottom: 10em
 | 
			
		||||
 | 
			
		||||
	// Black-transparent gradient from left to right to better read the overlay text.
 | 
			
		||||
	&.jumbotron-overlay
 | 
			
		||||
		position: relative
 | 
			
		||||
 | 
			
		||||
		&:after
 | 
			
		||||
			background-image: linear-gradient(45deg, rgba(black, .5) 25%, transparent 50%)
 | 
			
		||||
			bottom: 0
 | 
			
		||||
			content: ''
 | 
			
		||||
			left: 0
 | 
			
		||||
			position: absolute
 | 
			
		||||
			right: 0
 | 
			
		||||
			top: 0
 | 
			
		||||
 | 
			
		||||
		*
 | 
			
		||||
			z-index: 1
 | 
			
		||||
 | 
			
		||||
		h2, p
 | 
			
		||||
			text-shadow: 1px 1px rgba(black, .2), 1px 1px 25px rgba(black, .5)
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		text-decoration: none
 | 
			
		||||
@@ -1,231 +0,0 @@
 | 
			
		||||
/* Top level navigation bar. */
 | 
			
		||||
.navbar
 | 
			
		||||
	box-shadow: inset 0 -2px  $color-background
 | 
			
		||||
 | 
			
		||||
.navbar,
 | 
			
		||||
nav.sidebar
 | 
			
		||||
	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%
 | 
			
		||||
 | 
			
		||||
	.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
 | 
			
		||||
 | 
			
		||||
		&.active
 | 
			
		||||
			color: $primary
 | 
			
		||||
			box-shadow: inset 0 -3px 0 $primary
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
		min-width: 50px // navbar avatar size
 | 
			
		||||
 | 
			
		||||
		span.fa-stack
 | 
			
		||||
			position: absolute
 | 
			
		||||
			top: 50%
 | 
			
		||||
			left: 50%
 | 
			
		||||
			transform: translate(-50%, -50%)
 | 
			
		||||
 | 
			
		||||
		ul.dropdown-menu
 | 
			
		||||
			li
 | 
			
		||||
				a
 | 
			
		||||
					white-space: nowrap
 | 
			
		||||
 | 
			
		||||
					&:hover
 | 
			
		||||
						box-shadow: none // removes underline
 | 
			
		||||
 | 
			
		||||
				.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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Secondary navigation. */
 | 
			
		||||
$nav-secondary-bar-size: -2px
 | 
			
		||||
.nav-secondary
 | 
			
		||||
	align-items: center
 | 
			
		||||
	box-shadow: inset 0 $nav-secondary-bar-size 0 0 $color-background
 | 
			
		||||
 | 
			
		||||
	.nav-link
 | 
			
		||||
		box-shadow: inset 0 $nav-secondary-bar-size 0 0 $color-background
 | 
			
		||||
		color: $color-text
 | 
			
		||||
		cursor: pointer
 | 
			
		||||
		transition: box-shadow 150ms ease-in-out
 | 
			
		||||
 | 
			
		||||
	.nav-link:hover,
 | 
			
		||||
	.nav-link.active,
 | 
			
		||||
	.nav-item.dropdown.show .nav-link
 | 
			
		||||
		// Blue bar on the bottom.
 | 
			
		||||
		box-shadow: inset 0 $nav-secondary-bar-size 0 0 $primary
 | 
			
		||||
 | 
			
		||||
		i
 | 
			
		||||
			color: $primary
 | 
			
		||||
 | 
			
		||||
	&.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
 | 
			
		||||
			box-shadow: inset 0 -1px 0 0 $color-background, inset -1px 0 0 0 $color-background
 | 
			
		||||
 | 
			
		||||
			&:hover,
 | 
			
		||||
			&.active
 | 
			
		||||
				box-shadow: inset 0 -1px 0 0 $color-background, inset ($nav-secondary-bar-size * 1.5) 0 0 0 $primary
 | 
			
		||||
 | 
			
		||||
	&.nav-main
 | 
			
		||||
		.nav-link
 | 
			
		||||
			color: $color-text-dark-secondary
 | 
			
		||||
 | 
			
		||||
			&:hover
 | 
			
		||||
				color: $body-color
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.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
 | 
			
		||||
 | 
			
		||||
.navbar-brand
 | 
			
		||||
	color: inherit
 | 
			
		||||
	padding: 0 0 0 3px
 | 
			
		||||
	position: relative
 | 
			
		||||
	top: -2px
 | 
			
		||||
 | 
			
		||||
	&:hover
 | 
			
		||||
		color: $primary
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
.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%)
 | 
			
		||||
@@ -1,87 +0,0 @@
 | 
			
		||||
#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)
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
p.shortcode.nocap
 | 
			
		||||
	padding: 0.6em 3em
 | 
			
		||||
	font-size: .8em
 | 
			
		||||
	color: $color-text-dark-primary
 | 
			
		||||
	background-color: $color-background-dark
 | 
			
		||||
	border-radius: 3px
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
.tooltip
 | 
			
		||||
	transition: none
 | 
			
		||||
 | 
			
		||||
	.tooltip-inner
 | 
			
		||||
		white-space: nowrap
 | 
			
		||||
@@ -1,12 +1,7 @@
 | 
			
		||||
/* 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
 | 
			
		||||
 | 
			
		||||
/* Font properties. */
 | 
			
		||||
@font-face
 | 
			
		||||
  font-family: 'pillar-font'
 | 
			
		||||
  src: url('#{$pillar-font-path}/pillar-font.woff?54788822') format("woff"), url('#{$pillar-font-path}/pillar-font.woff2?54788822') format("woff2")
 | 
			
		||||
  src: url('../font/pillar-font.eot?55726379')
 | 
			
		||||
  src: url('../font/pillar-font.eot?55726379#iefix') format("embedded-opentype"), url('../font/pillar-font.woff2?55726379') format("woff2"), url('../font/pillar-font.woff?55726379') format("woff")
 | 
			
		||||
  font-weight: normal
 | 
			
		||||
  font-style: normal
 | 
			
		||||
 | 
			
		||||
@@ -20,99 +15,23 @@ $pillar-font-path: "../font" !default
 | 
			
		||||
  width: 1em
 | 
			
		||||
  margin-right: .2em
 | 
			
		||||
  text-align: center
 | 
			
		||||
 | 
			
		||||
  /* opacity: .8;
 | 
			
		||||
  /* For safety - reset parent styles, that can break glyph codes
 | 
			
		||||
  font-variant: normal
 | 
			
		||||
  text-transform: none
 | 
			
		||||
  /* fix buttons height, for twitter bootstrap
 | 
			
		||||
  line-height: 1em
 | 
			
		||||
  /* Animation center compensation - margins should be symmetric
 | 
			
		||||
  /* remove if not needed
 | 
			
		||||
  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
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale
 | 
			
		||||
 | 
			
		||||
/* 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 *
 | 
			
		||||
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | 
			
		||||
 */
 | 
			
		||||
  /* Uncomment for 3D effect
 | 
			
		||||
  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3);
 | 
			
		||||
 | 
			
		||||
.pi-collection-plus:before
 | 
			
		||||
  content: '\e800'
 | 
			
		||||
@@ -509,11 +428,6 @@ $pillar-font-path: "../font" !default
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-speed:before
 | 
			
		||||
  content: '\e84f'
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-attention:before
 | 
			
		||||
  content: '\e850'
 | 
			
		||||
 | 
			
		||||
@@ -664,6 +578,11 @@ $pillar-font-path: "../font" !default
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-users:before
 | 
			
		||||
  content: '\e86e'
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-flamenco:before
 | 
			
		||||
  content: '\e86f'
 | 
			
		||||
 | 
			
		||||
@@ -684,11 +603,6 @@ $pillar-font-path: "../font" !default
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-users:before
 | 
			
		||||
  content: '\e873'
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-pause:before
 | 
			
		||||
  content: '\f00e'
 | 
			
		||||
 | 
			
		||||
@@ -724,16 +638,6 @@ $pillar-font-path: "../font" !default
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-social-instagram:before
 | 
			
		||||
  content: '\f16d'
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-database:before
 | 
			
		||||
  content: '\f1c0'
 | 
			
		||||
 | 
			
		||||
/* ''
 | 
			
		||||
 | 
			
		||||
.pi-newspaper:before
 | 
			
		||||
  content: '\f1ea'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +1,15 @@
 | 
			
		||||
// Bootstrap variables and utilities.
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/functions"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/variables"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/mixins"
 | 
			
		||||
 | 
			
		||||
@import _normalize
 | 
			
		||||
@import _config
 | 
			
		||||
@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 */
 | 
			
		||||
@import _notifications
 | 
			
		||||
@import _comments
 | 
			
		||||
 | 
			
		||||
@import _project
 | 
			
		||||
@import _project-sharing
 | 
			
		||||
@import _project-dashboard
 | 
			
		||||
@import _user
 | 
			
		||||
@import _organizations
 | 
			
		||||
@import _search
 | 
			
		||||
@import _organizations
 | 
			
		||||
 | 
			
		||||
/* services, about, etc */
 | 
			
		||||
@import _pages
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
/* jsTree overrides */
 | 
			
		||||
 | 
			
		||||
$tree-color-text: $color-text-dark-primary
 | 
			
		||||
$tree-color-highlight: $color-primary-accent
 | 
			
		||||
$tree-color-highlight-background: $white
 | 
			
		||||
$tree-color-highlight-background-text: $primary
 | 
			
		||||
$tree-color-highlight: hsl(hue($color-background-active), 40%, 50%)
 | 
			
		||||
$tree-color-highlight-background: hsl(hue($color-background-active), 40%, 50%)
 | 
			
		||||
$tree-color-highlight-background-text: white
 | 
			
		||||
 | 
			
		||||
.jstree-default
 | 
			
		||||
	/* list item */
 | 
			
		||||
@@ -33,10 +34,11 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
 | 
			
		||||
		&[data-node-type="page"],
 | 
			
		||||
		&[data-node-type="blog"]
 | 
			
		||||
			color: darken($tree-color-highlight, 5%)
 | 
			
		||||
			font-weight: bold
 | 
			
		||||
 | 
			
		||||
			.jstree-anchor
 | 
			
		||||
				padding: 0 6px
 | 
			
		||||
				padding: 5px 8px 1px 8px
 | 
			
		||||
 | 
			
		||||
				&:after
 | 
			
		||||
					top: 3px !important
 | 
			
		||||
@@ -61,48 +63,49 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
		&.jstree-open
 | 
			
		||||
			/* Text of children for an open tree (like a folder) */
 | 
			
		||||
			.jstree-children > .jstree-node
 | 
			
		||||
				padding-left: 16px !important
 | 
			
		||||
				padding-left: 15px !important
 | 
			
		||||
 | 
			
		||||
				.jstree-icon:empty
 | 
			
		||||
					left: 20px !important
 | 
			
		||||
 | 
			
		||||
					// Tweaks for specific icons
 | 
			
		||||
					&.pi-file-archive
 | 
			
		||||
						left: 25px !important
 | 
			
		||||
						left: 22px !important
 | 
			
		||||
					&.pi-folder
 | 
			
		||||
						left: 20px !important
 | 
			
		||||
						left: 21px !important
 | 
			
		||||
						font-size: .9em !important
 | 
			
		||||
					&.pi-splay
 | 
			
		||||
						left: 20px !important
 | 
			
		||||
					&.pi-film-thick
 | 
			
		||||
						left: 22px !important
 | 
			
		||||
						font-size: .85em !important
 | 
			
		||||
 | 
			
		||||
				.jstree-anchor
 | 
			
		||||
					// box-shadow: inset 1px 0 0 0 $color-background
 | 
			
		||||
					box-shadow: inset 1px 0 0 0 rgba($tree-color-text, .2)
 | 
			
		||||
 | 
			
		||||
		/* Closed Folder */
 | 
			
		||||
		// &.jstree-closed
 | 
			
		||||
 | 
			
		||||
		&.jstree-open .jstree-icon.jstree-ocl,
 | 
			
		||||
		&.jstree-closed .jstree-icon.jstree-ocl
 | 
			
		||||
			float: left
 | 
			
		||||
			min-width: 30px
 | 
			
		||||
			opacity: 0
 | 
			
		||||
			position: absolute
 | 
			
		||||
			z-index: 1
 | 
			
		||||
			opacity: 0
 | 
			
		||||
			min-width: 30px
 | 
			
		||||
			float: left
 | 
			
		||||
 | 
			
		||||
		/* The text of the last level item */
 | 
			
		||||
		.jstree-anchor
 | 
			
		||||
			+media-xs
 | 
			
		||||
				padding: 0 !important
 | 
			
		||||
				width: 98%
 | 
			
		||||
				padding: 0 !important
 | 
			
		||||
			border: none
 | 
			
		||||
			font-size: 13px
 | 
			
		||||
			height: inherit
 | 
			
		||||
			line-height: 24px
 | 
			
		||||
			line-height: 26px
 | 
			
		||||
			overflow: hidden
 | 
			
		||||
			padding-left: 28px
 | 
			
		||||
			padding-right: 10px
 | 
			
		||||
			text-overflow: ellipsis
 | 
			
		||||
			transition: none
 | 
			
		||||
			transition: color 50ms ease-in-out, background-color 100ms ease-in-out
 | 
			
		||||
			white-space: nowrap
 | 
			
		||||
			width: 100%
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +113,7 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
			&:after
 | 
			
		||||
				content: '\e83a' !important
 | 
			
		||||
				font-family: 'pillar-font'
 | 
			
		||||
				color: $tree-color-highlight-background-text
 | 
			
		||||
				color: white
 | 
			
		||||
				display: none
 | 
			
		||||
				position: absolute
 | 
			
		||||
				right: 7px
 | 
			
		||||
@@ -118,31 +121,31 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
 | 
			
		||||
			// Icon, not selected
 | 
			
		||||
			.jstree-icon
 | 
			
		||||
				color: $tree-color-text
 | 
			
		||||
				color: $color-text-dark-secondary
 | 
			
		||||
				font-size: 95% !important
 | 
			
		||||
				margin: 0 !important
 | 
			
		||||
 | 
			
		||||
			/* Selected item */
 | 
			
		||||
			&.jstree-clicked
 | 
			
		||||
				background-color: $tree-color-highlight-background !important
 | 
			
		||||
				color: $tree-color-highlight-background-text !important
 | 
			
		||||
				font-weight: bold
 | 
			
		||||
				color: white !important
 | 
			
		||||
 | 
			
		||||
				&:after
 | 
			
		||||
					display: block
 | 
			
		||||
					color: $tree-color-highlight-background-text !important
 | 
			
		||||
					color: white !important
 | 
			
		||||
 | 
			
		||||
				.jstree-ocl,
 | 
			
		||||
				.jstree-icon
 | 
			
		||||
					color: $tree-color-highlight-background-text
 | 
			
		||||
					color: white
 | 
			
		||||
 | 
			
		||||
				/* hover an active item */
 | 
			
		||||
				&.jstree-hovered
 | 
			
		||||
					background-color: lighten($tree-color-highlight-background, 10%) !important
 | 
			
		||||
					box-shadow: none
 | 
			
		||||
					color: $tree-color-highlight-background-text !important
 | 
			
		||||
					color: white !important
 | 
			
		||||
 | 
			
		||||
				&.jstree-hovered .jstree-icon
 | 
			
		||||
					color: $tree-color-highlight-background-text !important
 | 
			
		||||
					color: white !important
 | 
			
		||||
 | 
			
		||||
			.jstree-hovered
 | 
			
		||||
				background-color: rgba($tree-color-highlight, .1) !important
 | 
			
		||||
@@ -181,8 +184,8 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
		position: absolute
 | 
			
		||||
 | 
			
		||||
		&:empty
 | 
			
		||||
			line-height: 24px
 | 
			
		||||
			left: 3px
 | 
			
		||||
			line-height: 26px
 | 
			
		||||
			left: 5px
 | 
			
		||||
 | 
			
		||||
	&.is_subscriber
 | 
			
		||||
		.jstree-node
 | 
			
		||||
@@ -266,7 +269,7 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
 | 
			
		||||
.jstree-default .jstree-node.jstree-closed .jstree-icon.jstree-ocl + .jstree-anchor,
 | 
			
		||||
.jstree-default .jstree-node.jstree-open .jstree-icon.jstree-ocl + .jstree-anchor
 | 
			
		||||
	padding-left: 24px !important
 | 
			
		||||
	padding-left: 28px !important
 | 
			
		||||
 | 
			
		||||
/* hovered text */
 | 
			
		||||
.jstree-default .jstree-hovered,
 | 
			
		||||
@@ -277,11 +280,11 @@ $tree-color-highlight-background-text: $primary
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	background-color: rgba($tree-color-highlight-background, .8) !important
 | 
			
		||||
	color: $tree-color-highlight-background-text !important
 | 
			
		||||
	color: white !important
 | 
			
		||||
 | 
			
		||||
i.jstree-icon.jstree-ocl
 | 
			
		||||
	color: rgba($tree-color-text, .5) !important
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
$videoplayer-controls-color: white
 | 
			
		||||
$videoplayer-background-color: darken($primary, 10%)
 | 
			
		||||
 | 
			
		||||
$videoplayer-progress-bar-height: .5em
 | 
			
		||||
$videoplayer-background-color: $color-background-nav
 | 
			
		||||
 | 
			
		||||
.video-js
 | 
			
		||||
	.vjs-big-play-button:before, .vjs-control:before, .vjs-modal-dialog
 | 
			
		||||
@@ -32,6 +30,7 @@ $videoplayer-progress-bar-height: .5em
 | 
			
		||||
	font-weight: normal
 | 
			
		||||
	font-style: normal
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.vjs-icon-play
 | 
			
		||||
	font-family: VideoJS
 | 
			
		||||
	font-weight: normal
 | 
			
		||||
@@ -286,6 +285,7 @@ $videoplayer-progress-bar-height: .5em
 | 
			
		||||
	line-height: 1
 | 
			
		||||
	font-weight: normal
 | 
			
		||||
	font-style: normal
 | 
			
		||||
	font-family: Arial, Helvetica, sans-serif
 | 
			
		||||
	-webkit-user-select: none
 | 
			
		||||
	-moz-user-select: none
 | 
			
		||||
	-ms-user-select: none
 | 
			
		||||
@@ -453,22 +453,20 @@ body.vjs-full-window
 | 
			
		||||
	list-style: none
 | 
			
		||||
	margin: 0
 | 
			
		||||
	padding: 0.2em 0
 | 
			
		||||
	line-height: 1.8em
 | 
			
		||||
	font-size: 1.1em
 | 
			
		||||
	line-height: 1.4em
 | 
			
		||||
	font-size: 1.2em
 | 
			
		||||
	text-align: center
 | 
			
		||||
	text-transform: lowercase
 | 
			
		||||
 | 
			
		||||
	&:focus, &:hover
 | 
			
		||||
		background-color: darken($primary, 20%)
 | 
			
		||||
 | 
			
		||||
		outline: 0
 | 
			
		||||
		background-color: #73859f
 | 
			
		||||
		background-color: rgba(115, 133, 159, 0.5)
 | 
			
		||||
	&.vjs-selected
 | 
			
		||||
		background-color: $videoplayer-controls-color
 | 
			
		||||
		color: $videoplayer-background-color
 | 
			
		||||
 | 
			
		||||
		&:focus, &:hover
 | 
			
		||||
			background-color: $videoplayer-controls-color
 | 
			
		||||
			color: $videoplayer-background-color
 | 
			
		||||
 | 
			
		||||
	&.vjs-menu-title
 | 
			
		||||
		text-align: center
 | 
			
		||||
		text-transform: uppercase
 | 
			
		||||
@@ -488,13 +486,12 @@ body.vjs-full-window
 | 
			
		||||
	height: 0em
 | 
			
		||||
	margin-bottom: 1.5em
 | 
			
		||||
	border-top-color: $videoplayer-background-color
 | 
			
		||||
 | 
			
		||||
	.vjs-menu-content
 | 
			
		||||
		background-color: $videoplayer-background-color
 | 
			
		||||
		position: absolute
 | 
			
		||||
		width: 100%
 | 
			
		||||
		bottom: 1.5em
 | 
			
		||||
		max-height: 25em
 | 
			
		||||
		max-height: 15em
 | 
			
		||||
 | 
			
		||||
.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, .vjs-menu-button-popup .vjs-menu.vjs-lock-showing
 | 
			
		||||
	display: block
 | 
			
		||||
@@ -658,12 +655,12 @@ body.vjs-full-window
 | 
			
		||||
		-moz-transition: all 0.2s
 | 
			
		||||
		-o-transition: all 0.2s
 | 
			
		||||
		transition: all 0.2s
 | 
			
		||||
		height: $videoplayer-progress-bar-height
 | 
			
		||||
		height: 0.3em
 | 
			
		||||
 | 
			
		||||
		.vjs-play-progress
 | 
			
		||||
			position: absolute
 | 
			
		||||
			display: block
 | 
			
		||||
			height: $videoplayer-progress-bar-height
 | 
			
		||||
			height: 0.3em
 | 
			
		||||
			margin: 0
 | 
			
		||||
			padding: 0
 | 
			
		||||
			width: 0
 | 
			
		||||
@@ -673,7 +670,7 @@ body.vjs-full-window
 | 
			
		||||
		.vjs-load-progress
 | 
			
		||||
			position: absolute
 | 
			
		||||
			display: block
 | 
			
		||||
			height: $videoplayer-progress-bar-height
 | 
			
		||||
			height: 0.3em
 | 
			
		||||
			margin: 0
 | 
			
		||||
			padding: 0
 | 
			
		||||
			width: 0
 | 
			
		||||
@@ -683,7 +680,7 @@ body.vjs-full-window
 | 
			
		||||
			div
 | 
			
		||||
				position: absolute
 | 
			
		||||
				display: block
 | 
			
		||||
				height: $videoplayer-progress-bar-height
 | 
			
		||||
				height: 0.3em
 | 
			
		||||
				margin: 0
 | 
			
		||||
				padding: 0
 | 
			
		||||
				width: 0
 | 
			
		||||
@@ -695,11 +692,10 @@ body.vjs-full-window
 | 
			
		||||
 | 
			
		||||
	.vjs-play-progress
 | 
			
		||||
		background-color: $videoplayer-controls-color
 | 
			
		||||
		border-radius: 999em
 | 
			
		||||
 | 
			
		||||
		&:before
 | 
			
		||||
			position: absolute
 | 
			
		||||
			top: -($videoplayer-progress-bar-height / 2) // halfway the height of the progress bar
 | 
			
		||||
			top: -0.333333333333333em
 | 
			
		||||
			right: -0.5em
 | 
			
		||||
 | 
			
		||||
		&:after
 | 
			
		||||
@@ -716,8 +712,8 @@ body.vjs-full-window
 | 
			
		||||
			z-index: 1
 | 
			
		||||
 | 
			
		||||
		.vjs-time-tooltip
 | 
			
		||||
			color: $videoplayer-controls-color
 | 
			
		||||
			background-color: $videoplayer-background-color
 | 
			
		||||
			color: $videoplayer-controls-color
 | 
			
		||||
			z-index: 1
 | 
			
		||||
 | 
			
		||||
			&:after
 | 
			
		||||
@@ -739,9 +735,9 @@ body.vjs-full-window
 | 
			
		||||
 | 
			
		||||
.vjs-time-tooltip
 | 
			
		||||
	background-color: $videoplayer-controls-color
 | 
			
		||||
	border-radius: $border-radius
 | 
			
		||||
	border-radius: 3px
 | 
			
		||||
	color: $videoplayer-background-color
 | 
			
		||||
	font-family: $font-family-base
 | 
			
		||||
	font-family: $font-body
 | 
			
		||||
	font-size: 1.2em
 | 
			
		||||
	font-weight: bold
 | 
			
		||||
	padding: 5px 8px
 | 
			
		||||
@@ -855,9 +851,9 @@ body.vjs-full-window
 | 
			
		||||
		font-size: 0.9em
 | 
			
		||||
 | 
			
		||||
.vjs-slider-horizontal .vjs-volume-level
 | 
			
		||||
	height: $videoplayer-progress-bar-height
 | 
			
		||||
	height: 0.3em
 | 
			
		||||
	&:before
 | 
			
		||||
		top: -$videoplayer-progress-bar-height
 | 
			
		||||
		top: -0.3em
 | 
			
		||||
		right: -0.5em
 | 
			
		||||
 | 
			
		||||
.vjs-menu-button-popup
 | 
			
		||||
@@ -1026,15 +1022,14 @@ video::-webkit-media-text-track-display
 | 
			
		||||
 | 
			
		||||
.vjs-playback-rate
 | 
			
		||||
	.vjs-playback-rate-value
 | 
			
		||||
		font-size: 1.25em
 | 
			
		||||
		font-size: 1.5em
 | 
			
		||||
		line-height: 2
 | 
			
		||||
		position: absolute
 | 
			
		||||
		top: 3px
 | 
			
		||||
		top: 0
 | 
			
		||||
		left: 0
 | 
			
		||||
		width: 100%
 | 
			
		||||
		height: 100%
 | 
			
		||||
		text-align: center
 | 
			
		||||
 | 
			
		||||
	.vjs-menu
 | 
			
		||||
		width: 4em
 | 
			
		||||
		left: 0em
 | 
			
		||||
@@ -1046,6 +1041,7 @@ video::-webkit-media-text-track-display
 | 
			
		||||
	&:before
 | 
			
		||||
		color: $videoplayer-controls-color
 | 
			
		||||
		content: 'X'
 | 
			
		||||
		font-family: Arial, Helvetica, sans-serif
 | 
			
		||||
		font-size: 4em
 | 
			
		||||
		left: 0
 | 
			
		||||
		line-height: 1
 | 
			
		||||
@@ -1365,28 +1361,3 @@ video::-webkit-media-text-track-display
 | 
			
		||||
			position: absolute
 | 
			
		||||
			bottom: 3em
 | 
			
		||||
			left: 0.5em
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#player-endcard
 | 
			
		||||
	background-color: rgba($black, .5)
 | 
			
		||||
	bottom: 0
 | 
			
		||||
	font-size: 1.5em
 | 
			
		||||
	left: 0
 | 
			
		||||
	position: absolute
 | 
			
		||||
	right: 0
 | 
			
		||||
	top: 0
 | 
			
		||||
	z-index: 1
 | 
			
		||||
 | 
			
		||||
	#related-content,
 | 
			
		||||
	.end-card-content
 | 
			
		||||
		@extend .align-items-center
 | 
			
		||||
		@extend .d-flex
 | 
			
		||||
		@extend .flex-column
 | 
			
		||||
		@extend .h-100
 | 
			
		||||
		@extend .justify-content-center
 | 
			
		||||
 | 
			
		||||
	.btn-video-overlay
 | 
			
		||||
		border: thin solid $white
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			background-color: rgba($white, .5)
 | 
			
		||||
@@ -1,76 +1,8 @@
 | 
			
		||||
// Bootstrap variables and utilities.
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/functions"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/variables"
 | 
			
		||||
@import "../../node_modules/bootstrap/scss/mixins"
 | 
			
		||||
 | 
			
		||||
@import _normalize
 | 
			
		||||
@import _config
 | 
			
		||||
@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"
 | 
			
		||||
 | 
			
		||||
@import _notifications
 | 
			
		||||
@import _comments
 | 
			
		||||
 | 
			
		||||
@import _project
 | 
			
		||||
@import _project-sharing
 | 
			
		||||
@import _project-dashboard
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,8 @@ $color-theatre-background-dark: darken($color-theatre-background, 5%)
 | 
			
		||||
 | 
			
		||||
$theatre-width: 350px
 | 
			
		||||
 | 
			
		||||
body.theatre
 | 
			
		||||
body.theatre,
 | 
			
		||||
body.theatre .container-page
 | 
			
		||||
	background-color: $color-theatre-background
 | 
			
		||||
	nav.navbar
 | 
			
		||||
		+media-lg
 | 
			
		||||
@@ -25,7 +26,6 @@ body.theatre
 | 
			
		||||
		display: flex
 | 
			
		||||
		align-items: center
 | 
			
		||||
		justify-content: center
 | 
			
		||||
 | 
			
		||||
		.page-body
 | 
			
		||||
			height: 100%
 | 
			
		||||
			width: 100%
 | 
			
		||||
 
 | 
			
		||||
@@ -7,21 +7,19 @@
 | 
			
		||||
| {% set node_type_name = 'folder' %}
 | 
			
		||||
| {% endif %}
 | 
			
		||||
li(class="button-{{ node_type['name'] }}")
 | 
			
		||||
	a.dropdown-item(
 | 
			
		||||
		class="item_add_node",
 | 
			
		||||
	a.item_add_node(
 | 
			
		||||
	href="#",
 | 
			
		||||
	title="{{ node_type['description'] }}",
 | 
			
		||||
	data-node-type-name="{{ node_type['name'] }}",
 | 
			
		||||
	data-toggle="tooltip",
 | 
			
		||||
	data-placement="left")
 | 
			
		||||
		i.pi(class="pi-{{ node_type['name'] }}")
 | 
			
		||||
		i.pi(class="icon-{{ node_type['name'] }}")
 | 
			
		||||
		| {% if node_type_name == 'group_texture' %}
 | 
			
		||||
		| Texture Folder
 | 
			
		||||
		| {% elif node_type_name == 'group_hdri' %}
 | 
			
		||||
		| HDRi Folder
 | 
			
		||||
		| {% else %}
 | 
			
		||||
		span.text-capitalize
 | 
			
		||||
			|{{ node_type_name }}
 | 
			
		||||
		| {{ node_type_name }}
 | 
			
		||||
		| {% endif %}
 | 
			
		||||
| {% endif %}
 | 
			
		||||
| {% endfor %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
| {% 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
 | 
			
		||||
		.card-title.mb-1.font-weight-bold
 | 
			
		||||
			| {{ asset.name }}
 | 
			
		||||
 | 
			
		||||
		ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto
 | 
			
		||||
			li.pr-2 {{ node_type | undertitle }}
 | 
			
		||||
			li {{ asset._created | pretty_date }}
 | 
			
		||||
 | 
			
		||||
		| {% 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...
 | 
			
		||||
						input(type='file', name='file', multiple='')
 | 
			
		||||
 | 
			
		||||
					button.btn.btn-outline-primary.start(type='submit')
 | 
			
		||||
					button.btn.btn-primary.start(type='submit')
 | 
			
		||||
						i.pi-upload
 | 
			
		||||
						span Start Upload
 | 
			
		||||
						span Start upload
 | 
			
		||||
 | 
			
		||||
					button.btn.btn-outline-warning.cancel(type='reset')
 | 
			
		||||
					button.btn.btn-warning.cancel(type='reset')
 | 
			
		||||
						i.pi-cancel
 | 
			
		||||
						span Cancel Upload
 | 
			
		||||
						span Cancel upload
 | 
			
		||||
 | 
			
		||||
					button.btn.btn-outline-danger.delete(type='button')
 | 
			
		||||
					button.btn.btn-danger.delete(type='button')
 | 
			
		||||
						i.pi-trash
 | 
			
		||||
						span Delete
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ script#template-upload(type="text/x-tmpl").
 | 
			
		||||
				</button>
 | 
			
		||||
			{% } %}
 | 
			
		||||
			{% if (!i) { %}
 | 
			
		||||
				<button class="btn btn-outline-secondary cancel">
 | 
			
		||||
				<button class="btn btn-warning cancel">
 | 
			
		||||
					<i class="ion-close-round"></i>
 | 
			
		||||
					<span>Cancel</span>
 | 
			
		||||
				</button>
 | 
			
		||||
@@ -61,7 +61,7 @@ script#template-download(type="text/x-tmpl").
 | 
			
		||||
		</td>
 | 
			
		||||
		<td>
 | 
			
		||||
			{% if (file.deleteUrl) { %}
 | 
			
		||||
				<button class="btn btn-outline-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
 | 
			
		||||
				<button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
 | 
			
		||||
					<i class="ion-trash-b"></i>
 | 
			
		||||
					<span>Delete</span>
 | 
			
		||||
				</button>
 | 
			
		||||
@@ -71,7 +71,7 @@ script#template-download(type="text/x-tmpl").
 | 
			
		||||
					Create
 | 
			
		||||
				</div>
 | 
			
		||||
			{% } else { %}
 | 
			
		||||
				<button class="btn btn-outline-secondary cancel">
 | 
			
		||||
				<button class="btn btn-warning cancel">
 | 
			
		||||
					<i class="ion-close-round"></i>
 | 
			
		||||
					<span>Cancel</span>
 | 
			
		||||
				</button>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src/templates/_macros/_navigation.pug
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/templates/_macros/_navigation.pug
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
| {% 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 %}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user