Compare commits
1 Commits
wip-refact
...
tmp-video-
Author | SHA1 | Date | |
---|---|---|---|
4927a26497 |
2176
package-lock.json
generated
2176
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,18 @@
|
||||
import base64
|
||||
import functools
|
||||
import logging
|
||||
import typing
|
||||
import urllib.parse
|
||||
|
||||
import pymongo.errors
|
||||
import werkzeug.exceptions as wz_exceptions
|
||||
from bson import ObjectId
|
||||
from flask import current_app, Blueprint, request
|
||||
|
||||
from pillar.api.nodes import hooks
|
||||
from pillar.api.nodes.hooks import short_link_info
|
||||
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
|
||||
from pillar.api.utils import str2id, jsonify
|
||||
from pillar.api.utils.authorization import check_permissions, require_login
|
||||
|
||||
@@ -15,6 +21,40 @@ blueprint = Blueprint('nodes_api', __name__)
|
||||
ROLES_FOR_SHARING = {'subscriber', 'demo'}
|
||||
|
||||
|
||||
def only_for_node_type_decorator(*required_node_type_names):
|
||||
"""Returns a decorator that checks its first argument's node type.
|
||||
|
||||
If the node type is not of the required node type, returns None,
|
||||
otherwise calls the wrapped function.
|
||||
|
||||
>>> deco = only_for_node_type_decorator('comment')
|
||||
>>> @deco
|
||||
... def handle_comment(node): pass
|
||||
|
||||
>>> deco = only_for_node_type_decorator('comment', 'post')
|
||||
>>> @deco
|
||||
... def handle_comment_or_post(node): pass
|
||||
|
||||
"""
|
||||
|
||||
# Convert to a set for efficient 'x in required_node_type_names' queries.
|
||||
required_node_type_names = set(required_node_type_names)
|
||||
|
||||
def only_for_node_type(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def wrapper(node, *args, **kwargs):
|
||||
if node.get('node_type') not in required_node_type_names:
|
||||
return
|
||||
|
||||
return wrapped(node, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
only_for_node_type.__doc__ = "Decorator, immediately returns when " \
|
||||
"the first argument is not of type %s." % required_node_type_names
|
||||
return only_for_node_type
|
||||
|
||||
|
||||
@blueprint.route('/<node_id>/share', methods=['GET', 'POST'])
|
||||
@require_login(require_roles=ROLES_FOR_SHARING)
|
||||
def share_node(node_id):
|
||||
@@ -54,31 +94,13 @@ def share_node(node_id):
|
||||
@blueprint.route('/tagged/<tag>')
|
||||
def tagged(tag=''):
|
||||
"""Return all tagged nodes of public projects as JSON."""
|
||||
from pillar.auth import current_user
|
||||
|
||||
# We explicitly register the tagless endpoint to raise a 404, otherwise the PATCH
|
||||
# handler on /api/nodes/<node_id> will return a 405 Method Not Allowed.
|
||||
if not tag:
|
||||
raise wz_exceptions.NotFound()
|
||||
|
||||
# Build the (cached) list of tagged nodes
|
||||
agg_list = _tagged(tag)
|
||||
|
||||
# If the user is anonymous, no more information is needed and we return
|
||||
if current_user.is_anonymous:
|
||||
return jsonify(agg_list)
|
||||
|
||||
# If the user is authenticated, attach view_progress for video assets
|
||||
view_progress = current_user.nodes['view_progress']
|
||||
for node in agg_list:
|
||||
node_id = str(node['_id'])
|
||||
# View progress should be added only for nodes of type 'asset' and
|
||||
# with content_type 'video', only if the video was already in the watched
|
||||
# list for the current user.
|
||||
if node_id in view_progress:
|
||||
node['view_progress'] = view_progress[node_id]
|
||||
|
||||
return jsonify(agg_list)
|
||||
return _tagged(tag)
|
||||
|
||||
|
||||
def _tagged(tag: str):
|
||||
@@ -107,8 +129,7 @@ def _tagged(tag: str):
|
||||
|
||||
{'$sort': {'_created': -1}}
|
||||
])
|
||||
|
||||
return list(agg)
|
||||
return jsonify(list(agg))
|
||||
|
||||
|
||||
def generate_and_store_short_code(node):
|
||||
@@ -186,6 +207,283 @@ def create_short_code(node) -> str:
|
||||
return short_code
|
||||
|
||||
|
||||
def short_link_info(short_code):
|
||||
"""Returns the short link info in a dict."""
|
||||
|
||||
short_link = urllib.parse.urljoin(
|
||||
current_app.config['SHORT_LINK_BASE_URL'], short_code)
|
||||
|
||||
return {
|
||||
'short_code': short_code,
|
||||
'short_link': short_link,
|
||||
}
|
||||
|
||||
|
||||
def before_replacing_node(item, original):
|
||||
check_permissions('nodes', original, 'PUT')
|
||||
update_file_name(item)
|
||||
|
||||
|
||||
def after_replacing_node(item, original):
|
||||
"""Push an update to the Algolia index when a node item is updated. If the
|
||||
project is private, prevent public indexing.
|
||||
"""
|
||||
|
||||
from pillar.celery import search_index_tasks as index
|
||||
|
||||
projects_collection = current_app.data.driver.db['projects']
|
||||
project = projects_collection.find_one({'_id': item['project']})
|
||||
if project.get('is_private', False):
|
||||
# Skip index updating and return
|
||||
return
|
||||
|
||||
status = item['properties'].get('status', 'unpublished')
|
||||
node_id = str(item['_id'])
|
||||
|
||||
if status == 'published':
|
||||
index.node_save.delay(node_id)
|
||||
else:
|
||||
index.node_delete.delay(node_id)
|
||||
|
||||
|
||||
def before_inserting_nodes(items):
|
||||
"""Before inserting a node in the collection we check if the user is allowed
|
||||
and we append the project id to it.
|
||||
"""
|
||||
from pillar.auth import current_user
|
||||
|
||||
nodes_collection = current_app.data.driver.db['nodes']
|
||||
|
||||
def find_parent_project(node):
|
||||
"""Recursive function that finds the ultimate parent of a node."""
|
||||
if node and 'parent' in node:
|
||||
parent = nodes_collection.find_one({'_id': node['parent']})
|
||||
return find_parent_project(parent)
|
||||
if node:
|
||||
return node
|
||||
else:
|
||||
return None
|
||||
|
||||
for item in items:
|
||||
check_permissions('nodes', item, 'POST')
|
||||
if 'parent' in item and 'project' not in item:
|
||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||
project = find_parent_project(parent)
|
||||
if project:
|
||||
item['project'] = project['_id']
|
||||
|
||||
# Default the 'user' property to the current user.
|
||||
item.setdefault('user', current_user.user_id)
|
||||
|
||||
|
||||
def after_inserting_nodes(items):
|
||||
for item in items:
|
||||
# Skip subscriptions for first level items (since the context is not a
|
||||
# node, but a project).
|
||||
# TODO: support should be added for mixed context
|
||||
if 'parent' not in item:
|
||||
return
|
||||
context_object_id = item['parent']
|
||||
if item['node_type'] == 'comment':
|
||||
nodes_collection = current_app.data.driver.db['nodes']
|
||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||
# Always subscribe to the parent node
|
||||
activity_subscribe(item['user'], 'node', item['parent'])
|
||||
if parent['node_type'] == 'comment':
|
||||
# If the parent is a comment, we provide its own parent as
|
||||
# context. We do this in order to point the user to an asset
|
||||
# or group when viewing the notification.
|
||||
verb = 'replied'
|
||||
context_object_id = parent['parent']
|
||||
# Subscribe to the parent of the parent comment (post or group)
|
||||
activity_subscribe(item['user'], 'node', parent['parent'])
|
||||
else:
|
||||
activity_subscribe(item['user'], 'node', item['_id'])
|
||||
verb = 'commented'
|
||||
elif item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
||||
verb = 'posted'
|
||||
activity_subscribe(item['user'], 'node', item['_id'])
|
||||
else:
|
||||
# Don't automatically create activities for non-Pillar node types,
|
||||
# as we don't know what would be a suitable verb (among other things).
|
||||
continue
|
||||
|
||||
activity_object_add(
|
||||
item['user'],
|
||||
verb,
|
||||
'node',
|
||||
item['_id'],
|
||||
'node',
|
||||
context_object_id
|
||||
)
|
||||
|
||||
|
||||
def deduct_content_type(node_doc, original=None):
|
||||
"""Deduct the content type from the attached file, if any."""
|
||||
|
||||
if node_doc['node_type'] != 'asset':
|
||||
log.debug('deduct_content_type: called on node type %r, ignoring', node_doc['node_type'])
|
||||
return
|
||||
|
||||
node_id = node_doc.get('_id')
|
||||
try:
|
||||
file_id = ObjectId(node_doc['properties']['file'])
|
||||
except KeyError:
|
||||
if node_id is None:
|
||||
# Creation of a file-less node is allowed, but updates aren't.
|
||||
return
|
||||
log.warning('deduct_content_type: Asset without properties.file, rejecting.')
|
||||
raise wz_exceptions.UnprocessableEntity('Missing file property for asset node')
|
||||
|
||||
files = current_app.data.driver.db['files']
|
||||
file_doc = files.find_one({'_id': file_id},
|
||||
{'content_type': 1})
|
||||
if not file_doc:
|
||||
log.warning('deduct_content_type: Node %s refers to non-existing file %s, rejecting.',
|
||||
node_id, file_id)
|
||||
raise wz_exceptions.UnprocessableEntity('File property refers to non-existing file')
|
||||
|
||||
# Guess the node content type from the file content type
|
||||
file_type = file_doc['content_type']
|
||||
if file_type.startswith('video/'):
|
||||
content_type = 'video'
|
||||
elif file_type.startswith('image/'):
|
||||
content_type = 'image'
|
||||
else:
|
||||
content_type = 'file'
|
||||
|
||||
node_doc['properties']['content_type'] = content_type
|
||||
|
||||
|
||||
def nodes_deduct_content_type(nodes):
|
||||
for node in nodes:
|
||||
deduct_content_type(node)
|
||||
|
||||
|
||||
def before_returning_node(node):
|
||||
# Run validation process, since GET on nodes entry point is public
|
||||
check_permissions('nodes', node, 'GET', append_allowed_methods=True)
|
||||
|
||||
# Embed short_link_info if the node has a short_code.
|
||||
short_code = node.get('short_code')
|
||||
if short_code:
|
||||
node['short_link'] = short_link_info(short_code)['short_link']
|
||||
|
||||
|
||||
def before_returning_nodes(nodes):
|
||||
for node in nodes['_items']:
|
||||
before_returning_node(node)
|
||||
|
||||
|
||||
def node_set_default_picture(node, original=None):
|
||||
"""Uses the image of an image asset or colour map of texture node as picture."""
|
||||
|
||||
if node.get('picture'):
|
||||
log.debug('Node %s already has a picture, not overriding', node.get('_id'))
|
||||
return
|
||||
|
||||
node_type = node.get('node_type')
|
||||
props = node.get('properties', {})
|
||||
content = props.get('content_type')
|
||||
|
||||
if node_type == 'asset' and content == 'image':
|
||||
image_file_id = props.get('file')
|
||||
elif node_type == 'texture':
|
||||
# Find the colour map, defaulting to the first image map available.
|
||||
image_file_id = None
|
||||
for image in props.get('files', []):
|
||||
if image_file_id is None or image.get('map_type') == 'color':
|
||||
image_file_id = image.get('file')
|
||||
else:
|
||||
log.debug('Not setting default picture on node type %s content type %s',
|
||||
node_type, content)
|
||||
return
|
||||
|
||||
if image_file_id is None:
|
||||
log.debug('Nothing to set the picture to.')
|
||||
return
|
||||
|
||||
log.debug('Setting default picture for node %s to %s', node.get('_id'), image_file_id)
|
||||
node['picture'] = image_file_id
|
||||
|
||||
|
||||
def nodes_set_default_picture(nodes):
|
||||
for node in nodes:
|
||||
node_set_default_picture(node)
|
||||
|
||||
|
||||
def before_deleting_node(node: dict):
|
||||
check_permissions('nodes', node, 'DELETE')
|
||||
|
||||
|
||||
def after_deleting_node(item):
|
||||
from pillar.celery import search_index_tasks as index
|
||||
index.node_delete.delay(str(item['_id']))
|
||||
|
||||
|
||||
only_for_textures = only_for_node_type_decorator('texture')
|
||||
|
||||
|
||||
@only_for_textures
|
||||
def texture_sort_files(node, original=None):
|
||||
"""Sort files alphabetically by map type, with colour map first."""
|
||||
|
||||
try:
|
||||
files = node['properties']['files']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# Sort the map types alphabetically, ensuring 'color' comes first.
|
||||
as_dict = {f['map_type']: f for f in files}
|
||||
types = sorted(as_dict.keys(), key=lambda k: '\0' if k == 'color' else k)
|
||||
node['properties']['files'] = [as_dict[map_type] for map_type in types]
|
||||
|
||||
|
||||
def textures_sort_files(nodes):
|
||||
for node in nodes:
|
||||
texture_sort_files(node)
|
||||
|
||||
|
||||
def parse_markdown(node, original=None):
|
||||
import copy
|
||||
|
||||
projects_collection = current_app.data.driver.db['projects']
|
||||
project = projects_collection.find_one({'_id': node['project']}, {'node_types': 1})
|
||||
# Query node type directly using the key
|
||||
node_type = next(nt for nt in project['node_types']
|
||||
if nt['name'] == node['node_type'])
|
||||
|
||||
# Create a copy to not overwrite the actual schema.
|
||||
schema = copy.deepcopy(current_app.config['DOMAIN']['nodes']['schema'])
|
||||
schema['properties'] = node_type['dyn_schema']
|
||||
|
||||
def find_markdown_fields(schema, node):
|
||||
"""Find and process all makrdown validated fields."""
|
||||
for k, v in schema.items():
|
||||
if not isinstance(v, dict):
|
||||
continue
|
||||
|
||||
if v.get('validator') == 'markdown':
|
||||
# If there is a match with the validator: markdown pair, assign the sibling
|
||||
# property (following the naming convention _<property>_html)
|
||||
# the processed value.
|
||||
if k in node:
|
||||
html = pillar.markdown.markdown(node[k])
|
||||
field_name = pillar.markdown.cache_field_name(k)
|
||||
node[field_name] = html
|
||||
if isinstance(node, dict) and k in node:
|
||||
find_markdown_fields(v, node[k])
|
||||
|
||||
find_markdown_fields(schema, node)
|
||||
|
||||
return 'ok'
|
||||
|
||||
|
||||
def parse_markdowns(items):
|
||||
for item in items:
|
||||
parse_markdown(item)
|
||||
|
||||
|
||||
def setup_app(app, url_prefix):
|
||||
global _tagged
|
||||
|
||||
@@ -195,26 +493,26 @@ def setup_app(app, url_prefix):
|
||||
from . import patch
|
||||
patch.setup_app(app, url_prefix=url_prefix)
|
||||
|
||||
app.on_fetched_item_nodes += hooks.before_returning_node
|
||||
app.on_fetched_resource_nodes += hooks.before_returning_nodes
|
||||
app.on_fetched_item_nodes += before_returning_node
|
||||
app.on_fetched_resource_nodes += before_returning_nodes
|
||||
|
||||
app.on_replace_nodes += hooks.before_replacing_node
|
||||
app.on_replace_nodes += hooks.parse_markdown
|
||||
app.on_replace_nodes += hooks.texture_sort_files
|
||||
app.on_replace_nodes += hooks.deduct_content_type
|
||||
app.on_replace_nodes += hooks.node_set_default_picture
|
||||
app.on_replaced_nodes += hooks.after_replacing_node
|
||||
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 += hooks.before_inserting_nodes
|
||||
app.on_insert_nodes += hooks.parse_markdowns
|
||||
app.on_insert_nodes += hooks.nodes_deduct_content_type
|
||||
app.on_insert_nodes += hooks.nodes_set_default_picture
|
||||
app.on_insert_nodes += hooks.textures_sort_files
|
||||
app.on_inserted_nodes += hooks.after_inserting_nodes
|
||||
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
|
||||
app.on_inserted_nodes += after_inserting_nodes
|
||||
|
||||
app.on_update_nodes += hooks.texture_sort_files
|
||||
app.on_update_nodes += texture_sort_files
|
||||
|
||||
app.on_delete_item_nodes += hooks.before_deleting_node
|
||||
app.on_deleted_item_nodes += hooks.after_deleting_node
|
||||
app.on_delete_item_nodes += before_deleting_node
|
||||
app.on_deleted_item_nodes += after_deleting_node
|
||||
|
||||
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|
||||
|
@@ -1,325 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
import urllib.parse
|
||||
from bson import ObjectId
|
||||
from flask import current_app
|
||||
from werkzeug import exceptions as wz_exceptions
|
||||
|
||||
import pillar.markdown
|
||||
from pillar.api.activities import activity_subscribe, activity_object_add
|
||||
from pillar.api.file_storage_backends.gcs import update_file_name
|
||||
from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES
|
||||
from pillar.api.utils.authorization import check_permissions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def before_returning_node(node):
|
||||
# Run validation process, since GET on nodes entry point is public
|
||||
check_permissions('nodes', node, 'GET', append_allowed_methods=True)
|
||||
|
||||
# Embed short_link_info if the node has a short_code.
|
||||
short_code = node.get('short_code')
|
||||
if short_code:
|
||||
node['short_link'] = short_link_info(short_code)['short_link']
|
||||
|
||||
|
||||
def before_returning_nodes(nodes):
|
||||
for node in nodes['_items']:
|
||||
before_returning_node(node)
|
||||
|
||||
|
||||
def only_for_node_type_decorator(*required_node_type_names):
|
||||
"""Returns a decorator that checks its first argument's node type.
|
||||
|
||||
If the node type is not of the required node type, returns None,
|
||||
otherwise calls the wrapped function.
|
||||
|
||||
>>> deco = only_for_node_type_decorator('comment')
|
||||
>>> @deco
|
||||
... def handle_comment(node): pass
|
||||
|
||||
>>> deco = only_for_node_type_decorator('comment', 'post')
|
||||
>>> @deco
|
||||
... def handle_comment_or_post(node): pass
|
||||
|
||||
"""
|
||||
|
||||
# Convert to a set for efficient 'x in required_node_type_names' queries.
|
||||
required_node_type_names = set(required_node_type_names)
|
||||
|
||||
def only_for_node_type(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def wrapper(node, *args, **kwargs):
|
||||
if node.get('node_type') not in required_node_type_names:
|
||||
return
|
||||
|
||||
return wrapped(node, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
only_for_node_type.__doc__ = "Decorator, immediately returns when " \
|
||||
"the first argument is not of type %s." % required_node_type_names
|
||||
return only_for_node_type
|
||||
|
||||
|
||||
def before_replacing_node(item, original):
|
||||
check_permissions('nodes', original, 'PUT')
|
||||
update_file_name(item)
|
||||
|
||||
|
||||
def after_replacing_node(item, original):
|
||||
"""Push an update to the Algolia index when a node item is updated. If the
|
||||
project is private, prevent public indexing.
|
||||
"""
|
||||
|
||||
from pillar.celery import search_index_tasks as index
|
||||
|
||||
projects_collection = current_app.data.driver.db['projects']
|
||||
project = projects_collection.find_one({'_id': item['project']})
|
||||
if project.get('is_private', False):
|
||||
# Skip index updating and return
|
||||
return
|
||||
|
||||
status = item['properties'].get('status', 'unpublished')
|
||||
node_id = str(item['_id'])
|
||||
|
||||
if status == 'published':
|
||||
index.node_save.delay(node_id)
|
||||
else:
|
||||
index.node_delete.delay(node_id)
|
||||
|
||||
|
||||
def before_inserting_nodes(items):
|
||||
"""Before inserting a node in the collection we check if the user is allowed
|
||||
and we append the project id to it.
|
||||
"""
|
||||
from pillar.auth import current_user
|
||||
|
||||
nodes_collection = current_app.data.driver.db['nodes']
|
||||
|
||||
def find_parent_project(node):
|
||||
"""Recursive function that finds the ultimate parent of a node."""
|
||||
if node and 'parent' in node:
|
||||
parent = nodes_collection.find_one({'_id': node['parent']})
|
||||
return find_parent_project(parent)
|
||||
if node:
|
||||
return node
|
||||
else:
|
||||
return None
|
||||
|
||||
for item in items:
|
||||
check_permissions('nodes', item, 'POST')
|
||||
if 'parent' in item and 'project' not in item:
|
||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||
project = find_parent_project(parent)
|
||||
if project:
|
||||
item['project'] = project['_id']
|
||||
|
||||
# Default the 'user' property to the current user.
|
||||
item.setdefault('user', current_user.user_id)
|
||||
|
||||
|
||||
def after_inserting_nodes(items):
|
||||
for item in items:
|
||||
# Skip subscriptions for first level items (since the context is not a
|
||||
# node, but a project).
|
||||
# TODO: support should be added for mixed context
|
||||
if 'parent' not in item:
|
||||
return
|
||||
context_object_id = item['parent']
|
||||
if item['node_type'] == 'comment':
|
||||
nodes_collection = current_app.data.driver.db['nodes']
|
||||
parent = nodes_collection.find_one({'_id': item['parent']})
|
||||
# Always subscribe to the parent node
|
||||
activity_subscribe(item['user'], 'node', item['parent'])
|
||||
if parent['node_type'] == 'comment':
|
||||
# If the parent is a comment, we provide its own parent as
|
||||
# context. We do this in order to point the user to an asset
|
||||
# or group when viewing the notification.
|
||||
verb = 'replied'
|
||||
context_object_id = parent['parent']
|
||||
# Subscribe to the parent of the parent comment (post or group)
|
||||
activity_subscribe(item['user'], 'node', parent['parent'])
|
||||
else:
|
||||
activity_subscribe(item['user'], 'node', item['_id'])
|
||||
verb = 'commented'
|
||||
elif item['node_type'] in PILLAR_NAMED_NODE_TYPES:
|
||||
verb = 'posted'
|
||||
activity_subscribe(item['user'], 'node', item['_id'])
|
||||
else:
|
||||
# Don't automatically create activities for non-Pillar node types,
|
||||
# as we don't know what would be a suitable verb (among other things).
|
||||
continue
|
||||
|
||||
activity_object_add(
|
||||
item['user'],
|
||||
verb,
|
||||
'node',
|
||||
item['_id'],
|
||||
'node',
|
||||
context_object_id
|
||||
)
|
||||
|
||||
|
||||
def deduct_content_type(node_doc, original=None):
|
||||
"""Deduct the content type from the attached file, if any."""
|
||||
|
||||
if node_doc['node_type'] != 'asset':
|
||||
log.debug('deduct_content_type: called on node type %r, ignoring', node_doc['node_type'])
|
||||
return
|
||||
|
||||
node_id = node_doc.get('_id')
|
||||
try:
|
||||
file_id = ObjectId(node_doc['properties']['file'])
|
||||
except KeyError:
|
||||
if node_id is None:
|
||||
# Creation of a file-less node is allowed, but updates aren't.
|
||||
return
|
||||
log.warning('deduct_content_type: Asset without properties.file, rejecting.')
|
||||
raise wz_exceptions.UnprocessableEntity('Missing file property for asset node')
|
||||
|
||||
files = current_app.data.driver.db['files']
|
||||
file_doc = files.find_one({'_id': file_id},
|
||||
{'content_type': 1})
|
||||
if not file_doc:
|
||||
log.warning('deduct_content_type: Node %s refers to non-existing file %s, rejecting.',
|
||||
node_id, file_id)
|
||||
raise wz_exceptions.UnprocessableEntity('File property refers to non-existing file')
|
||||
|
||||
# Guess the node content type from the file content type
|
||||
file_type = file_doc['content_type']
|
||||
if file_type.startswith('video/'):
|
||||
content_type = 'video'
|
||||
elif file_type.startswith('image/'):
|
||||
content_type = 'image'
|
||||
else:
|
||||
content_type = 'file'
|
||||
|
||||
node_doc['properties']['content_type'] = content_type
|
||||
|
||||
|
||||
def nodes_deduct_content_type(nodes):
|
||||
for node in nodes:
|
||||
deduct_content_type(node)
|
||||
|
||||
|
||||
def node_set_default_picture(node, original=None):
|
||||
"""Uses the image of an image asset or colour map of texture node as picture."""
|
||||
|
||||
if node.get('picture'):
|
||||
log.debug('Node %s already has a picture, not overriding', node.get('_id'))
|
||||
return
|
||||
|
||||
node_type = node.get('node_type')
|
||||
props = node.get('properties', {})
|
||||
content = props.get('content_type')
|
||||
|
||||
if node_type == 'asset' and content == 'image':
|
||||
image_file_id = props.get('file')
|
||||
elif node_type == 'texture':
|
||||
# Find the colour map, defaulting to the first image map available.
|
||||
image_file_id = None
|
||||
for image in props.get('files', []):
|
||||
if image_file_id is None or image.get('map_type') == 'color':
|
||||
image_file_id = image.get('file')
|
||||
else:
|
||||
log.debug('Not setting default picture on node type %s content type %s',
|
||||
node_type, content)
|
||||
return
|
||||
|
||||
if image_file_id is None:
|
||||
log.debug('Nothing to set the picture to.')
|
||||
return
|
||||
|
||||
log.debug('Setting default picture for node %s to %s', node.get('_id'), image_file_id)
|
||||
node['picture'] = image_file_id
|
||||
|
||||
|
||||
def nodes_set_default_picture(nodes):
|
||||
for node in nodes:
|
||||
node_set_default_picture(node)
|
||||
|
||||
|
||||
def before_deleting_node(node: dict):
|
||||
check_permissions('nodes', node, 'DELETE')
|
||||
|
||||
|
||||
def after_deleting_node(item):
|
||||
from pillar.celery import search_index_tasks as index
|
||||
index.node_delete.delay(str(item['_id']))
|
||||
|
||||
|
||||
only_for_textures = only_for_node_type_decorator('texture')
|
||||
|
||||
|
||||
@only_for_textures
|
||||
def texture_sort_files(node, original=None):
|
||||
"""Sort files alphabetically by map type, with colour map first."""
|
||||
|
||||
try:
|
||||
files = node['properties']['files']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
# Sort the map types alphabetically, ensuring 'color' comes first.
|
||||
as_dict = {f['map_type']: f for f in files}
|
||||
types = sorted(as_dict.keys(), key=lambda k: '\0' if k == 'color' else k)
|
||||
node['properties']['files'] = [as_dict[map_type] for map_type in types]
|
||||
|
||||
|
||||
def textures_sort_files(nodes):
|
||||
for node in nodes:
|
||||
texture_sort_files(node)
|
||||
|
||||
|
||||
def parse_markdown(node, original=None):
|
||||
import copy
|
||||
|
||||
projects_collection = current_app.data.driver.db['projects']
|
||||
project = projects_collection.find_one({'_id': node['project']}, {'node_types': 1})
|
||||
# Query node type directly using the key
|
||||
node_type = next(nt for nt in project['node_types']
|
||||
if nt['name'] == node['node_type'])
|
||||
|
||||
# Create a copy to not overwrite the actual schema.
|
||||
schema = copy.deepcopy(current_app.config['DOMAIN']['nodes']['schema'])
|
||||
schema['properties'] = node_type['dyn_schema']
|
||||
|
||||
def find_markdown_fields(schema, node):
|
||||
"""Find and process all makrdown validated fields."""
|
||||
for k, v in schema.items():
|
||||
if not isinstance(v, dict):
|
||||
continue
|
||||
|
||||
if v.get('validator') == 'markdown':
|
||||
# If there is a match with the validator: markdown pair, assign the sibling
|
||||
# property (following the naming convention _<property>_html)
|
||||
# the processed value.
|
||||
if k in node:
|
||||
html = pillar.markdown.markdown(node[k])
|
||||
field_name = pillar.markdown.cache_field_name(k)
|
||||
node[field_name] = html
|
||||
if isinstance(node, dict) and k in node:
|
||||
find_markdown_fields(v, node[k])
|
||||
|
||||
find_markdown_fields(schema, node)
|
||||
|
||||
return 'ok'
|
||||
|
||||
|
||||
def parse_markdowns(items):
|
||||
for item in items:
|
||||
parse_markdown(item)
|
||||
|
||||
|
||||
def short_link_info(short_code):
|
||||
"""Returns the short link info in a dict."""
|
||||
|
||||
short_link = urllib.parse.urljoin(
|
||||
current_app.config['SHORT_LINK_BASE_URL'], short_code)
|
||||
|
||||
return {
|
||||
'short_code': short_code,
|
||||
'short_link': short_link,
|
||||
}
|
@@ -162,12 +162,9 @@ class YouTube:
|
||||
if not youtube_id:
|
||||
return html_module.escape('{youtube invalid YouTube ID/URL}')
|
||||
|
||||
src = f'https://www.youtube.com/embed/{youtube_id}?rel=0'
|
||||
html = f'<div class="embed-responsive embed-responsive-16by9">' \
|
||||
f'<iframe class="shortcode youtube embed-responsive-item"' \
|
||||
f' width="{width}" height="{height}" src="{src}"' \
|
||||
f' frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>' \
|
||||
f'</div>'
|
||||
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>'
|
||||
return html
|
||||
|
||||
|
||||
@@ -228,25 +225,12 @@ class Attachment:
|
||||
|
||||
return self.render(file_doc, pargs, kwargs)
|
||||
|
||||
def sdk_file(self, slug: str, document: dict) -> pillarsdk.File:
|
||||
def sdk_file(self, slug: str, node_properties: dict) -> pillarsdk.File:
|
||||
"""Return the file document for the attachment with this slug."""
|
||||
|
||||
from pillar.web import system_util
|
||||
|
||||
# TODO (fsiddi) Make explicit what 'document' is.
|
||||
# In some cases we pass the entire node or project documents, in other cases
|
||||
# we pass node.properties. This should be unified at the level of do_markdown.
|
||||
# For now we do a quick hack and first look for 'properties' in the doc,
|
||||
# then we look for 'attachments'.
|
||||
|
||||
doc_properties = document.get('properties')
|
||||
if doc_properties:
|
||||
# We passed an entire document (all nodes must have 'properties')
|
||||
attachments = doc_properties.get('attachments', {})
|
||||
else:
|
||||
# The value of document could have been defined as 'node.properties'
|
||||
attachments = document.get('attachments', {})
|
||||
|
||||
attachments = node_properties.get('properties', {}).get('attachments', {})
|
||||
attachment = attachments.get(slug)
|
||||
if not attachment:
|
||||
raise self.NoSuchSlug(slug)
|
||||
|
@@ -61,10 +61,16 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
||||
post.picture = get_file(post.picture, api=api)
|
||||
post.url = url_for_node(node=post)
|
||||
|
||||
# Use the *_main_project.html template for the main blog
|
||||
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
|
||||
main_project_template = '_main_project' if is_main_project else ''
|
||||
main_project_template = '_main_project'
|
||||
index_arch = 'archive' if archive else 'index'
|
||||
template_path = f'nodes/custom/blog/{index_arch}.html',
|
||||
template_path = f'nodes/custom/blog/{index_arch}{main_project_template}.html',
|
||||
|
||||
if url:
|
||||
template_path = f'nodes/custom/post/view{main_project_template}.html',
|
||||
|
||||
post = Node.find_one({
|
||||
'where': {'parent': blog._id, 'properties.url': url},
|
||||
'embedded': {'node_type': 1, 'user': 1},
|
||||
@@ -89,7 +95,6 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
||||
can_create_blog_posts = project.node_type_has_method('post', 'POST', api=api)
|
||||
|
||||
# Use functools.partial so we can later pass page=X.
|
||||
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
|
||||
if is_main_project:
|
||||
url_func = functools.partial(url_for, 'main.main_blog_archive')
|
||||
else:
|
||||
@@ -116,7 +121,7 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
|
||||
return render_template(
|
||||
template_path,
|
||||
blog=blog,
|
||||
node=post, # node is used by the generic comments rendering (see custom/_scripts.pug)
|
||||
node=post,
|
||||
posts=posts._items,
|
||||
posts_meta=pmeta,
|
||||
more_posts_available=pmeta['total'] > pmeta['max_results'],
|
||||
|
109
pillar/web/static/assets/js/vendor/videojs.endcard.js
vendored
Normal file
109
pillar/web/static/assets/js/vendor/videojs.endcard.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
(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);
|
@@ -403,6 +403,7 @@ nav.sidebar
|
||||
$loader-bar-width: 100px
|
||||
$loader-bar-height: 2px
|
||||
.loader-bar
|
||||
background-color: $color-background
|
||||
bottom: 0
|
||||
content: ''
|
||||
display: none
|
||||
@@ -411,12 +412,10 @@ $loader-bar-height: 2px
|
||||
position: absolute
|
||||
visibility: hidden
|
||||
width: 100%
|
||||
z-index: 20
|
||||
|
||||
&:before
|
||||
animation: none
|
||||
background-color: $primary
|
||||
background-image: linear-gradient(to right, $primary-accent, $primary)
|
||||
content: ''
|
||||
display: block
|
||||
height: $loader-bar-height
|
||||
@@ -454,13 +453,3 @@ $loader-bar-height: 2px
|
||||
|
||||
.progress-bar
|
||||
background-color: $primary
|
||||
background-image: linear-gradient(to right, $primary-accent, $primary)
|
||||
|
||||
.node-details-description
|
||||
+node-details-description
|
||||
|
||||
@include media-breakpoint-up(lg)
|
||||
max-width: map-get($grid-breakpoints, "md")
|
||||
|
||||
@include media-breakpoint-up(xl)
|
||||
max-width: map-get($grid-breakpoints, "lg")
|
||||
|
@@ -1,9 +1,7 @@
|
||||
$comments-width-max: 710px
|
||||
|
||||
.comments-container
|
||||
max-width: $comments-width-max
|
||||
position: relative
|
||||
width: 100%
|
||||
|
||||
#comments-reload
|
||||
text-align: center
|
||||
@@ -316,6 +314,9 @@ $comments-width-max: 710px
|
||||
color: $color-success
|
||||
|
||||
.comment-reply
|
||||
&-container
|
||||
background-color: $color-background
|
||||
|
||||
/* Little gravatar icon on the left */
|
||||
&-avatar
|
||||
img
|
||||
@@ -332,7 +333,7 @@ $comments-width-max: 710px
|
||||
width: 100%
|
||||
|
||||
&-field
|
||||
background-color: $color-background-light
|
||||
background-color: $color-background-dark
|
||||
border-radius: 3px
|
||||
box-shadow: inset 0 0 2px 0 rgba(darken($color-background-dark, 20%), .5)
|
||||
display: flex
|
||||
@@ -341,7 +342,6 @@ $comments-width-max: 710px
|
||||
|
||||
textarea
|
||||
+node-details-description
|
||||
background-color: $color-background-light
|
||||
border-bottom-right-radius: 0
|
||||
border-top-right-radius: 0
|
||||
border: none
|
||||
@@ -376,6 +376,7 @@ $comments-width-max: 710px
|
||||
|
||||
&.filled
|
||||
textarea
|
||||
background-color: $color-background-light
|
||||
border-bottom: thin solid $color-background
|
||||
|
||||
&:focus
|
||||
|
@@ -30,7 +30,6 @@ $color-primary: #009eff !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
|
||||
$primary-accent: #0bd
|
||||
|
||||
$color-secondary: #f42942 !default
|
||||
$color-secondary-light: hsl(hue($color-secondary), 30%, 90%) !default
|
||||
@@ -157,7 +156,3 @@ $tooltip-max-width: auto
|
||||
$tooltip-opacity: 1
|
||||
|
||||
$nav-link-height: 37px
|
||||
$navbar-padding-x: 0
|
||||
$navbar-padding-y: 0
|
||||
|
||||
$grid-breakpoints: (xs: 0,sm: 576px,md: 768px,lg: 1100px,xl: 1500px, xxl: 1800px)
|
||||
|
@@ -24,16 +24,13 @@
|
||||
color: $color-secondary
|
||||
|
||||
#notifications-toggle
|
||||
color: $color-text
|
||||
cursor: pointer
|
||||
font-size: 1.5em
|
||||
position: relative
|
||||
user-select: none
|
||||
|
||||
> i:before
|
||||
content: '\e815'
|
||||
font-size: 1.3em
|
||||
position: relative
|
||||
top: 2px
|
||||
|
||||
&.has-notifications
|
||||
> i:before
|
||||
|
@@ -77,8 +77,6 @@ body.workshops
|
||||
|
||||
a
|
||||
color: $primary
|
||||
i
|
||||
+active-gradient
|
||||
|
||||
a
|
||||
align-items: center
|
||||
@@ -651,6 +649,9 @@ section.node-details-container
|
||||
width: 100%
|
||||
max-width: 100%
|
||||
|
||||
.node-details-description
|
||||
+node-details-description
|
||||
|
||||
.node-details-meta
|
||||
> ul
|
||||
align-items: center
|
||||
@@ -1775,7 +1776,7 @@ a.learn-more
|
||||
box-shadow: 0 5px 35px rgba(black, .2)
|
||||
color: $color-text-dark-primary
|
||||
position: absolute
|
||||
top: -$project_header-height
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
width: 80%
|
||||
@@ -1803,7 +1804,7 @@ a.learn-more
|
||||
&.visible
|
||||
visibility: visible
|
||||
opacity: 1
|
||||
top: 0
|
||||
top: $project_header-height
|
||||
|
||||
.overlay-container
|
||||
.title
|
||||
|
@@ -95,7 +95,7 @@ $search-hit-width_grid: 100px
|
||||
.search-list
|
||||
width: 30%
|
||||
|
||||
.card-deck.card-deck-vertical
|
||||
.card-deck.card-deck-horizontal
|
||||
.card .embed-responsive
|
||||
max-width: 80px
|
||||
|
||||
|
@@ -67,6 +67,131 @@
|
||||
&:hover
|
||||
background-color: lighten($provider-color-google, 7%)
|
||||
|
||||
#settings
|
||||
#settings-sidebar
|
||||
+media-xs
|
||||
width: 100%
|
||||
|
||||
+container-box
|
||||
background-color: $color-background-light
|
||||
color: $color-text
|
||||
margin-right: 15px
|
||||
width: 30%
|
||||
|
||||
.settings-content
|
||||
padding: 0
|
||||
|
||||
ul
|
||||
list-style: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
a
|
||||
&:hover
|
||||
text-decoration: none
|
||||
|
||||
li
|
||||
background-color: lighten($color-background, 5%)
|
||||
|
||||
li
|
||||
border-bottom: thin solid $color-background
|
||||
border-left: thick solid transparent
|
||||
margin: 0
|
||||
padding: 25px
|
||||
transition: all 100ms ease-in-out
|
||||
|
||||
i
|
||||
font-size: 1.1em
|
||||
padding-right: 15px
|
||||
|
||||
.active
|
||||
li
|
||||
background-color: lighten($color-background, 5%)
|
||||
border-left: thick solid $color-info
|
||||
|
||||
|
||||
#settings-container
|
||||
+media-xs
|
||||
width: 100%
|
||||
|
||||
+container-box
|
||||
background-color: $color-background-light
|
||||
width: 70%
|
||||
|
||||
.settings-header
|
||||
background-color: $color-background
|
||||
border-top-left-radius: 3px
|
||||
border-top-right-radius: 3px
|
||||
|
||||
.settings-title
|
||||
font:
|
||||
size: 1.5em
|
||||
weight: 300
|
||||
padding: 10px 15px 10px 25px
|
||||
|
||||
.settings-content
|
||||
padding: 25px
|
||||
|
||||
.settings-billing-info
|
||||
font-size: 1.2em
|
||||
|
||||
.subscription-active
|
||||
color: $color-success
|
||||
padding-bottom: 20px
|
||||
.subscription-demo
|
||||
color: $color-info
|
||||
margin-top: 0
|
||||
.subscription-missing
|
||||
color: $color-danger
|
||||
margin-top: 0
|
||||
|
||||
.button-submit
|
||||
clear: both
|
||||
display: block
|
||||
min-width: 200px
|
||||
margin: 0 auto
|
||||
+button($color-primary, 3px, true)
|
||||
|
||||
|
||||
#settings-container
|
||||
#settings-form
|
||||
width: 100%
|
||||
|
||||
|
||||
.settings-form
|
||||
align-items: center
|
||||
display: flex
|
||||
justify-content: center
|
||||
|
||||
.left, .right
|
||||
padding: 25px 0
|
||||
|
||||
.left
|
||||
width: 60%
|
||||
float: left
|
||||
|
||||
.right
|
||||
width: 40%
|
||||
float: right
|
||||
text-align: center
|
||||
|
||||
label
|
||||
color: $color-text
|
||||
display: block
|
||||
|
||||
.settings-avatar
|
||||
img
|
||||
border-radius: 3px
|
||||
|
||||
span
|
||||
display: block
|
||||
padding: 15px 0
|
||||
font:
|
||||
size: .9em
|
||||
|
||||
.settings-password
|
||||
color: $color-text-dark-primary
|
||||
|
||||
|
||||
#user-edit-container
|
||||
padding: 15px
|
||||
|
@@ -171,25 +171,17 @@
|
||||
/* Small but wide: phablets, iPads
|
||||
** Menu is collapsed, columns stack, no brand */
|
||||
=media-sm
|
||||
@include media-breakpoint-up(sm)
|
||||
@media (min-width: #{$screen-tablet}) and (max-width: #{$screen-desktop - 1px})
|
||||
@content
|
||||
|
||||
/* Tablets portrait.
|
||||
** Menu is expanded, but columns stack, brand is shown */
|
||||
=media-md
|
||||
@include media-breakpoint-up(md)
|
||||
@media (min-width: #{$screen-desktop})
|
||||
@content
|
||||
|
||||
=media-lg
|
||||
@include media-breakpoint-up(lg)
|
||||
@content
|
||||
|
||||
=media-xl
|
||||
@include media-breakpoint-up(xl)
|
||||
@content
|
||||
|
||||
=media-xxl
|
||||
@include media-breakpoint-up(xxl)
|
||||
@media (min-width: #{$screen-lg-desktop})
|
||||
@content
|
||||
|
||||
=media-print
|
||||
@@ -667,9 +659,6 @@
|
||||
.user-select-none
|
||||
user-select: none
|
||||
|
||||
.pointer-events-none
|
||||
pointer-events: none
|
||||
|
||||
// Bootstrap has .img-fluid, a class to limit the width of an image to 100%.
|
||||
// .imgs-fluid below is to be applied on a parent container when we can't add
|
||||
// classes to the images themselves. e.g. the blog.
|
||||
@@ -680,15 +669,3 @@
|
||||
|
||||
.overflow-hidden
|
||||
overflow: hidden
|
||||
|
||||
=text-gradient($color_from, $color_to)
|
||||
background: linear-gradient(to right, $color_from, $color_to)
|
||||
background-clip: text
|
||||
-webkit-background-clip: text
|
||||
-webkit-text-fill-color: transparent
|
||||
|
||||
=active-gradient
|
||||
+text-gradient($primary-accent, $primary)
|
||||
|
||||
&:before
|
||||
+text-gradient($primary-accent, $primary)
|
||||
|
@@ -15,39 +15,85 @@
|
||||
|
||||
@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/card"
|
||||
@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 _notifications
|
||||
@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
|
||||
|
||||
#blog_container
|
||||
+media-xs
|
||||
flex-direction: column
|
||||
padding-top: 0
|
||||
|
||||
video
|
||||
max-width: 100%
|
||||
|
||||
#blog_post-edit-form
|
||||
padding: 20px
|
||||
@@ -120,6 +166,7 @@
|
||||
margin-bottom: 15px
|
||||
border-top: thin solid $color-text-dark-hint
|
||||
|
||||
|
||||
.form-group.description,
|
||||
.form-group.summary,
|
||||
.form-group.content
|
||||
@@ -189,8 +236,62 @@
|
||||
|
||||
#blog_post-create-container,
|
||||
#blog_post-edit-container
|
||||
+container-box
|
||||
padding: 25px
|
||||
|
||||
.blog_index-item
|
||||
.item-picture
|
||||
position: relative
|
||||
width: 100%
|
||||
max-height: 350px
|
||||
min-height: 200px
|
||||
height: auto
|
||||
overflow: hidden
|
||||
border-top-left-radius: 3px
|
||||
border-top-right-radius: 3px
|
||||
+clearfix
|
||||
|
||||
img
|
||||
+position-center-translate
|
||||
width: 100%
|
||||
border-top-left-radius: 3px
|
||||
border-top-right-radius: 3px
|
||||
|
||||
+media-xs
|
||||
min-height: 150px
|
||||
+media-sm
|
||||
min-height: 150px
|
||||
+media-md
|
||||
min-height: 250px
|
||||
+media-lg
|
||||
min-height: 250px
|
||||
|
||||
.item-content
|
||||
+node-details-description
|
||||
|
||||
+media-xs
|
||||
padding:
|
||||
left: 0
|
||||
right: 0
|
||||
|
||||
img
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.item-meta
|
||||
color: $color-text-dark-secondary
|
||||
padding:
|
||||
left: 25px
|
||||
right: 25px
|
||||
|
||||
+media-xs
|
||||
padding:
|
||||
left: 10px
|
||||
right: 10px
|
||||
|
||||
#blog_index-container,
|
||||
#blog_post-create-container,
|
||||
#blog_post-edit-container
|
||||
+container-box
|
||||
width: 75%
|
||||
|
||||
+media-xs
|
||||
@@ -205,6 +306,11 @@
|
||||
+media-lg
|
||||
width: 100%
|
||||
|
||||
|
||||
.item-picture+.button-back+.button-edit
|
||||
right: 20px
|
||||
top: 20px
|
||||
|
||||
#blog_post-edit-form
|
||||
padding: 0
|
||||
|
||||
@@ -239,3 +345,206 @@
|
||||
|
||||
.form-upload-file-meta
|
||||
width: initial
|
||||
|
||||
#blog_post-edit-title
|
||||
padding: 0
|
||||
color: $color-text
|
||||
font:
|
||||
size: 1.8em
|
||||
weight: 300
|
||||
margin: 0 20px 15px 0
|
||||
|
||||
#blog_index-sidebar
|
||||
width: 25%
|
||||
padding: 0 15px
|
||||
|
||||
+media-xs
|
||||
width: 100%
|
||||
clear: both
|
||||
display: block
|
||||
margin-top: 25px
|
||||
+media-sm
|
||||
width: 40%
|
||||
+media-md
|
||||
width: 30%
|
||||
+media-lg
|
||||
width: 25%
|
||||
|
||||
.button-back
|
||||
+button($color-info, 6px, true)
|
||||
display: block
|
||||
width: 100%
|
||||
margin: 15px 0 0 0
|
||||
|
||||
#blog_post-edit-form
|
||||
.form-group
|
||||
.form-control
|
||||
background-color: white
|
||||
|
||||
.blog_index-sidebar,
|
||||
.blog_project-sidebar
|
||||
+container-box
|
||||
background-color: lighten($color-background, 5%)
|
||||
padding: 20px
|
||||
|
||||
.blog_project-card
|
||||
position: relative
|
||||
width: 100%
|
||||
border-radius: 3px
|
||||
overflow: hidden
|
||||
background-color: white
|
||||
color: lighten($color-text, 10%)
|
||||
box-shadow: 0 0 30px rgba(black, .2)
|
||||
|
||||
margin:
|
||||
top: 0
|
||||
bottom: 15px
|
||||
left: auto
|
||||
right: auto
|
||||
|
||||
|
||||
a.item-header
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100px
|
||||
display: block
|
||||
background-size: 100% 100%
|
||||
|
||||
overflow: hidden
|
||||
|
||||
.overlay
|
||||
z-index: 1
|
||||
width: 100%
|
||||
height: 100px
|
||||
@include overlay(transparent, 0%, white, 100%)
|
||||
|
||||
|
||||
img.background
|
||||
width: 100%
|
||||
transform: scale(1.4)
|
||||
|
||||
.card-thumbnail
|
||||
position: absolute
|
||||
z-index: 2
|
||||
height: 90px
|
||||
width: 90px
|
||||
display: block
|
||||
top: 35px
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
background-color: white
|
||||
border-radius: 3px
|
||||
overflow: hidden
|
||||
|
||||
&:hover
|
||||
img.thumb
|
||||
opacity: .9
|
||||
|
||||
img.thumb
|
||||
width: 100%
|
||||
border-radius: 3px
|
||||
transition: opacity 150ms ease-in-out
|
||||
+position-center-translate
|
||||
|
||||
.item-info
|
||||
padding: 10px 20px
|
||||
background-color: white
|
||||
border-bottom-left-radius: 3px
|
||||
border-bottom-right-radius: 3px
|
||||
|
||||
a.item-title
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 30px 0 15px 0
|
||||
color: $color-text-dark
|
||||
text-align: center
|
||||
font:
|
||||
size: 1.6em
|
||||
weight: 300
|
||||
|
||||
transition: color 150ms ease-in-out
|
||||
|
||||
&:hover
|
||||
text-decoration: none
|
||||
color: $color-primary
|
||||
|
||||
.blog-archive-navigation
|
||||
+media-xs
|
||||
font-size: 1em
|
||||
max-width: initial
|
||||
|
||||
border-bottom: thin solid $color-background-dark
|
||||
display: flex
|
||||
font:
|
||||
size: 1.2em
|
||||
weight: 300
|
||||
margin: 0 auto
|
||||
max-width: 780px
|
||||
text-align: center
|
||||
+text-overflow-ellipsis
|
||||
|
||||
&:last-child
|
||||
border: none
|
||||
|
||||
a, span
|
||||
+media-xs
|
||||
padding: 10px
|
||||
|
||||
flex: 1
|
||||
padding: 25px 15px
|
||||
|
||||
span
|
||||
color: $color-text-dark-secondary
|
||||
pointer-events: none
|
||||
|
||||
// Specific tweaks for blogs in the context of a project.
|
||||
#project_context
|
||||
.blog_index-item
|
||||
+media-xs
|
||||
margin-left: 0
|
||||
padding: 0
|
||||
margin-left: 10px
|
||||
|
||||
&.list
|
||||
margin-left: 35px !important
|
||||
|
||||
.item-title,
|
||||
.item-info
|
||||
+media-xs
|
||||
padding-left: 0
|
||||
padding-left: 25px
|
||||
|
||||
#blog_container
|
||||
.comments-container
|
||||
+media-sm
|
||||
margin-left: 10px
|
||||
margin-left: 30px
|
||||
|
||||
.blog-archive-navigation
|
||||
margin-left: 35px
|
||||
|
||||
// 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
|
||||
|
@@ -4,38 +4,24 @@
|
||||
@extend .row
|
||||
|
||||
.card
|
||||
@extend .col-md-4
|
||||
|
||||
+media-sm
|
||||
@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 33%
|
||||
max-width: 33%
|
||||
|
||||
+media-lg
|
||||
flex: 1 0 33%
|
||||
max-width: 33%
|
||||
|
||||
+media-xl
|
||||
flex: 1 0 25%
|
||||
max-width: 25%
|
||||
|
||||
+media-xxl
|
||||
+media-lg
|
||||
flex: 1 0 20%
|
||||
max-width: 20%
|
||||
|
||||
&.card-3-columns .card
|
||||
+media-xl
|
||||
flex: 1 0 33%
|
||||
max-width: 33%
|
||||
|
||||
+media-xxl
|
||||
flex: 1 0 33%
|
||||
max-width: 33%
|
||||
|
||||
&.card-deck-vertical
|
||||
&.card-deck-horizontal
|
||||
@extend .flex-column
|
||||
flex-wrap: initial
|
||||
|
||||
@@ -43,7 +29,6 @@
|
||||
@extend .w-100
|
||||
@extend .flex-row
|
||||
flex: initial
|
||||
flex-wrap: wrap
|
||||
max-width: 100%
|
||||
|
||||
.card-img-top
|
||||
@@ -113,7 +98,6 @@
|
||||
i
|
||||
opacity: .2
|
||||
|
||||
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
||||
.card-label
|
||||
background-color: rgba($black, .5)
|
||||
border-radius: 3px
|
||||
@@ -121,7 +105,7 @@
|
||||
display: block
|
||||
font-size: $font-size-xxs
|
||||
left: 5px
|
||||
top: -27px // enough to be above the progress-bar
|
||||
top: -25px
|
||||
position: absolute
|
||||
padding: 1px 5px
|
||||
z-index: 1
|
||||
|
@@ -24,21 +24,3 @@ ul.dropdown-menu
|
||||
nav .dropdown:hover
|
||||
ul.dropdown-menu
|
||||
display: block
|
||||
|
||||
nav .dropdown.large:hover
|
||||
.dropdown-menu
|
||||
@extend .d-flex
|
||||
|
||||
.dropdown.large.show
|
||||
@extend .d-flex
|
||||
|
||||
.dropdown-menu.show
|
||||
@extend .d-flex
|
||||
|
||||
.dropdown-menu-tab
|
||||
display: none
|
||||
min-width: 100px
|
||||
|
||||
|
||||
&.show // .dropdown-menu-tab.show
|
||||
@extend .d-flex
|
||||
|
@@ -1,41 +1,26 @@
|
||||
// Mainly overrides bootstrap jumbotron settings
|
||||
.jumbotron
|
||||
@extend .d-flex
|
||||
@extend .mb-0
|
||||
@extend .rounded-0
|
||||
background-size: cover
|
||||
border-radius: 0
|
||||
margin-bottom: 0
|
||||
padding-top: 10em
|
||||
padding-bottom: 10em
|
||||
position: relative
|
||||
|
||||
&:after
|
||||
background-color: rgba(black, .5)
|
||||
bottom: 0
|
||||
content: ''
|
||||
display: none
|
||||
left: 0
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
visibility: hidden
|
||||
|
||||
// Black-transparent gradient from left to right to better read the overlay text.
|
||||
&.jumbotron-overlay
|
||||
*
|
||||
z-index: 1
|
||||
&:after
|
||||
display: block
|
||||
visibility: visible
|
||||
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
|
||||
|
||||
&.jumbotron-overlay-gradient
|
||||
*
|
||||
z-index: 1
|
||||
&:after
|
||||
background-color: transparent
|
||||
background-image: linear-gradient(45deg, rgba(black, .5) 25%, transparent 50%)
|
||||
display: block
|
||||
visibility: visible
|
||||
|
||||
h2, p
|
||||
text-shadow: 1px 1px rgba(black, .2), 1px 1px 25px rgba(black, .5)
|
||||
|
@@ -2,7 +2,8 @@
|
||||
.navbar
|
||||
box-shadow: inset 0 -2px $color-background
|
||||
|
||||
.nav
|
||||
.navbar,
|
||||
nav.sidebar
|
||||
border: none
|
||||
color: $color-text-dark-secondary
|
||||
padding: 0
|
||||
@@ -18,6 +19,29 @@
|
||||
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
|
||||
@@ -56,72 +80,49 @@
|
||||
i
|
||||
+position-center-translate
|
||||
|
||||
.dropdown
|
||||
.navbar-item
|
||||
&:hover
|
||||
box-shadow: none // Remove the blue underline usually on navbar, from dropdown items.
|
||||
.dropdown
|
||||
min-width: 50px // navbar avatar size
|
||||
|
||||
ul.dropdown-menu
|
||||
li
|
||||
a
|
||||
white-space: nowrap
|
||||
span.fa-stack
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
|
||||
.subitem // e.g. "Not Sintel? Log out"
|
||||
font-size: .8em
|
||||
text-transform: initial
|
||||
ul.dropdown-menu
|
||||
li
|
||||
a
|
||||
white-space: nowrap
|
||||
|
||||
i
|
||||
width: 30px
|
||||
&:hover
|
||||
box-shadow: none // removes underline
|
||||
|
||||
&.subscription-status
|
||||
a, a:hover
|
||||
color: $white
|
||||
.subitem // e.g. "Not Sintel? Log out"
|
||||
font-size: .8em
|
||||
text-transform: initial
|
||||
|
||||
&.none
|
||||
background-color: $color-danger
|
||||
i
|
||||
width: 30px
|
||||
|
||||
&.subscriber
|
||||
background-color: $color-success
|
||||
&.subscription-status
|
||||
a, a:hover
|
||||
color: $white
|
||||
|
||||
&.demo
|
||||
background-color: $color-info
|
||||
&.none
|
||||
background-color: $color-danger
|
||||
|
||||
span.info
|
||||
display: block
|
||||
&.subscriber
|
||||
background-color: $color-success
|
||||
|
||||
span.renew
|
||||
&.demo
|
||||
background-color: $color-info
|
||||
|
||||
span.info
|
||||
display: block
|
||||
font-size: .9em
|
||||
|
||||
|
||||
.nav-link
|
||||
@extend .d-flex
|
||||
|
||||
.nav-title
|
||||
white-space: nowrap
|
||||
|
||||
.navbar-item
|
||||
align-items: center
|
||||
display: flex
|
||||
user-select: none
|
||||
color: inherit
|
||||
|
||||
+media-sm
|
||||
padding-left: 10px
|
||||
padding-right: 10px
|
||||
|
||||
&:hover, &:focus
|
||||
color: $primary
|
||||
background-color: transparent
|
||||
box-shadow: inset 0 -3px 0 $primary
|
||||
text-decoration: none
|
||||
|
||||
&:focus
|
||||
box-shadow: inset 0 -3px 0 $primary
|
||||
|
||||
&.active
|
||||
color: $primary
|
||||
box-shadow: inset 0 -3px 0 $primary
|
||||
span.renew
|
||||
display: block
|
||||
font-size: .9em
|
||||
|
||||
|
||||
/* Secondary navigation. */
|
||||
@@ -131,36 +132,19 @@ $nav-secondary-bar-size: -2px
|
||||
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: color 150ms ease-in-out
|
||||
|
||||
&:after
|
||||
background-color: transparent
|
||||
bottom: 0
|
||||
content: ''
|
||||
height: 2px
|
||||
position: absolute
|
||||
right: 0
|
||||
left: 0
|
||||
width: 0
|
||||
transition: width 150ms ease-in-out
|
||||
transition: box-shadow 150ms ease-in-out
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active,
|
||||
.nav-item.dropdown.show > .nav-link
|
||||
.nav-item.dropdown.show .nav-link
|
||||
// Blue bar on the bottom.
|
||||
&:after
|
||||
background-color: $primary-accent
|
||||
background-image: linear-gradient(to right, $primary-accent 70%, $primary)
|
||||
height: 2px
|
||||
width: 100%
|
||||
|
||||
span
|
||||
+active-gradient
|
||||
box-shadow: inset 0 $nav-secondary-bar-size 0 0 $primary
|
||||
|
||||
i
|
||||
color: $primary-accent
|
||||
color: $primary
|
||||
|
||||
&.nav-secondary-vertical
|
||||
align-items: flex-start
|
||||
@@ -172,30 +156,19 @@ $nav-secondary-bar-size: -2px
|
||||
|
||||
// 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
|
||||
color: $primary
|
||||
@extend .bg-white
|
||||
box-shadow: inset 0 -1px 0 0 $color-background, inset ($nav-secondary-bar-size * 1.5) 0 0 0 $primary
|
||||
|
||||
&:after
|
||||
background-image: linear-gradient($primary-accent 70%, $primary)
|
||||
height: 100%
|
||||
left: initial
|
||||
top: 0
|
||||
width: 3px
|
||||
|
||||
// Big navigation dropdown.
|
||||
.nav-main
|
||||
.nav-secondary
|
||||
&.nav-main
|
||||
.nav-link
|
||||
@extend .pr-5
|
||||
box-shadow: none
|
||||
color: $color-text-dark-secondary
|
||||
|
||||
&.nav-see-more
|
||||
color: $primary
|
||||
&:hover
|
||||
color: $body-color
|
||||
|
||||
i, span
|
||||
+active-gradient
|
||||
|
||||
.navbar-overlay
|
||||
+media-lg
|
||||
@@ -215,6 +188,15 @@ $nav-secondary-bar-size: -2px
|
||||
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
|
||||
|
@@ -1365,3 +1365,28 @@ 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)
|
82
src/styles/project-main.sass
Normal file
82
src/styles/project-main.sass
Normal file
@@ -0,0 +1,82 @@
|
||||
// Bootstrap variables and utilities.
|
||||
@import "../../node_modules/bootstrap/scss/functions"
|
||||
@import "../../node_modules/bootstrap/scss/variables"
|
||||
@import "../../node_modules/bootstrap/scss/mixins"
|
||||
|
||||
@import _config
|
||||
@import _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
|
||||
@import _error
|
||||
|
||||
@import _search
|
||||
|
||||
@import plugins/_jstree
|
||||
@import plugins/_js_select2
|
@@ -43,6 +43,7 @@ html(lang="en")
|
||||
|
||||
| {% block css %}
|
||||
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
|
||||
link(href="{{ url_for('static_pillar', filename='assets/css/base.css') }}", rel="stylesheet")
|
||||
| {% if title == 'blog' %}
|
||||
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
|
||||
| {% else %}
|
||||
|
@@ -1,11 +1,10 @@
|
||||
| {% if current_user.is_authenticated %}
|
||||
|
||||
li.nav-notifications.nav-item
|
||||
a.nav-link.px-2(
|
||||
id="notifications-toggle",
|
||||
title="Notifications",
|
||||
data-toggle="tooltip",
|
||||
data-placement="bottom")
|
||||
li.nav-notifications
|
||||
a.navbar-item#notifications-toggle.px-0(
|
||||
title="Notifications",
|
||||
data-toggle="tooltip",
|
||||
data-placement="bottom")
|
||||
i.pi-notifications-none.nav-notifications-icon
|
||||
span#notifications-count
|
||||
span
|
||||
|
@@ -12,6 +12,25 @@ li.dropdown
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
| {% if not current_user.has_role('protected') %}
|
||||
| {% block menu_list %}
|
||||
li
|
||||
a.navbar-item.px-2(
|
||||
href="{{ url_for('projects.home_project') }}"
|
||||
title="Home")
|
||||
| #[i.pi-home] Home
|
||||
|
||||
li
|
||||
a.navbar-item.px-2(
|
||||
href="{{ url_for('projects.index') }}"
|
||||
title="My Projects")
|
||||
| #[i.pi-star] My Projects
|
||||
|
||||
| {% if current_user.has_organizations() %}
|
||||
li
|
||||
a.navbar-item.px-2(
|
||||
href="{{ url_for('pillar.web.organizations.index') }}"
|
||||
title="My Organizations")
|
||||
| #[i.pi-users] My Organizations
|
||||
| {% endif %}
|
||||
|
||||
li
|
||||
a.navbar-item.px-2(
|
||||
|
@@ -6,7 +6,7 @@
|
||||
// #}
|
||||
mixin jumbotron(title, text, image, url)
|
||||
if url
|
||||
a.jumbotron.text-white(
|
||||
a.jumbotron.jumbotron-overlay.text-white(
|
||||
style='background-image: url(' + image + ');',
|
||||
href=url)&attributes(attributes)
|
||||
.container
|
||||
@@ -19,7 +19,7 @@ mixin jumbotron(title, text, image, url)
|
||||
.lead
|
||||
=text
|
||||
else
|
||||
.jumbotron.text-white(style='background-image: url(' + image + ');')&attributes(attributes)
|
||||
.jumbotron.jumbotron-overlay.text-white(style='background-image: url(' + image + ');')&attributes(attributes)
|
||||
.container
|
||||
.row
|
||||
.col-md-9
|
||||
@@ -35,8 +35,8 @@ mixin jumbotron(title, text, image, url)
|
||||
mixin nav-secondary(title)
|
||||
ul.nav.nav-secondary&attributes(attributes)
|
||||
if title
|
||||
li.nav-item
|
||||
span.nav-title.nav-link.font-weight-bold.pointer-events-none= title
|
||||
li.font-weight-bold.px-2
|
||||
=title
|
||||
|
||||
if block
|
||||
block
|
||||
@@ -48,8 +48,8 @@ mixin nav-secondary-link()
|
||||
a.nav-link&attributes(attributes)
|
||||
block
|
||||
|
||||
mixin card-deck(max_columns)
|
||||
.card-deck.card-padless.card-deck-responsive(class="card-" + max_columns + "-columns")&attributes(attributes)
|
||||
mixin card-deck()
|
||||
.card-deck.card-padless.card-deck-responsive()&attributes(attributes)
|
||||
if block
|
||||
block
|
||||
else
|
||||
|
@@ -91,9 +91,32 @@ script(type="text/javascript").
|
||||
'fetch_progress_url': fetch_progress_url,
|
||||
});
|
||||
|
||||
this.endcard({
|
||||
getRelatedContent: getRelatedContent
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
function getRelatedContent(callback) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'end-card-content';
|
||||
el.innerHTML = '<h4><button class="js-video-play my-3 px-5 btn btn-video-overlay"><i class="pi-play"></i> Play Again</button></h4>';
|
||||
el.innerHTML += '<button class="js-video-loop px-4 btn btn-sm btn-video-overlay"><i class="pi-back"></i> Loop</button>';
|
||||
setTimeout(function(){
|
||||
callback([el])
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$('body').on('click', '.js-video-play', function(){
|
||||
videoPlayer.play();
|
||||
});
|
||||
|
||||
$('body').on('click', '.js-video-loop', function(){
|
||||
videoPlayerToggleLoop(videoPlayer, videoPlayerLoopButton);
|
||||
videoPlayer.play();
|
||||
});
|
||||
|
||||
// Generic utility to add-buttons to the player.
|
||||
function addVideoPlayerButton(data) {
|
||||
|
||||
|
@@ -1,11 +1,8 @@
|
||||
| {% extends 'nodes/custom/blog/index.html' %}
|
||||
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
|
||||
|
||||
| {% block body %}
|
||||
.container
|
||||
.pt-5.pb-2
|
||||
h2.text-uppercase.font-weight-bold.text-center
|
||||
| {{ project.name }} Blog Archive
|
||||
|
||||
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
|
||||
| {% endblock body %}
|
||||
| {% block project_context %}
|
||||
#blog_container
|
||||
#blog_index-container.expand-image-links
|
||||
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
|
||||
| {% endblock project_context%}
|
||||
|
9
src/templates/nodes/custom/blog/archive_main_project.pug
Normal file
9
src/templates/nodes/custom/blog/archive_main_project.pug
Normal file
@@ -0,0 +1,9 @@
|
||||
| {% extends 'nodes/custom/blog/index_main_project.html' %}
|
||||
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
|
||||
|
||||
| {% block body %}
|
||||
.container
|
||||
h3 Blog Archive
|
||||
|
||||
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
|
||||
| {% endblock body %}
|
@@ -1,40 +1,55 @@
|
||||
| {% extends 'layout.html' %}
|
||||
| {% extends 'projects/view.html' %}
|
||||
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
|
||||
| {% from 'projects/_macros.html' import render_secondary_navigation %}
|
||||
|
||||
| {% set title = 'blog' %}
|
||||
|
||||
| {% block page_title %}Blog{% endblock%}
|
||||
|
||||
| {% block navigation_tabs %}
|
||||
| {{ render_secondary_navigation(project, navigation_links, title) }}
|
||||
| {% endblock navigation_tabs %}
|
||||
| {% block css %}
|
||||
| {{ super() }}
|
||||
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
|
||||
| {% endblock %}
|
||||
|
||||
| {% block body %}
|
||||
| {{ blogmacros.render_blog_index(node, project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=pages) }}
|
||||
| {% block project_context %}
|
||||
| {{ blogmacros.render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta) }}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block project_tree %}
|
||||
#project_tree.jstree.jstree-default.blog
|
||||
ul.jstree-container-ul.jstree-children
|
||||
li.jstree-node(data-node-type="page")
|
||||
a.jstree-anchor(
|
||||
href="{{ url_for('projects.view', project_url=project.url) }}")
|
||||
| Browse Project
|
||||
|
||||
li.jstree-node(data-node-type="page")
|
||||
a.jstree-anchor.jstree-clicked(
|
||||
href="{{ url_for('main.project_blog', project_url=project.url) }}") Blog
|
||||
|
||||
| {% for post in posts %}
|
||||
li.jstree-node
|
||||
a.jstree-anchor.tree-item.post(
|
||||
href="{{ node.url }}")
|
||||
.tree-item-thumbnail
|
||||
| {% if post.picture %}
|
||||
img(src="{{ post.picture.thumbnail('s', api=api) }}")
|
||||
| {% else %}
|
||||
i.pi-document-text
|
||||
| {% endif %}
|
||||
span.tree-item-title {{ post.name }}
|
||||
span.tree-item-info {{ post._created | pretty_date }}
|
||||
| {% endfor %}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
|
||||
include ../_scripts
|
||||
script.
|
||||
hopToTop(); // Display jump to top button
|
||||
/* UI Stuff */
|
||||
var project_container = document.getElementById('project-container');
|
||||
|
||||
/* Expand images when their link points to a jpg/png/gif */
|
||||
/* TODO: De-duplicate code from view post */
|
||||
var page_overlay = document.getElementById('page-overlay');
|
||||
$('.item-content a img').on('click', function(e){
|
||||
e.preventDefault();
|
||||
$(window).on("load resize",function(){
|
||||
containerResizeY($(window).height());
|
||||
|
||||
var href = $(this).parent().attr('href');
|
||||
var src = $(this).attr('src');
|
||||
|
||||
if (href.match("jpg$") || href.match("png$") || href.match("gif$")) {
|
||||
$(page_overlay)
|
||||
.addClass('active')
|
||||
.html('<img src="' + src + '"/>');
|
||||
} else {
|
||||
window.location.href = href;
|
||||
if ($(window).width() > 480) {
|
||||
project_container.style.height = (window.innerHeight - project_container.offsetTop) + "px";
|
||||
}
|
||||
});
|
||||
|
||||
|
40
src/templates/nodes/custom/blog/index_main_project.pug
Normal file
40
src/templates/nodes/custom/blog/index_main_project.pug
Normal file
@@ -0,0 +1,40 @@
|
||||
| {% extends 'layout.html' %}
|
||||
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
|
||||
| {% set title = 'blog' %}
|
||||
|
||||
| {% block page_title %}Blog{% endblock%}
|
||||
|
||||
| {% block css %}
|
||||
| {{ super() }}
|
||||
link(href="{{ url_for('static_cloud', filename='assets/css/project-landing.css') }}", rel="stylesheet")
|
||||
| {% endblock css %}
|
||||
|
||||
| {% block body %}
|
||||
| {{ blogmacros.render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=pages) }}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
|
||||
include ../_scripts
|
||||
script.
|
||||
hopToTop(); // Display jump to top button
|
||||
|
||||
/* Expand images when their link points to a jpg/png/gif */
|
||||
/* TODO: De-duplicate code from view post */
|
||||
var page_overlay = document.getElementById('page-overlay');
|
||||
$('.item-content a img').on('click', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
var href = $(this).parent().attr('href');
|
||||
var src = $(this).attr('src');
|
||||
|
||||
if (href.match("jpg$") || href.match("png$") || href.match("gif$")) {
|
||||
$(page_overlay)
|
||||
.addClass('active')
|
||||
.html('<img src="' + src + '"/>');
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
|
||||
| {% endblock %}
|
@@ -5,7 +5,7 @@
|
||||
class="{% if is_reply %}is-reply{% else %}is-first{% endif %}")
|
||||
|
||||
.comment-avatar
|
||||
img(src="{{ comment._user.email | gravatar }}", alt="{{ comment._user.full_name }}")
|
||||
img(src="{{ comment._user.email | gravatar }}")
|
||||
|
||||
.comment-content
|
||||
.comment-body
|
||||
|
@@ -23,8 +23,60 @@ include ../../../mixins/components
|
||||
i.pi-list
|
||||
|
||||
+card-deck(class="px-2")
|
||||
| {% for child in children %}
|
||||
| {% for child in children %}
|
||||
| {# Browse type: List #}
|
||||
a(
|
||||
href="{{ url_for_node(node=child) }}",
|
||||
data-node_id="{{ child._id }}",
|
||||
class="js-item-open list-node-children-item browse-list")
|
||||
.list-node-children-item-thumbnail
|
||||
|
||||
| {% if child.picture %}
|
||||
img(
|
||||
src="{{ child.picture.thumbnail('t', api=api)}} ")
|
||||
| {% else %}
|
||||
.cloud-logo
|
||||
i.pi-blender-cloud
|
||||
| {% endif %}
|
||||
|
||||
| {% if child.permissions.world %}
|
||||
.list-node-children-item-ribbon
|
||||
span free
|
||||
| {% endif %}
|
||||
|
||||
.list-node-children-item-thumbnail-icon
|
||||
| {% if child.properties.content_type and child.properties.content_type == 'video' %}
|
||||
i.pi-play
|
||||
| {% elif child.properties.content_type and child.properties.content_type == 'image' %}
|
||||
i.pi-image
|
||||
| {% elif child.properties.content_type and child.properties.content_type == 'file' %}
|
||||
i.pi-file-archive
|
||||
| {% else %}
|
||||
i.pi-folder
|
||||
| {% endif %}
|
||||
|
||||
.list-node-children-item-name {{ child.name }}
|
||||
|
||||
.list-node-children-item-meta
|
||||
| {% if child.properties.status != 'published' %}
|
||||
span.status {{ child.properties.status }}
|
||||
| {% endif %}
|
||||
|
||||
span.type
|
||||
| {% if child.properties.content_type %}
|
||||
| {{ child.properties.content_type | undertitle }} ·
|
||||
| {% elif child.node_type == 'group' %}
|
||||
| Folder ·
|
||||
| {% else %}
|
||||
| {{ child.node_type | undertitle }} ·
|
||||
| {% endif %}
|
||||
|
||||
span(title="Created on {{ child._created }}") {{ child._created | pretty_date }}
|
||||
|
||||
| {# Browse type: Icon #}
|
||||
|
||||
| {{ asset_list_item(child, current_user) }}
|
||||
|
||||
| {% endfor %}
|
||||
| {% else %}
|
||||
.list-node-children-container
|
||||
@@ -63,12 +115,14 @@ include ../../../mixins/components
|
||||
|
||||
// Browse type: icon or list
|
||||
function projectBrowseTypeIcon() {
|
||||
$(".card-deck").removeClass('card-deck-vertical');
|
||||
$(".list-node-children-item.browse-list").hide();
|
||||
$(".list-node-children-item.browse-icon").show();
|
||||
$(".js-btn-browsetoggle").html('<i class="pi-list"></i> List View');
|
||||
};
|
||||
|
||||
function projectBrowseTypeList() {
|
||||
$(".card-deck").addClass('card-deck-vertical');
|
||||
$(".list-node-children-item.browse-list").show();
|
||||
$(".list-node-children-item.browse-icon").hide();
|
||||
$(".js-btn-browsetoggle").html('<i class="pi-layout"></i> Grid View');
|
||||
};
|
||||
|
||||
|
@@ -1,28 +1,28 @@
|
||||
| {% extends 'projects/landing.html' %}
|
||||
include ../../../mixins/components
|
||||
|
||||
| {% block body %}
|
||||
.expand-image-links.imgs-fluid
|
||||
| {% if node.picture %}
|
||||
+jumbotron(
|
||||
"{{ node.name }}",
|
||||
"{{ node._created | pretty_date }}{% if node.user.full_name %} · {{ node.user.full_name }}{% endif %}",
|
||||
"{{ node.picture.thumbnail('h', api=api) }}",
|
||||
"{{ node.url }}")
|
||||
| {% endif %}
|
||||
| {% if node.picture %}
|
||||
header
|
||||
img.header(src="{{ node.picture.thumbnail('h', api=api) }}")
|
||||
| {% endif %}
|
||||
| {% block navbar_secondary %}
|
||||
| {{ super() }}
|
||||
| {% endblock navbar_secondary %}
|
||||
#node-container
|
||||
#node-overlay
|
||||
|
||||
.container.pb-5
|
||||
.row
|
||||
.col-8.mx-auto
|
||||
h2.pt-5.pb-3.text-center {{node.name}}
|
||||
.node-details-container.page.expand-image-links.imgs-fluid
|
||||
|
||||
| {% if node.description %}
|
||||
.node-details-description
|
||||
| {{ node | markdowned('description') }}
|
||||
| {% endif %}
|
||||
h2.pt-3.text-center {{node.name}}
|
||||
|
||||
small.text-muted
|
||||
span(title="created {{ node._created | pretty_date }}") Updated {{ node._updated | pretty_date }}
|
||||
hr
|
||||
|
||||
| {% if node.description %}
|
||||
| {{ node | markdowned('description') }}
|
||||
| {% endif %}
|
||||
|
||||
small.text-muted
|
||||
span(title="created {{ node._created | pretty_date }}") Updated {{ node._updated | pretty_date }}
|
||||
|
||||
include ../_scripts
|
||||
|
||||
|
@@ -122,38 +122,41 @@ script(type="text/javascript").
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
var convert = new Markdown.getSanitizingConverter().makeHtml;
|
||||
|
||||
/* Build the markdown preview when typing in textarea */
|
||||
$(function() {
|
||||
var $contentField = $('.form-group.description textarea'),
|
||||
$contentPreview = $('<div class="node-edit-form-md-preview" />').insertAfter($contentField);
|
||||
|
||||
function parseDescriptionContent(content) {
|
||||
var $textarea = $('.form-group.content textarea'),
|
||||
$loader = $('<div class="md-preview-loading"><i class="pi-spin spin"></i></div>').insertAfter($textarea),
|
||||
$preview = $('<div class="node-edit-form-md-preview" />').insertAfter($loader);
|
||||
|
||||
$.ajax({
|
||||
url: "{{ url_for('nodes.preview_markdown')}}",
|
||||
type: 'post',
|
||||
data: {content: content},
|
||||
headers: {"X-CSRFToken": csrf_token},
|
||||
headers: {},
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function (data) {
|
||||
$contentPreview.html(data.content);
|
||||
})
|
||||
.fail(function (err) {
|
||||
toastr.error(xhrErrorResponseMessage(err), 'Parsing failed');
|
||||
});
|
||||
}
|
||||
$loader.hide();
|
||||
|
||||
var options = {
|
||||
callback: parseDescriptionContent,
|
||||
wait: 750,
|
||||
highlight: false,
|
||||
allowSubmit: false,
|
||||
captureLength: 2
|
||||
}
|
||||
// Delay function to not start converting heavy posts immediately
|
||||
var delay = (function(){
|
||||
var timer = 0;
|
||||
return function(callback, ms){
|
||||
clearTimeout (timer);
|
||||
timer = setTimeout(callback, ms);
|
||||
};
|
||||
})();
|
||||
|
||||
$contentField.typeWatch(options);
|
||||
$textarea.keyup(function() {
|
||||
/* If there's an iframe (YouTube embed), delay markdown convert 1.5s */
|
||||
if (/iframe/i.test($textarea.val())) {
|
||||
$loader.show();
|
||||
|
||||
delay(function(){
|
||||
// Convert markdown
|
||||
$preview.html(convert($textarea.val()));
|
||||
$loader.hide();
|
||||
}, 1500 );
|
||||
} else {
|
||||
// Convert markdown
|
||||
$preview.html(convert($textarea.val()));
|
||||
};
|
||||
}).trigger('keyup');
|
||||
});
|
||||
|
||||
$(function() {
|
||||
|
73
src/templates/nodes/custom/post/view.pug
Normal file
73
src/templates/nodes/custom/post/view.pug
Normal file
@@ -0,0 +1,73 @@
|
||||
| {% extends 'projects/view.html' %}
|
||||
| {% set title = 'blog' %}
|
||||
|
||||
| {% block og %}
|
||||
meta(property="og:title", content="{{ node.name }}")
|
||||
meta(property="og:url", content="{{ url_for('main.project_blog', project_url=project.url, url=node.properties.url, _external=True)}}")
|
||||
meta(property="og:type", content="website")
|
||||
| {% if node.picture %}
|
||||
meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
|
||||
| {% endif %}
|
||||
meta(property="og:description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
|
||||
|
||||
meta(name="twitter:title", content="{{ node.name }}")
|
||||
meta(name="twitter:description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
|
||||
| {% if node.picture %}
|
||||
meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
|
||||
| {% endif %}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block page_title %}{{node.name}} - Blog{% endblock%}
|
||||
|
||||
| {% block css %}
|
||||
| {{ super() }}
|
||||
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
|
||||
| {% endblock %}
|
||||
|
||||
| {% block project_context %}
|
||||
| {% include 'nodes/custom/post/view_embed.html' %}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block project_tree %}
|
||||
#project_tree.jstree.jstree-default.blog
|
||||
ul.jstree-container-ul.jstree-children
|
||||
li.jstree-node(data-node-type="page")
|
||||
a.jstree-anchor(
|
||||
href="{{ url_for('projects.view', project_url=project.url) }}")
|
||||
| Browse Project
|
||||
|
||||
li.jstree-node(data-node-type="page")
|
||||
a.jstree-anchor(
|
||||
href="{{ url_for('main.project_blog', project_url=project.url) }}") Blog
|
||||
|
||||
| {% for post in posts %}
|
||||
li.jstree-node
|
||||
a.jstree-anchor.tree-item.post(
|
||||
href="{{ url_for_node(node=post) }}",
|
||||
class="{% if post._id == node._id %}jstree-clicked{% endif %}")
|
||||
.tree-item-thumbnail
|
||||
| {% if post.picture %}
|
||||
img(src="{{ post.picture.thumbnail('s', api=api) }}")
|
||||
| {% else %}
|
||||
i.pi-document-text
|
||||
| {% endif %}
|
||||
span.tree-item-title {{ post.name }}
|
||||
span.tree-item-info {{ post._created | pretty_date }}
|
||||
| {% endfor %}
|
||||
| {% endblock %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
script.
|
||||
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: false, nodeId: '{{node._id}}'});
|
||||
|
||||
/* UI Stuff */
|
||||
var project_container = document.getElementById('project-container');
|
||||
$(window).on("load resize",function(){
|
||||
containerResizeY($(window).height());
|
||||
|
||||
if ($(window).width() > 480) {
|
||||
project_container.style.height = (window.innerHeight - project_container.offsetTop) + "px";
|
||||
}
|
||||
});
|
||||
|
||||
| {% endblock footer_scripts %}
|
9
src/templates/nodes/custom/post/view_embed.pug
Normal file
9
src/templates/nodes/custom/post/view_embed.pug
Normal file
@@ -0,0 +1,9 @@
|
||||
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
|
||||
|
||||
| {{ blogmacros.render_blog_post(node, project=project) }}
|
||||
|
||||
#comments-embed.comments-compact
|
||||
.comments-list-loading
|
||||
i.pi-spin
|
||||
|
||||
include ../_scripts
|
@@ -18,6 +18,11 @@ meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
|
||||
|
||||
| {% block page_title %}{{node.name}} - Blog{% endblock%}
|
||||
|
||||
| {% block css %}
|
||||
| {{ super() }}
|
||||
link(href="{{ url_for('static_cloud', filename='assets/css/project-landing.css') }}", rel="stylesheet")
|
||||
| {% endblock css %}
|
||||
|
||||
| {% set title = 'blog' %}
|
||||
|
||||
| {% block body %}
|
||||
|
@@ -68,7 +68,7 @@ script.
|
||||
|
||||
#stats.search-list-stats
|
||||
|
||||
+card-deck()(id='hits', class="h-100 m-0 pt-3 pr-2 card-deck-vertical")
|
||||
+card-deck()(id='hits', class="h-100 m-0 pt-3 pr-2 card-deck-horizontal")
|
||||
|
||||
#search-details.border-left.search-details
|
||||
#search-error
|
||||
|
@@ -25,11 +25,12 @@
|
||||
| {{ node | markdowned('description') }}
|
||||
| {% endif %}
|
||||
|
||||
|
||||
| {# DETAILS #}
|
||||
section.node-details-meta.pl-4.pr-2.py-2.border-bottom
|
||||
section.node-details-meta.px-4.py-2
|
||||
ul.list-unstyled.m-0
|
||||
| {% if node.properties.license_type %}
|
||||
li.px-2
|
||||
li
|
||||
a.node-details-license(
|
||||
href="https://creativecommons.org/licenses/",
|
||||
target="_blank",
|
||||
@@ -40,15 +41,15 @@
|
||||
| {% endif %}
|
||||
|
||||
| {% if node.has_method('PUT') and (node.properties.status != 'published') %}
|
||||
li.px-2(class="status-{{ node.properties.status }}")
|
||||
li(class="status-{{ node.properties.status }}")
|
||||
| {{ node.properties.status | undertitle }}
|
||||
| {% endif %}
|
||||
|
||||
li.px-2(title="Author")
|
||||
li(title="Author")
|
||||
| {{ node.user.full_name }}
|
||||
| {{ node.user.badges.html|safe }}
|
||||
|
||||
li.px-2(
|
||||
li(
|
||||
title="Created {{ node._created }} (updated {{ node._updated | pretty_date_time }})")
|
||||
| {{ node._created | pretty_date }}
|
||||
|
||||
@@ -59,12 +60,14 @@
|
||||
| Shared
|
||||
| {% endif %}
|
||||
|
||||
li.ml-auto
|
||||
|
||||
|
||||
li.left-side
|
||||
|
||||
| {% if node.file %}
|
||||
li.px-2(title="File size")
|
||||
li(title="File size")
|
||||
| {{ node.file.length | filesizeformat }}
|
||||
li.px-2.js-type(title="File format")
|
||||
li.js-type(title="File format")
|
||||
| {{ node.file.content_type }}
|
||||
| {% endif %}
|
||||
|
||||
@@ -92,11 +95,11 @@
|
||||
| {% endblock node_download %}
|
||||
|
||||
| {% elif current_user.has_cap('can-renew-subscription') %}
|
||||
a.btn.btn-outline-primary(
|
||||
a.btn.btn-success(
|
||||
title="Renew your subscription to download",
|
||||
target="_blank",
|
||||
href="/renew")
|
||||
i.pi-heart.pr-2
|
||||
i.pi-heart
|
||||
| Renew Subscription
|
||||
|
||||
| {% elif current_user.is_authenticated %}
|
||||
@@ -113,32 +116,12 @@
|
||||
| {% endif %}
|
||||
|
||||
| {% endblock node_details %}
|
||||
.container-fluid
|
||||
.row
|
||||
| {% block node_comments %}
|
||||
.col-md-8.col-sm-12
|
||||
#comments-embed
|
||||
.comments-list-loading
|
||||
i.pi-spin
|
||||
| {% endblock node_comments %}
|
||||
|
||||
| {% if node.properties.tags %}
|
||||
.col-md-4.d-none.d-lg-block
|
||||
script(src="{{ url_for('static_cloud', filename='assets/js/tagged_assets.min.js') }}")
|
||||
script.
|
||||
$(function() {
|
||||
$('.js-asset-list').loadTaggedAssets(4, 0);
|
||||
})
|
||||
.tagged-similar.p-3
|
||||
h6 Similar assets
|
||||
| {% for tag in node.properties.tags[:3] %}
|
||||
| {% if loop.index < 4 %}
|
||||
.card-deck.card-padless.card-deck-vertical.mx-0(
|
||||
class="js-asset-list",
|
||||
data-asset-tag="{{ tag }}")
|
||||
| {% endif %}
|
||||
| {% endfor %}
|
||||
| {% endif %}
|
||||
| {% block node_comments %}
|
||||
#comments-embed
|
||||
.comments-list-loading
|
||||
i.pi-spin
|
||||
| {% endblock node_comments %}
|
||||
|
||||
| {% include 'nodes/custom/_scripts.html' %}
|
||||
|
||||
|
@@ -1,50 +1,41 @@
|
||||
include ../mixins/components
|
||||
|
||||
| {% macro render_secondary_navigation(project, navigation_links, title) %}
|
||||
|
||||
| {% if project.category == 'course' %}
|
||||
| {% set category_url = url_for('cloud.courses') %}
|
||||
| {% elif project.category == 'workshop' %}
|
||||
| {% set category_url = url_for('cloud.workshops') %}
|
||||
| {% elif project.category == 'film' %}
|
||||
| {% set category_url = url_for('cloud.open_projects') %}
|
||||
| {% else %}
|
||||
| {% set category_url = url_for('main.homepage') %}
|
||||
| {% endif %}
|
||||
|
||||
+nav-secondary()
|
||||
| {% if project.url != 'blender-cloud' %}
|
||||
| {% if not project.is_private %}
|
||||
li.text-capitalize
|
||||
a.nav-link.text-muted.px-0(href="{{ category_url }}")
|
||||
span {{ project.category }}
|
||||
li.px-1
|
||||
i.pi-angle-right
|
||||
| {% endif %}
|
||||
|
||||
+nav-secondary-link(
|
||||
class="px-1 font-weight-bold",
|
||||
href="{{url_for('projects.view', project_url=project.url, _external=True)}}")
|
||||
span {{ project.name }}
|
||||
| {% endif %}
|
||||
|
||||
| {% for link in navigation_links %}
|
||||
+nav-secondary-link(href="{{ link['url'] }}")
|
||||
| {{ link['label'] }}
|
||||
| {% endfor %}
|
||||
|
||||
| {% if project.nodes_featured %}
|
||||
| {# In some cases featured_nodes might might be embedded #}
|
||||
| {% if '_id' in project.nodes_featured[0] %}
|
||||
| {% set featured_node_id=project.nodes_featured[0]._id %}
|
||||
| {% else %}
|
||||
| {% set featured_node_id=project.nodes_featured[0] %}
|
||||
| {% endif %}
|
||||
+nav-secondary-link(
|
||||
href="{{ url_for('projects.view_node', project_url=project.url, node_id=featured_node_id) }}",
|
||||
title="Explore {{ project.name }}",
|
||||
class="{% if title == 'project' %}active{% endif %}")
|
||||
span Explore
|
||||
| {% endif %}
|
||||
|
||||
| {% macro render_secondary_navigation(project, pages=None) %}
|
||||
nav.navbar-secondary
|
||||
nav.collapse.navbar-collapse
|
||||
ul.navbar-nav.navbar-right
|
||||
li
|
||||
a.navbar-item(
|
||||
href="{{ url_for('projects.view', project_url=project.url) }}",
|
||||
title="{{ project.name }} Homepage")
|
||||
span
|
||||
b {{ project.name }}
|
||||
li
|
||||
a.navbar-item(
|
||||
href="{{ url_for('main.project_blog', project_url=project.url) }}",
|
||||
title="Project Blog",
|
||||
class="{% if category == 'blog' %}active{% endif %}")
|
||||
span Blog
|
||||
| {% if pages %}
|
||||
| {% for p in pages %}
|
||||
li
|
||||
a.navbar-item(
|
||||
href="{{ url_for('projects.view_node', project_url=project.url, node_id=p._id) }}",
|
||||
title="{{ p.name }}",
|
||||
class="{% if category == 'page' %}active{% endif %}")
|
||||
span {{ p.name }}
|
||||
| {% endfor %}
|
||||
| {% endif %}
|
||||
| {% if project.nodes_featured %}
|
||||
| {# In some cases featured_nodes might might be embedded #}
|
||||
| {% if '_id' in project.nodes_featured[0] %}
|
||||
| {% set featured_node_id=project.nodes_featured[0]._id %}
|
||||
| {% else %}
|
||||
| {% set featured_node_id=project.nodes_featured[0] %}
|
||||
| {% endif %}
|
||||
li
|
||||
a.navbar-item(
|
||||
href="{{ url_for('projects.view_node', project_url=project.url, node_id=featured_node_id) }}",
|
||||
title="Explore {{ project.name }}",
|
||||
class="{% if category == 'blog' %}active{% endif %}")
|
||||
span Explore
|
||||
| {% endif %}
|
||||
| {% endmacro %}
|
||||
|
@@ -29,7 +29,7 @@
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-12
|
||||
h5.pl-2.mb-0.pt-3 Project Overview
|
||||
h5.pl-2.mb-0 Project Overview
|
||||
|
||||
#node-edit-container
|
||||
form(
|
||||
@@ -122,6 +122,7 @@ script(type="text/javascript").
|
||||
$('.project-mode-edit').displayAs('inline-block');
|
||||
|
||||
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: true, nodeId: ''});
|
||||
var convert = new Markdown.getSanitizingConverter().makeHtml;
|
||||
|
||||
$('.button-save').on('click', function(e){
|
||||
e.preventDefault();
|
||||
@@ -135,36 +136,36 @@ script(type="text/javascript").
|
||||
/* Build the markdown preview when typing in textarea */
|
||||
$(function() {
|
||||
|
||||
var $contentField = $('.form-group.description textarea'),
|
||||
$contentPreview = $('<div class="node-edit-form-md-preview" />').insertAfter($contentField);
|
||||
var $textarea = $('.form-group.description textarea'),
|
||||
$loader = $('<div class="md-preview-loading"><i class="pi-spin spin"></i></div>').insertAfter($textarea),
|
||||
$preview = $('<div class="node-edit-form-md-preview" />').insertAfter($loader);
|
||||
|
||||
function parseDescriptionContent(content) {
|
||||
$loader.hide();
|
||||
|
||||
$.ajax({
|
||||
url: "{{ url_for('nodes.preview_markdown')}}",
|
||||
type: 'post',
|
||||
data: {content: content},
|
||||
headers: {"X-CSRFToken": csrf_token},
|
||||
headers: {},
|
||||
dataType: 'json'
|
||||
})
|
||||
.done(function (data) {
|
||||
$contentPreview.html(data.content);
|
||||
})
|
||||
.fail(function (err) {
|
||||
toastr.error(xhrErrorResponseMessage(err), 'Parsing failed');
|
||||
});
|
||||
}
|
||||
// Delay function to not start converting heavy posts immediately
|
||||
var delay = (function(){
|
||||
var timer = 0;
|
||||
return function(callback, ms){
|
||||
clearTimeout (timer);
|
||||
timer = setTimeout(callback, ms);
|
||||
};
|
||||
})();
|
||||
|
||||
var options = {
|
||||
callback: parseDescriptionContent,
|
||||
wait: 750,
|
||||
highlight: false,
|
||||
allowSubmit: false,
|
||||
captureLength: 2
|
||||
}
|
||||
$textarea.keyup(function() {
|
||||
/* If there's an iframe (YouTube embed), delay markdown convert 1.5s */
|
||||
if (/iframe/i.test($textarea.val())) {
|
||||
$loader.show();
|
||||
|
||||
$contentField.typeWatch(options);
|
||||
delay(function(){
|
||||
// Convert markdown
|
||||
$preview.html(convert($textarea.val()));
|
||||
$loader.hide();
|
||||
}, 1500 );
|
||||
} else {
|
||||
// Convert markdown
|
||||
$preview.html(convert($textarea.val()));
|
||||
};
|
||||
}).trigger('keyup');
|
||||
|
||||
$('input, textarea').keypress(function () {
|
||||
// Unused: save status of the page as 'edited'
|
||||
|
@@ -45,7 +45,7 @@ include ../mixins/components
|
||||
#project_nav
|
||||
#project_nav-container
|
||||
// TODO - make list a macro
|
||||
#project_tree.edit.bg-light
|
||||
#project_tree.edit.bg-white
|
||||
+nav-secondary()(class="nav-secondary-vertical")
|
||||
+nav-secondary-link(
|
||||
class="{% if title == 'edit' %}active{% endif %}",
|
||||
|
@@ -1,34 +1,36 @@
|
||||
| {% extends 'layout.html' %}
|
||||
include ../../mixins/components
|
||||
|
||||
//- Don't extend this base file directly. Instead, extend page.html so that Pillar extensions
|
||||
//- can provide overrides.
|
||||
| {% block body %}
|
||||
.container.py-4
|
||||
.row
|
||||
.col-md-3
|
||||
.container
|
||||
#settings.d-flex.py-4.flex-xs-column
|
||||
#settings-sidebar
|
||||
| {% block settings_sidebar %}
|
||||
+nav-secondary('Settings')(class="nav-secondary-vertical bg-light")
|
||||
| {% block settings_sidebar_menu %}
|
||||
+nav-secondary-link(
|
||||
class="{% if title == 'profile' %}active{% endif %} border-top",
|
||||
href="{{ url_for('settings.profile') }}")
|
||||
i.pr-3.pi-vcard
|
||||
span Profile
|
||||
| {% endblock settings_sidebar_menu %}
|
||||
|
||||
| {% block settings_sidebar_menu_bottom %}
|
||||
+nav-secondary-link(
|
||||
class="{% if title == 'roles' %}active{% endif %}",
|
||||
href="{{ url_for('settings.roles') }}")
|
||||
i.pr-3.pi-cog
|
||||
span Roles & Capabilities
|
||||
| {% endblock settings_sidebar_menu_bottom %}
|
||||
.settings-header
|
||||
.settings-title Settings
|
||||
.settings-content
|
||||
ul
|
||||
| {% block settings_sidebar_menu %}
|
||||
a(class="{% if title == 'profile' %}active{% endif %}",
|
||||
href="{{ url_for('settings.profile') }}")
|
||||
li
|
||||
i.pi-vcard
|
||||
| Profile
|
||||
| {% endblock settings_sidebar_menu %}
|
||||
| {% block settings_sidebar_menu_bottom %}
|
||||
a(class="{% if title == 'roles' %}active{% endif %}",
|
||||
href="{{ url_for('settings.roles') }}")
|
||||
li
|
||||
i.pi-cog
|
||||
| Roles & Capabilities
|
||||
| {% endblock settings_sidebar_menu_bottom %}
|
||||
| {% endblock %}
|
||||
|
||||
.col-md-9
|
||||
h3.py-1 {% block settings_page_title %}{{ _("Title not set") }}{% endblock %}
|
||||
#settings-container
|
||||
.settings-header
|
||||
.settings-title {% block settings_page_title %}{{ _("Title not set") }}{% endblock %}
|
||||
|
||||
| {% block settings_page_content %}No content set, update your template.{% endblock %}
|
||||
.settings-content
|
||||
| {% block settings_page_content %}No content set, update your template.{% endblock %}
|
||||
|
||||
| {% endblock %}
|
||||
|
@@ -21,7 +21,7 @@ style.
|
||||
| {% block settings_page_content %}
|
||||
.settings-form
|
||||
form#settings-form(method='POST', action="{{url_for('settings.profile')}}")
|
||||
.pb-3
|
||||
.left
|
||||
.form-group
|
||||
| {{ form.username.label }}
|
||||
| {{ form.username(size=20, class='form-control') }}
|
||||
@@ -45,13 +45,14 @@ style.
|
||||
| {{ current_user.badges_html|safe }}
|
||||
p.hint-text Note that updates to these badges may take a few minutes to be visible here.
|
||||
| {% endif %}
|
||||
.py-3
|
||||
a(href="https://gravatar.com/")
|
||||
img.rounded-circle(src="{{ current_user.gravatar }}")
|
||||
span.p-3 {{ _("Change Gravatar") }}
|
||||
.right
|
||||
.settings-avatar
|
||||
a(href="https://gravatar.com/")
|
||||
img(src="{{ current_user.gravatar }}")
|
||||
span {{ _("Change Gravatar") }}
|
||||
|
||||
.py-3
|
||||
button.btn.btn-outline-success.px-5.button-submit(type='submit')
|
||||
i.pi-check.pr-2
|
||||
.buttons
|
||||
button.btn.btn-outline-success.button-submit(type='submit')
|
||||
i.pi-check
|
||||
| {{ _("Save Changes") }}
|
||||
| {% endblock %}
|
||||
|
@@ -479,65 +479,61 @@ class TextureSortFilesTest(AbstractPillarTest):
|
||||
|
||||
|
||||
class TaggedNodesTest(AbstractPillarTest):
|
||||
def setUp(self, **kwargs):
|
||||
super().setUp(**kwargs)
|
||||
|
||||
self.pid, _ = self.ensure_project_exists()
|
||||
self.file_id, _ = self.ensure_file_exists()
|
||||
self.uid = self.create_user()
|
||||
|
||||
from pillar.api.utils import utcnow
|
||||
self.fake_now = utcnow()
|
||||
|
||||
def test_tagged_nodes_api(self):
|
||||
from pillar.api.utils import utcnow
|
||||
from datetime import timedelta
|
||||
|
||||
pid, _ = self.ensure_project_exists()
|
||||
file_id, _ = self.ensure_file_exists()
|
||||
uid = self.create_user()
|
||||
|
||||
now = utcnow()
|
||||
base_node = {
|
||||
'name': 'Just a node name',
|
||||
'project': self.pid,
|
||||
'project': pid,
|
||||
'description': '',
|
||||
'node_type': 'asset',
|
||||
'user': self.uid,
|
||||
'user': uid,
|
||||
}
|
||||
base_props = {'status': 'published',
|
||||
'file': self.file_id,
|
||||
'file': file_id,
|
||||
'content_type': 'video',
|
||||
'order': 0}
|
||||
# No tags, should never be returned.
|
||||
self.create_node({
|
||||
'_created': self.fake_now,
|
||||
'_created': now,
|
||||
'properties': base_props,
|
||||
**base_node})
|
||||
# Empty tag list, should never be returned.
|
||||
self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=1),
|
||||
'_created': now + timedelta(seconds=1),
|
||||
'properties': {'tags': [], **base_props},
|
||||
**base_node})
|
||||
# Empty string as tag, should never be returned.
|
||||
self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=1),
|
||||
'_created': now + timedelta(seconds=1),
|
||||
'properties': {'tags': [''], **base_props},
|
||||
**base_node})
|
||||
nid_single_tag = self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=2),
|
||||
'_created': now + timedelta(seconds=2),
|
||||
# 'एनिमेशन' is 'animation' in Hindi.
|
||||
'properties': {'tags': ['एनिमेशन'], **base_props},
|
||||
**base_node,
|
||||
})
|
||||
nid_double_tag = self.create_node({
|
||||
'_created': self.fake_now + timedelta(hours=3),
|
||||
'_created': now + timedelta(hours=3),
|
||||
'properties': {'tags': ['एनिमेशन', 'rigging'], **base_props},
|
||||
**base_node,
|
||||
})
|
||||
nid_other_tag = self.create_node({
|
||||
'_deleted': False,
|
||||
'_created': self.fake_now + timedelta(days=4),
|
||||
'_created': now + timedelta(days=4),
|
||||
'properties': {'tags': ['producción'], **base_props},
|
||||
**base_node,
|
||||
})
|
||||
# Matching tag but deleted node, should never be returned.
|
||||
self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=1),
|
||||
'_created': now + timedelta(seconds=1),
|
||||
'_deleted': True,
|
||||
'properties': {'tags': ['एनिमेशन'], **base_props},
|
||||
**base_node})
|
||||
@@ -560,76 +556,3 @@ class TaggedNodesTest(AbstractPillarTest):
|
||||
with self.app.app_context():
|
||||
invalid_url = flask.url_for('nodes_api.tagged', tag='')
|
||||
self.get(invalid_url, expected_status=404)
|
||||
|
||||
def test_tagged_nodes_asset_video_with_progress_api(self):
|
||||
from datetime import timedelta
|
||||
from pillar.auth import current_user
|
||||
|
||||
base_node = {
|
||||
'name': 'Spring hair rig setup',
|
||||
'project': self.pid,
|
||||
'description': '',
|
||||
'node_type': 'asset',
|
||||
'user': self.uid,
|
||||
}
|
||||
base_props = {'status': 'published',
|
||||
'file': self.file_id,
|
||||
'content_type': 'video',
|
||||
'order': 0}
|
||||
|
||||
# Create one node of type asset video
|
||||
nid_single_tag = self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=2),
|
||||
# 'एनिमेशन' is 'animation' in Hindi.
|
||||
'properties': {'tags': ['एनिमेशन'], **base_props},
|
||||
**base_node,
|
||||
})
|
||||
|
||||
# Create another node
|
||||
self.create_node({
|
||||
'_created': self.fake_now + timedelta(seconds=2),
|
||||
# 'एनिमेशन' is 'animation' in Hindi.
|
||||
'properties': {'tags': ['एनिमेशन'], **base_props},
|
||||
**base_node,
|
||||
})
|
||||
|
||||
# Add video watch progress for the self.uid user
|
||||
with self.app.app_context():
|
||||
users_coll = self.app.db('users')
|
||||
# Define video progress
|
||||
progress_in_sec = 333
|
||||
video_progress = {
|
||||
'progress_in_sec': progress_in_sec,
|
||||
'progress_in_percent': 70,
|
||||
'done': False,
|
||||
'last_watched': self.fake_now + timedelta(seconds=2),
|
||||
}
|
||||
users_coll.update_one(
|
||||
{'_id': self.uid},
|
||||
{'$set': {f'nodes.view_progress.{nid_single_tag}': video_progress}})
|
||||
|
||||
# Utility to fetch tagged nodes and return them as JSON list
|
||||
def do_query():
|
||||
animation_tags_url = flask.url_for('nodes_api.tagged', tag='एनिमेशन')
|
||||
return self.get(animation_tags_url).json
|
||||
|
||||
# Ensure that anonymous users get videos with no view_progress info
|
||||
with self.app.app_context():
|
||||
resp = do_query()
|
||||
for node in resp:
|
||||
self.assertNotIn('view_progress', node)
|
||||
|
||||
# Ensure that an authenticated user gets view_progress info if the video was watched
|
||||
with self.login_as(self.uid):
|
||||
resp = do_query()
|
||||
for node in resp:
|
||||
if node['_id'] in current_user.nodes['view_progress']:
|
||||
self.assertIn('view_progress', node)
|
||||
self.assertEqual(progress_in_sec, node['view_progress']['progress_in_sec'])
|
||||
|
||||
# Ensure that another user with no view progress does not get any view progress info
|
||||
other_user = self.create_user(user_id=ObjectId())
|
||||
with self.login_as(other_user):
|
||||
resp = do_query()
|
||||
for node in resp:
|
||||
self.assertNotIn('view_progress', node)
|
||||
|
@@ -187,48 +187,37 @@ class AttachmentTest(AbstractPillarTest):
|
||||
],
|
||||
'filename': 'cute_kitten.jpg',
|
||||
})
|
||||
|
||||
node_properties = {'attachments': {
|
||||
'img': {'oid': oid},
|
||||
node_doc = {'properties': {
|
||||
'attachments': {
|
||||
'img': {'oid': oid},
|
||||
}
|
||||
}}
|
||||
|
||||
node_doc = {'properties': node_properties}
|
||||
|
||||
# Collect the two possible context that can be provided for attachemt
|
||||
# rendering. See pillar.shortcodes.sdk_file for more info.
|
||||
possible_contexts = [node_properties, node_doc]
|
||||
|
||||
# We have to get the file document again, because retrieving it via the
|
||||
# API (which is what the shortcode rendering is doing) will change its
|
||||
# link URL.
|
||||
db_file = self.get(f'/api/files/{oid}').get_json()
|
||||
link = db_file['variations'][0]['link']
|
||||
|
||||
def do_render(context, link):
|
||||
"""Utility to run attachment rendering in different contexts."""
|
||||
with self.app.test_request_context():
|
||||
self_linked = f'<a class="expand-image-links" href="{link}">' \
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/></a>'
|
||||
self.assertEqual(
|
||||
self_linked,
|
||||
render('{attachment img link}', context=context).strip()
|
||||
)
|
||||
self.assertEqual(
|
||||
self_linked,
|
||||
render('{attachment img link=self}', context=context).strip()
|
||||
)
|
||||
self.assertEqual(
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/>',
|
||||
render('{attachment img}', context=context).strip()
|
||||
)
|
||||
with self.app.test_request_context():
|
||||
self_linked = f'<a class="expand-image-links" href="{link}">' \
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/></a>'
|
||||
self.assertEqual(
|
||||
self_linked,
|
||||
render('{attachment img link}', context=node_doc).strip()
|
||||
)
|
||||
self.assertEqual(
|
||||
self_linked,
|
||||
render('{attachment img link=self}', context=node_doc).strip()
|
||||
)
|
||||
self.assertEqual(
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/>',
|
||||
render('{attachment img}', context=node_doc).strip()
|
||||
)
|
||||
|
||||
tag_link = 'https://i.imgur.com/FmbuPNe.jpg'
|
||||
self.assertEqual(
|
||||
f'<a href="{tag_link}" target="_blank">'
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/></a>',
|
||||
render('{attachment img link=%r}' % tag_link, context=context).strip()
|
||||
)
|
||||
|
||||
# Test both possible contexts for rendering attachments
|
||||
for context in possible_contexts:
|
||||
do_render(context, link)
|
||||
tag_link = 'https://i.imgur.com/FmbuPNe.jpg'
|
||||
self.assertEqual(
|
||||
f'<a href="{tag_link}" target="_blank">'
|
||||
f'<img src="{link}" alt="cute_kitten.jpg"/></a>',
|
||||
render('{attachment img link=%r}' % tag_link, context=node_doc).strip()
|
||||
)
|
||||
|
Reference in New Issue
Block a user