Quick-Search: Added Quick-search in the topbar
Changed how and what we store in elastic to unify it with how we store things in mongodb so we can have more generic javascript code to render the data. Elastic changes: Added: Node.project.url Altered to store id instead of url Node.picture Made Post searchable ./manage.py elastic reset_index ./manage.py elastic reindex Thanks to Pablo and Sybren
This commit is contained in:
parent
a897e201ba
commit
6ae9a5ddeb
@ -89,9 +89,11 @@ gulp.task('scripts', function(done) {
|
||||
});
|
||||
|
||||
function browserify_base(entry) {
|
||||
let pathSplited = path.dirname(entry).split(path.sep);
|
||||
let moduleName = pathSplited[pathSplited.length - 1];
|
||||
return browserify({
|
||||
entries: [entry],
|
||||
standalone: 'pillar.' + path.basename(entry, '.js'),
|
||||
standalone: 'pillar.' + moduleName,
|
||||
})
|
||||
.transform(babelify, { "presets": ["@babel/preset-env"] })
|
||||
.bundle()
|
||||
@ -99,16 +101,17 @@ function browserify_base(entry) {
|
||||
.pipe(sourceStream(path.basename(entry)))
|
||||
.pipe(buffer())
|
||||
.pipe(rename({
|
||||
basename: moduleName,
|
||||
extname: '.min.js'
|
||||
}));
|
||||
}
|
||||
|
||||
function browserify_common() {
|
||||
return glob.sync('src/scripts/js/es6/common/*.js').map(browserify_base);
|
||||
return glob.sync('src/scripts/js/es6/common/**/init.js').map(browserify_base);
|
||||
}
|
||||
|
||||
gulp.task('scripts_browserify', function(done) {
|
||||
glob('src/scripts/js/es6/individual/*.js', function(err, files) {
|
||||
glob('src/scripts/js/es6/individual/**/init.js', function(err, files) {
|
||||
if(err) done(err);
|
||||
|
||||
var tasks = files.map(function(entry) {
|
||||
|
@ -81,6 +81,7 @@ class Node(es.DocType):
|
||||
fields={
|
||||
'id': es.Keyword(),
|
||||
'name': es.Keyword(),
|
||||
'url': es.Keyword(),
|
||||
}
|
||||
)
|
||||
|
||||
@ -153,18 +154,21 @@ def create_doc_from_node_data(node_to_index: dict) -> typing.Optional[Node]:
|
||||
doc.objectID = str(node_to_index['objectID'])
|
||||
doc.node_type = node_to_index['node_type']
|
||||
doc.name = node_to_index['name']
|
||||
doc.description = node_to_index.get('description')
|
||||
doc.user.id = str(node_to_index['user']['_id'])
|
||||
doc.user.name = node_to_index['user']['full_name']
|
||||
doc.project.id = str(node_to_index['project']['_id'])
|
||||
doc.project.name = node_to_index['project']['name']
|
||||
doc.project.url = node_to_index['project']['url']
|
||||
|
||||
if node_to_index['node_type'] == 'asset':
|
||||
doc.media = node_to_index['media']
|
||||
|
||||
doc.picture = node_to_index.get('picture')
|
||||
doc.picture = str(node_to_index.get('picture'))
|
||||
|
||||
doc.tags = node_to_index.get('tags')
|
||||
doc.license_notes = node_to_index.get('license_notes')
|
||||
doc.is_free = node_to_index.get('is_free')
|
||||
|
||||
doc.created_at = node_to_index['created']
|
||||
doc.updated_at = node_to_index['updated']
|
||||
|
@ -3,16 +3,18 @@ import logging
|
||||
import typing
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from elasticsearch_dsl import Search, Q
|
||||
from elasticsearch_dsl import Search, Q, MultiSearch
|
||||
from elasticsearch_dsl.query import Query
|
||||
|
||||
from pillar import current_app
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NODE_AGG_TERMS = ['node_type', 'media', 'tags', 'is_free']
|
||||
BOOLEAN_TERMS = ['is_free']
|
||||
NODE_AGG_TERMS = ['node_type', 'media', 'tags', *BOOLEAN_TERMS]
|
||||
USER_AGG_TERMS = ['roles', ]
|
||||
ITEMS_PER_PAGE = 10
|
||||
USER_SOURCE_INCLUDE = ['full_name', 'objectID', 'username']
|
||||
|
||||
# Will be set in setup_app()
|
||||
client: Elasticsearch = None
|
||||
@ -27,26 +29,25 @@ def add_aggs_to_search(search, agg_terms):
|
||||
search.aggs.bucket(term, 'terms', field=term)
|
||||
|
||||
|
||||
def make_must(must: list, terms: dict) -> list:
|
||||
def make_filter(must: list, terms: dict) -> list:
|
||||
""" Given term parameters append must queries to the must list """
|
||||
|
||||
for field, value in terms.items():
|
||||
if value:
|
||||
must.append({'match': {field: value}})
|
||||
if value not in (None, ''):
|
||||
must.append({'term': {field: value}})
|
||||
|
||||
return must
|
||||
|
||||
|
||||
def nested_bool(must: list, should: list, terms: dict, *, index_alias: str) -> Search:
|
||||
def nested_bool(filters: list, should: list, terms: dict, *, index_alias: str) -> Search:
|
||||
"""
|
||||
Create a nested bool, where the aggregation selection is a must.
|
||||
|
||||
:param index_alias: 'USER' or 'NODE', see ELASTIC_INDICES config.
|
||||
"""
|
||||
must = make_must(must, terms)
|
||||
filters = make_filter(filters, terms)
|
||||
bool_query = Q('bool', should=should)
|
||||
must.append(bool_query)
|
||||
bool_query = Q('bool', must=must)
|
||||
bool_query = Q('bool', must=bool_query, filter=filters)
|
||||
|
||||
index = current_app.config['ELASTIC_INDICES'][index_alias]
|
||||
search = Search(using=client, index=index)
|
||||
@ -55,12 +56,34 @@ def nested_bool(must: list, should: list, terms: dict, *, index_alias: str) -> S
|
||||
return search
|
||||
|
||||
|
||||
def do_multi_node_search(queries: typing.List[dict]) -> typing.List[dict]:
|
||||
"""
|
||||
Given user query input and term refinements
|
||||
search for public published nodes
|
||||
"""
|
||||
search = create_multi_node_search(queries)
|
||||
return _execute_multi(search)
|
||||
|
||||
|
||||
def do_node_search(query: str, terms: dict, page: int, project_id: str='') -> dict:
|
||||
"""
|
||||
Given user query input and term refinements
|
||||
search for public published nodes
|
||||
"""
|
||||
search = create_node_search(query, terms, page, project_id)
|
||||
return _execute(search)
|
||||
|
||||
|
||||
def create_multi_node_search(queries: typing.List[dict]) -> MultiSearch:
|
||||
search = MultiSearch(using=client)
|
||||
for q in queries:
|
||||
search = search.add(create_node_search(**q))
|
||||
|
||||
return search
|
||||
|
||||
|
||||
def create_node_search(query: str, terms: dict, page: int, project_id: str='') -> Search:
|
||||
terms = _transform_terms(terms)
|
||||
should = [
|
||||
Q('match', name=query),
|
||||
|
||||
@ -71,52 +94,30 @@ def do_node_search(query: str, terms: dict, page: int, project_id: str='') -> di
|
||||
Q('term', media=query),
|
||||
Q('term', tags=query),
|
||||
]
|
||||
|
||||
must = []
|
||||
filters = []
|
||||
if project_id:
|
||||
must.append({'term': {'project.id': project_id}})
|
||||
|
||||
filters.append({'term': {'project.id': project_id}})
|
||||
if not query:
|
||||
should = []
|
||||
|
||||
search = nested_bool(must, should, terms, index_alias='NODE')
|
||||
search = nested_bool(filters, should, terms, index_alias='NODE')
|
||||
if not query:
|
||||
search = search.sort('-created_at')
|
||||
add_aggs_to_search(search, NODE_AGG_TERMS)
|
||||
search = paginate(search, page)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(search.to_dict(), indent=4))
|
||||
|
||||
response = search.execute()
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(response.to_dict(), indent=4))
|
||||
|
||||
return response.to_dict()
|
||||
return search
|
||||
|
||||
|
||||
def do_user_search(query: str, terms: dict, page: int) -> dict:
|
||||
""" return user objects represented in elasicsearch result dict"""
|
||||
|
||||
must, should = _common_user_search(query)
|
||||
search = nested_bool(must, should, terms, index_alias='USER')
|
||||
add_aggs_to_search(search, USER_AGG_TERMS)
|
||||
search = paginate(search, page)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(search.to_dict(), indent=4))
|
||||
|
||||
response = search.execute()
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(response.to_dict(), indent=4))
|
||||
|
||||
return response.to_dict()
|
||||
search = create_user_search(query, terms, page)
|
||||
return _execute(search)
|
||||
|
||||
|
||||
def _common_user_search(query: str) -> (typing.List[Query], typing.List[Query]):
|
||||
"""Construct (must,shoud) for regular + admin user search."""
|
||||
"""Construct (filter,should) for regular + admin user search."""
|
||||
if not query:
|
||||
return [], []
|
||||
|
||||
@ -144,8 +145,31 @@ def do_user_search_admin(query: str, terms: dict, page: int) -> dict:
|
||||
search all user fields and provide aggregation information
|
||||
"""
|
||||
|
||||
must, should = _common_user_search(query)
|
||||
search = create_user_admin_search(query, terms, page)
|
||||
return _execute(search)
|
||||
|
||||
|
||||
def _execute(search: Search) -> dict:
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(search.to_dict(), indent=4))
|
||||
resp = search.execute()
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(resp.to_dict(), indent=4))
|
||||
return resp.to_dict()
|
||||
|
||||
|
||||
def _execute_multi(search: typing.List[Search]) -> typing.List[dict]:
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(search.to_dict(), indent=4))
|
||||
resp = search.execute()
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(resp.to_dict(), indent=4))
|
||||
return [r.to_dict() for r in resp]
|
||||
|
||||
|
||||
def create_user_admin_search(query: str, terms: dict, page: int) -> Search:
|
||||
terms = _transform_terms(terms)
|
||||
filters, should = _common_user_search(query)
|
||||
if query:
|
||||
# We most likely got and id field. we should find it.
|
||||
if len(query) == len('563aca02c379cf0005e8e17d'):
|
||||
@ -155,26 +179,34 @@ def do_user_search_admin(query: str, terms: dict, page: int) -> dict:
|
||||
'boost': 100, # how much more it counts for the score
|
||||
}
|
||||
}})
|
||||
|
||||
search = nested_bool(must, should, terms, index_alias='USER')
|
||||
search = nested_bool(filters, should, terms, index_alias='USER')
|
||||
add_aggs_to_search(search, USER_AGG_TERMS)
|
||||
search = paginate(search, page)
|
||||
return search
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(search.to_dict(), indent=4))
|
||||
|
||||
response = search.execute()
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug(json.dumps(response.to_dict(), indent=4))
|
||||
|
||||
return response.to_dict()
|
||||
def create_user_search(query: str, terms: dict, page: int) -> Search:
|
||||
search = create_user_admin_search(query, terms, page)
|
||||
return search.source(include=USER_SOURCE_INCLUDE)
|
||||
|
||||
|
||||
def paginate(search: Search, page_idx: int) -> Search:
|
||||
return search[page_idx * ITEMS_PER_PAGE:(page_idx + 1) * ITEMS_PER_PAGE]
|
||||
|
||||
|
||||
def _transform_terms(terms: dict) -> dict:
|
||||
"""
|
||||
Ugly hack! Elastic uses 1/0 for boolean values in its aggregate response,
|
||||
but expects true/false in queries.
|
||||
"""
|
||||
transformed = terms.copy()
|
||||
for t in BOOLEAN_TERMS:
|
||||
orig = transformed.get(t)
|
||||
if orig in ('1', '0'):
|
||||
transformed[t] = bool(int(orig))
|
||||
return transformed
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
global client
|
||||
|
||||
|
@ -18,7 +18,7 @@ TERMS = [
|
||||
]
|
||||
|
||||
|
||||
def _term_filters() -> dict:
|
||||
def _term_filters(args) -> dict:
|
||||
"""
|
||||
Check if frontent wants to filter stuff
|
||||
on specific fields AKA facets
|
||||
@ -26,35 +26,53 @@ def _term_filters() -> dict:
|
||||
return mapping with term field name
|
||||
and provided user term value
|
||||
"""
|
||||
return {term: request.args.get(term, '') for term in TERMS}
|
||||
return {term: args.get(term, '') for term in TERMS}
|
||||
|
||||
|
||||
def _page_index() -> int:
|
||||
def _page_index(page) -> int:
|
||||
"""Return the page index from the query string."""
|
||||
try:
|
||||
page_idx = int(request.args.get('page') or '0')
|
||||
page_idx = int(page)
|
||||
except TypeError:
|
||||
log.info('invalid page number %r received', request.args.get('page'))
|
||||
raise wz_exceptions.BadRequest()
|
||||
return page_idx
|
||||
|
||||
|
||||
@blueprint_search.route('/')
|
||||
@blueprint_search.route('/', methods=['GET'])
|
||||
def search_nodes():
|
||||
searchword = request.args.get('q', '')
|
||||
project_id = request.args.get('project', '')
|
||||
terms = _term_filters()
|
||||
page_idx = _page_index()
|
||||
terms = _term_filters(request.args)
|
||||
page_idx = _page_index(request.args.get('page', 0))
|
||||
|
||||
result = queries.do_node_search(searchword, terms, page_idx, project_id)
|
||||
return jsonify(result)
|
||||
|
||||
@blueprint_search.route('/multisearch', methods=['GET'])
|
||||
def multi_search_nodes():
|
||||
import json
|
||||
if len(request.args) != 1:
|
||||
log.info(f'Expected 1 argument, received {len(request.args)}')
|
||||
|
||||
json_obj = json.loads([a for a in request.args][0])
|
||||
q = []
|
||||
for row in json_obj:
|
||||
q.append({
|
||||
'query': row.get('q', ''),
|
||||
'project_id': row.get('project', ''),
|
||||
'terms': _term_filters(row),
|
||||
'page': _page_index(row.get('page', 0))
|
||||
})
|
||||
|
||||
result = queries.do_multi_node_search(q)
|
||||
return jsonify(result)
|
||||
|
||||
@blueprint_search.route('/user')
|
||||
def search_user():
|
||||
searchword = request.args.get('q', '')
|
||||
terms = _term_filters()
|
||||
page_idx = _page_index()
|
||||
terms = _term_filters(request.args)
|
||||
page_idx = _page_index(request.args.get('page', 0))
|
||||
# result is the raw elasticseach output.
|
||||
# we need to filter fields in case of user objects.
|
||||
|
||||
@ -65,27 +83,6 @@ def search_user():
|
||||
resp.status_code = 500
|
||||
return resp
|
||||
|
||||
# filter sensitive stuff
|
||||
# we only need. objectID, full_name, username
|
||||
hits = result.get('hits', {})
|
||||
|
||||
new_hits = []
|
||||
|
||||
for hit in hits.get('hits'):
|
||||
source = hit['_source']
|
||||
single_hit = {
|
||||
'_source': {
|
||||
'objectID': source.get('objectID'),
|
||||
'username': source.get('username'),
|
||||
'full_name': source.get('full_name'),
|
||||
}
|
||||
}
|
||||
|
||||
new_hits.append(single_hit)
|
||||
|
||||
# replace search result with safe subset
|
||||
result['hits']['hits'] = new_hits
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@ -97,8 +94,8 @@ def search_user_admin():
|
||||
"""
|
||||
|
||||
searchword = request.args.get('q', '')
|
||||
terms = _term_filters()
|
||||
page_idx = _page_index()
|
||||
terms = _term_filters(request.args)
|
||||
page_idx = _page_index(_page_index(request.args.get('page', 0)))
|
||||
|
||||
try:
|
||||
result = queries.do_user_search_admin(searchword, terms, page_idx)
|
||||
|
@ -11,7 +11,6 @@ from flask import Blueprint, current_app, request, url_for
|
||||
import pillar
|
||||
from pillar import shortcodes
|
||||
from pillar.api.utils import jsonify, pretty_duration, str2id
|
||||
from pillar.web.utils import pretty_date
|
||||
|
||||
blueprint = Blueprint('timeline', __name__)
|
||||
|
||||
@ -209,7 +208,6 @@ class TimeLineBuilder:
|
||||
|
||||
@classmethod
|
||||
def node_prettyfy(cls, node: dict)-> dict:
|
||||
node['pretty_created'] = pretty_date(node['_created'])
|
||||
duration_seconds = node['properties'].get('duration_seconds')
|
||||
if duration_seconds is not None:
|
||||
node['properties']['duration'] = pretty_duration(duration_seconds)
|
||||
|
@ -1,4 +1,6 @@
|
||||
import logging
|
||||
|
||||
import bleach
|
||||
from bson import ObjectId
|
||||
|
||||
from pillar import current_app
|
||||
@ -10,7 +12,7 @@ from pillar.api.search import algolia_indexing
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INDEX_ALLOWED_NODE_TYPES = {'asset', 'texture', 'group', 'hdri'}
|
||||
INDEX_ALLOWED_NODE_TYPES = {'asset', 'texture', 'group', 'hdri', 'post'}
|
||||
|
||||
|
||||
SEARCH_BACKENDS = {
|
||||
@ -28,34 +30,6 @@ def _get_node_from_id(node_id: str):
|
||||
return node
|
||||
|
||||
|
||||
def _handle_picture(node: dict, to_index: dict):
|
||||
"""Add picture URL in-place to the to-be-indexed node."""
|
||||
|
||||
picture_id = node.get('picture')
|
||||
if not picture_id:
|
||||
return
|
||||
|
||||
files_collection = current_app.data.driver.db['files']
|
||||
lookup = {'_id': ObjectId(picture_id)}
|
||||
picture = files_collection.find_one(lookup)
|
||||
|
||||
for item in picture.get('variations', []):
|
||||
if item['size'] != 't':
|
||||
continue
|
||||
|
||||
# Not all files have a project...
|
||||
pid = picture.get('project')
|
||||
if pid:
|
||||
link = generate_link(picture['backend'],
|
||||
item['file_path'],
|
||||
str(pid),
|
||||
is_public=True)
|
||||
else:
|
||||
link = item['link']
|
||||
to_index['picture'] = link
|
||||
break
|
||||
|
||||
|
||||
def prepare_node_data(node_id: str, node: dict=None) -> dict:
|
||||
"""Given a node id or a node document, return an indexable version of it.
|
||||
|
||||
@ -86,25 +60,30 @@ def prepare_node_data(node_id: str, node: dict=None) -> dict:
|
||||
users_collection = current_app.data.driver.db['users']
|
||||
user = users_collection.find_one({'_id': ObjectId(node['user'])})
|
||||
|
||||
clean_description = bleach.clean(node.get('_description_html') or '', strip=True)
|
||||
if not clean_description and node['node_type'] == 'post':
|
||||
clean_description = bleach.clean(node['properties'].get('_content_html') or '', strip=True)
|
||||
|
||||
to_index = {
|
||||
'objectID': node['_id'],
|
||||
'name': node['name'],
|
||||
'project': {
|
||||
'_id': project['_id'],
|
||||
'name': project['name']
|
||||
'name': project['name'],
|
||||
'url': project['url'],
|
||||
},
|
||||
'created': node['_created'],
|
||||
'updated': node['_updated'],
|
||||
'node_type': node['node_type'],
|
||||
'picture': node.get('picture') or '',
|
||||
'user': {
|
||||
'_id': user['_id'],
|
||||
'full_name': user['full_name']
|
||||
},
|
||||
'description': node.get('description'),
|
||||
'description': clean_description or None,
|
||||
'is_free': False
|
||||
}
|
||||
|
||||
_handle_picture(node, to_index)
|
||||
|
||||
# If the node has world permissions, compute the Free permission
|
||||
if 'world' in node.get('permissions', {}):
|
||||
if 'GET' in node['permissions']['world']:
|
||||
|
@ -11,10 +11,8 @@ $(document).ready(function() {
|
||||
var what = '';
|
||||
|
||||
// Templates binding
|
||||
var hitTemplate = Hogan.compile($('#hit-template').text());
|
||||
var statsTemplate = Hogan.compile($('#stats-template').text());
|
||||
var facetTemplate = Hogan.compile($('#facet-template').text());
|
||||
var sliderTemplate = Hogan.compile($('#slider-template').text());
|
||||
var paginationTemplate = Hogan.compile($('#pagination-template').text());
|
||||
|
||||
// defined in tutti/4_search.js
|
||||
@ -47,6 +45,7 @@ $(document).ready(function() {
|
||||
renderFacets(content);
|
||||
renderPagination(content);
|
||||
renderFirstHit($(hits).children('.search-hit:first'));
|
||||
updateUrlParams();
|
||||
});
|
||||
|
||||
/***************
|
||||
@ -66,7 +65,7 @@ $(document).ready(function() {
|
||||
|
||||
window.setTimeout(function() {
|
||||
// Ignore getting that first result when there is none.
|
||||
var hit_id = firstHit.attr('data-hit-id');
|
||||
var hit_id = firstHit.attr('data-node-id');
|
||||
if (hit_id === undefined) {
|
||||
done();
|
||||
return;
|
||||
@ -87,12 +86,6 @@ $(document).ready(function() {
|
||||
// Initial search
|
||||
initWithUrlParams();
|
||||
|
||||
function convertTimestamp(iso8601) {
|
||||
var d = new Date(iso8601)
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
|
||||
function renderStats(content) {
|
||||
var stats = {
|
||||
nbHits: numberWithDelimiter(content.count),
|
||||
@ -103,20 +96,17 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
function renderHits(content) {
|
||||
var hitsHtml = '';
|
||||
for (var i = 0; i < content.hits.length; ++i) {
|
||||
var created = content.hits[i].created_at;
|
||||
if (created) {
|
||||
content.hits[i].created_at = convertTimestamp(created);
|
||||
}
|
||||
var updated = content.hits[i].updated_at;
|
||||
if (updated) {
|
||||
content.hits[i].updated_at = convertTimestamp(updated);
|
||||
}
|
||||
hitsHtml += hitTemplate.render(content.hits[i]);
|
||||
$hits.empty();
|
||||
if (content.hits.length === 0) {
|
||||
$hits.html('<p id="no-hits">We didn\'t find any items. Try searching something else.</p>');
|
||||
}
|
||||
else {
|
||||
listof$hits = content.hits.map(function(hit){
|
||||
return pillar.templates.Component.create$listItem(hit)
|
||||
.addClass('js-search-hit cursor-pointer search-hit');
|
||||
})
|
||||
$hits.append(listof$hits);
|
||||
}
|
||||
if (content.hits.length === 0) hitsHtml = '<p id="no-hits">We didn\'t find any items. Try searching something else.</p>';
|
||||
$hits.html(hitsHtml);
|
||||
}
|
||||
|
||||
function renderFacets(content) {
|
||||
@ -133,7 +123,7 @@ $(document).ready(function() {
|
||||
var refined = search.isRefined(label, item.key);
|
||||
values.push({
|
||||
facet: label,
|
||||
label: item.key,
|
||||
label: item.key_as_string || item.key,
|
||||
value: item.key,
|
||||
count: item.doc_count,
|
||||
refined: refined,
|
||||
@ -153,7 +143,7 @@ $(document).ready(function() {
|
||||
|
||||
buckets.forEach(storeValue(values, label));
|
||||
facets.push({
|
||||
title: label,
|
||||
title: removeUnderscore(label),
|
||||
values: values.slice(0),
|
||||
});
|
||||
}
|
||||
@ -218,6 +208,9 @@ $(document).ready(function() {
|
||||
$pagination.html(paginationTemplate.render(pagination));
|
||||
}
|
||||
|
||||
function removeUnderscore(s) {
|
||||
return s.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
// Event bindings
|
||||
// Click binding
|
||||
@ -300,37 +293,46 @@ $(document).ready(function() {
|
||||
};
|
||||
|
||||
function initWithUrlParams() {
|
||||
var sPageURL = location.hash;
|
||||
if (!sPageURL || sPageURL.length === 0) {
|
||||
return true;
|
||||
var pageURL = decodeURIComponent(window.location.search.substring(1)),
|
||||
urlVariables = pageURL.split('&'),
|
||||
query,
|
||||
i;
|
||||
for (i = 0; i < urlVariables.length; i++) {
|
||||
var parameterPair = urlVariables[i].split('='),
|
||||
key = parameterPair[0],
|
||||
sValue = parameterPair[1];
|
||||
if (!key) continue;
|
||||
if (key === 'q') {
|
||||
query = sValue;
|
||||
continue;
|
||||
}
|
||||
if (key === 'page') {
|
||||
var page = Number.parseInt(sValue)
|
||||
search.setCurrentPage(isNaN(page) ? 0 : page)
|
||||
continue;
|
||||
}
|
||||
if (key === 'project') {
|
||||
continue; // We take the project from the path
|
||||
}
|
||||
if (sValue !== undefined) {
|
||||
var iValue = Number.parseInt(sValue),
|
||||
value = isNaN(iValue) ? sValue : iValue;
|
||||
search.toggleTerm(key, value);
|
||||
continue;
|
||||
}
|
||||
console.log('Unhandled url parameter pair:', parameterPair)
|
||||
}
|
||||
var sURLVariables = sPageURL.split('&');
|
||||
if (!sURLVariables || sURLVariables.length === 0) {
|
||||
return true;
|
||||
}
|
||||
var query = decodeURIComponent(sURLVariables[0].split('=')[1]);
|
||||
$inputField.val(query);
|
||||
search.setQuery(query, what);
|
||||
|
||||
for (var i = 2; i < sURLVariables.length; i++) {
|
||||
var sParameterName = sURLVariables[i].split('=');
|
||||
var facet = decodeURIComponent(sParameterName[0]);
|
||||
var value = decodeURIComponent(sParameterName[1]);
|
||||
}
|
||||
// Page has to be set in the end to avoid being overwritten
|
||||
var page = decodeURIComponent(sURLVariables[1].split('=')[1]) - 1;
|
||||
search.setCurrentPage(page);
|
||||
do_search(query || '');
|
||||
}
|
||||
|
||||
function setURLParams(state) {
|
||||
var urlParams = '?';
|
||||
var currentQuery = state.query;
|
||||
urlParams += 'q=' + encodeURIComponent(currentQuery);
|
||||
var currentPage = state.page + 1;
|
||||
urlParams += '&page=' + currentPage;
|
||||
location.replace(urlParams);
|
||||
function updateUrlParams() {
|
||||
var prevState = history.state,
|
||||
prevTitle = document.title,
|
||||
params = search.getParams(),
|
||||
newUrl = window.location.pathname + '?';
|
||||
delete params['project'] // We take the project from the path
|
||||
newUrl += jQuery.param(params)
|
||||
history.replaceState(prevState, prevTitle, newUrl);
|
||||
}
|
||||
|
||||
// do empty search to fill aggregations
|
||||
do_search('');
|
||||
});
|
||||
|
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
@ -0,0 +1,58 @@
|
||||
import {SearchParams} from './SearchParams';
|
||||
|
||||
export class MultiSearch {
|
||||
constructor(kwargs) {
|
||||
this.uiUrl = kwargs['uiUrl']; // Url for advanced search
|
||||
this.apiUrl = kwargs['apiUrl']; // Url for api calls
|
||||
this.searchParams = MultiSearch.createMultiSearchParams(kwargs['searchParams']);
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
this.searchParams.forEach((qsParam) => {
|
||||
qsParam.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.uiUrl + '?q=' + this.q;
|
||||
}
|
||||
|
||||
getAllParams() {
|
||||
let retval = $.map(this.searchParams, (msParams) => {
|
||||
return msParams.params;
|
||||
});
|
||||
return retval;
|
||||
}
|
||||
|
||||
parseResult(rawResult) {
|
||||
return $.map(rawResult, (subResult, index) => {
|
||||
let name = this.searchParams[index].name;
|
||||
let pStr = this.searchParams[index].getParamStr();
|
||||
let result = $.map(subResult.hits.hits, (hit) => {
|
||||
return hit._source;
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
url: this.uiUrl + '?' + pStr,
|
||||
result: result,
|
||||
hasResults: !!result.length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
thenExecute() {
|
||||
let data = JSON.stringify(this.getAllParams());
|
||||
let rawAjax = $.getJSON(this.apiUrl, data);
|
||||
let prettyPromise = rawAjax.then(this.parseResult.bind(this));
|
||||
prettyPromise['abort'] = rawAjax.abort.bind(rawAjax); // Hack to be able to abort the promise down the road
|
||||
return prettyPromise;
|
||||
}
|
||||
|
||||
static createMultiSearchParams(argsList) {
|
||||
return $.map(argsList, (args) => {
|
||||
return new SearchParams(args);
|
||||
});
|
||||
}
|
||||
}
|
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
@ -0,0 +1,204 @@
|
||||
import { create$noHits, create$results, create$input } from './templates'
|
||||
import {SearchFacade} from './SearchFacade';
|
||||
/**
|
||||
* QuickSearch : Interacts with the dom document
|
||||
* 1-SearchFacade : Controls which multisearch is active
|
||||
* *-MultiSearch : One multi search is typically Project or Cloud
|
||||
* *-SearchParams : The search params for the individual searches
|
||||
*/
|
||||
|
||||
export class QuickSearch {
|
||||
/**
|
||||
* Interacts with the dom document and deligates the input down to the SearchFacade
|
||||
* @param {selector string} searchToggle The quick-search toggle
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(searchToggle, kwargs) {
|
||||
this.$body = $('body');
|
||||
this.$quickSearch = $('.quick-search');
|
||||
this.$inputComponent = $(kwargs['inputTarget']);
|
||||
this.$inputComponent.empty();
|
||||
this.$inputComponent.append(create$input(kwargs['searches']));
|
||||
this.$searchInput = this.$inputComponent.find('input');
|
||||
this.$searchSelect = this.$inputComponent.find('select');
|
||||
this.$resultTarget = $(kwargs['resultTarget']);
|
||||
this.$searchSymbol = this.$inputComponent.find('.qs-busy-symbol');
|
||||
this.searchFacade = new SearchFacade(kwargs['searches'] || {});
|
||||
this.$searchToggle = $(searchToggle);
|
||||
this.isBusy = false;
|
||||
this.attach();
|
||||
}
|
||||
|
||||
attach() {
|
||||
if (this.$searchSelect.length) {
|
||||
this.$searchSelect
|
||||
.change(this.execute.bind(this))
|
||||
.change(() => this.$searchInput.focus());
|
||||
this.$searchInput.addClass('multi-scope');
|
||||
}
|
||||
|
||||
this.$searchInput
|
||||
.keyup(this.onInputKeyUp.bind(this));
|
||||
|
||||
this.$inputComponent
|
||||
.on('pillar:workStart', () => {
|
||||
this.$searchSymbol.addClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
})
|
||||
.on('pillar:workStop', () => {
|
||||
this.$searchSymbol.removeClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
});
|
||||
|
||||
this.searchFacade.setOnResultCB(this.renderResult.bind(this));
|
||||
this.searchFacade.setOnFailureCB(this.onSearchFailed.bind(this));
|
||||
this.$searchToggle
|
||||
.one('click', this.execute.bind(this)); // Initial search executed once
|
||||
|
||||
this.registerShowGui();
|
||||
this.registerHideGui();
|
||||
}
|
||||
|
||||
registerShowGui() {
|
||||
this.$searchToggle
|
||||
.click((e) => {
|
||||
this.showGUI();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
registerHideGui() {
|
||||
this.$searchSymbol
|
||||
.click(() => {
|
||||
this.hideGUI();
|
||||
});
|
||||
this.$body.click((e) => {
|
||||
let $target = $(e.target);
|
||||
let isClickInResult = $target.hasClass('.qs-result') || !!$target.parents('.qs-result').length;
|
||||
let isClickInInput = $target.hasClass('.qs-input') || !!$target.parents('.qs-input').length;
|
||||
if (!isClickInResult && !isClickInInput) {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
$(document).keyup((e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showGUI() {
|
||||
this.$body.addClass('has-overlay');
|
||||
this.$quickSearch.trigger('pillar:searchShow');
|
||||
this.$quickSearch.addClass('show');
|
||||
if (!this.$searchInput.is(':focus')) {
|
||||
this.$searchInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hideGUI() {
|
||||
this.$body.removeClass('has-overlay');
|
||||
this.$searchToggle.addClass('pi-search');
|
||||
this.$searchInput.blur();
|
||||
this.$quickSearch.removeClass('show');
|
||||
this.$quickSearch.trigger('pillar:searchHidden');
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
let newQ = this.$searchInput.val();
|
||||
let currQ = this.searchFacade.getSearchWord();
|
||||
this.searchFacade.setSearchWord(newQ);
|
||||
let searchUrl = this.searchFacade.getSearchUrl();
|
||||
if (e.key === 'Enter') {
|
||||
window.location.href = searchUrl;
|
||||
return;
|
||||
}
|
||||
if (newQ !== currQ) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.busy(true);
|
||||
let scope = this.getScope();
|
||||
this.searchFacade.setCurrentScope(scope);
|
||||
let q = this.$searchInput.val();
|
||||
this.searchFacade.setSearchWord(q);
|
||||
this.searchFacade.execute();
|
||||
}
|
||||
|
||||
renderResult(results) {
|
||||
this.$resultTarget.empty();
|
||||
this.$resultTarget.append(this.create$result(results));
|
||||
this.busy(false);
|
||||
}
|
||||
|
||||
create$result(results) {
|
||||
let withHits = results.reduce((aggr, subResult) => {
|
||||
if (subResult.hasResults) {
|
||||
aggr.push(subResult);
|
||||
}
|
||||
return aggr;
|
||||
}, []);
|
||||
|
||||
if (!withHits.length) {
|
||||
return create$noHits(this.searchFacade.getSearchUrl());
|
||||
}
|
||||
return create$results(results, this.searchFacade.getSearchUrl());
|
||||
}
|
||||
|
||||
onSearchFailed(err) {
|
||||
toastr.error(xhrErrorResponseMessage(err), 'Unable to perform search:');
|
||||
this.busy(false);
|
||||
this.$inputComponent.trigger('pillar:failed', err);
|
||||
}
|
||||
|
||||
getScope() {
|
||||
return !!this.$searchSelect.length ? this.$searchSelect.val() : 'cloud';
|
||||
}
|
||||
|
||||
busy(val) {
|
||||
if (val !== this.isBusy) {
|
||||
var eventType = val ? 'pillar:workStart' : 'pillar:workStop';
|
||||
this.$inputComponent.trigger(eventType);
|
||||
}
|
||||
this.isBusy = val;
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.extend({
|
||||
/**
|
||||
* $('#qs-toggle').quickSearch({
|
||||
* resultTarget: '#search-overlay',
|
||||
* inputTarget: '#qs-input',
|
||||
* searches: {
|
||||
* project: {
|
||||
* name: 'Project',
|
||||
* uiUrl: '{{ url_for("projects.search", project_url=project.url)}}',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {project: '{{ project._id }}', node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {project: '{{ project._id }}', node_type: 'post'}},
|
||||
* {name: 'Groups', params: {project: '{{ project._id }}', node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* cloud: {
|
||||
* name: 'Cloud',
|
||||
* uiUrl: '/search',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {node_type: 'post'}},
|
||||
* {name: 'Groups', params: {node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
quickSearch: function (kwargs) {
|
||||
$(this).each((i, qsElem) => {
|
||||
new QuickSearch(qsElem, kwargs);
|
||||
});
|
||||
}
|
||||
})
|
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
@ -0,0 +1,68 @@
|
||||
import {MultiSearch} from './MultiSearch';
|
||||
|
||||
export class SearchFacade {
|
||||
/**
|
||||
* One SearchFacade holds n-number of MultiSearch objects, and delegates search requests to the active mutlisearch
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(kwargs) {
|
||||
this.searches = SearchFacade.createMultiSearches(kwargs);
|
||||
this.currentScope = 'cloud'; // which multisearch to use
|
||||
this.currRequest;
|
||||
this.resultCB;
|
||||
this.failureCB;
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
$.each(this.searches, (k, mSearch) => {
|
||||
mSearch.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchWord() {
|
||||
return this.q;
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.searches[this.currentScope].getSearchUrl();
|
||||
}
|
||||
|
||||
setCurrentScope(scope) {
|
||||
this.currentScope = scope;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.currRequest) {
|
||||
this.currRequest.abort();
|
||||
}
|
||||
this.currRequest = this.searches[this.currentScope].thenExecute();
|
||||
this.currRequest
|
||||
.then((results) => {
|
||||
this.resultCB(results);
|
||||
})
|
||||
.fail((err, reason) => {
|
||||
if (reason == 'abort') {
|
||||
return;
|
||||
}
|
||||
this.failureCB(err);
|
||||
});
|
||||
}
|
||||
|
||||
setOnResultCB(cb) {
|
||||
this.resultCB = cb;
|
||||
}
|
||||
|
||||
setOnFailureCB(cb) {
|
||||
this.failureCB = cb;
|
||||
}
|
||||
|
||||
static createMultiSearches(kwargs) {
|
||||
var searches = {};
|
||||
$.each(kwargs, (key, value) => {
|
||||
searches[key] = new MultiSearch(value);
|
||||
});
|
||||
return searches;
|
||||
}
|
||||
}
|
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
@ -0,0 +1,14 @@
|
||||
export class SearchParams {
|
||||
constructor(kwargs) {
|
||||
this.name = kwargs['name'] || '';
|
||||
this.params = kwargs['params'] || {};
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.params['q'] = q || '';
|
||||
}
|
||||
|
||||
getParamStr() {
|
||||
return jQuery.param(this.params);
|
||||
}
|
||||
}
|
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
@ -0,0 +1 @@
|
||||
export { QuickSearch } from './QuickSearch';
|
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Creates the jQuery object that is rendered when nothing is found
|
||||
* @param {String} advancedUrl Url to the advanced search with the current query
|
||||
* @returns {$element} The jQuery element that is rendered wher there are no hits
|
||||
*/
|
||||
function create$noHits(advancedUrl) {
|
||||
return $('<div>')
|
||||
.addClass('qs-msg text-center p-3')
|
||||
.append(
|
||||
$('<div>')
|
||||
.addClass('h1 pi-displeased'),
|
||||
$('<div>')
|
||||
.addClass('h2')
|
||||
.append(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search')
|
||||
)
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Creates the jQuery object that is rendered as the search input
|
||||
* @param {Dict} searches The searches dict that is passed in on construction of the Quick-Search
|
||||
* @returns {$element} The jQuery object that renders the search input components.
|
||||
*/
|
||||
function create$input(searches) {
|
||||
let input = $('<input>')
|
||||
.addClass('qs-input')
|
||||
.attr('type', 'search')
|
||||
.attr('autocomplete', 'off')
|
||||
.attr('spellcheck', 'false')
|
||||
.attr('autocorrect', 'false')
|
||||
.attr('placeholder', 'Search...');
|
||||
let workingSymbol = $('<i>')
|
||||
.addClass('pi-cancel qs-busy-symbol');
|
||||
let inputComponent = [input, workingSymbol];
|
||||
if (Object.keys(searches).length > 1) {
|
||||
let i = 0;
|
||||
let select = $('<select>')
|
||||
.append(
|
||||
$.map(searches, (it, value) => {
|
||||
let option = $('<option>')
|
||||
.attr('value', value)
|
||||
.text(it['name']);
|
||||
if (i === 0) {
|
||||
option.attr('selected', 'selected');
|
||||
}
|
||||
i += 1;
|
||||
return option;
|
||||
})
|
||||
);
|
||||
inputComponent.push(select);
|
||||
}
|
||||
return inputComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the search result
|
||||
* @param {List} results
|
||||
* @param {String} advancedUrl
|
||||
* @returns {$element} The jQuery object that is rendered as the result
|
||||
*/
|
||||
function create$results(results, advancedUrl) {
|
||||
let $results = results.reduce((agg, res)=> {
|
||||
if(res['result'].length) {
|
||||
agg.push(
|
||||
$('<a>')
|
||||
.addClass('h4 mt-4 d-flex')
|
||||
.attr('href', res['url'])
|
||||
.text(res['name'])
|
||||
)
|
||||
agg.push(
|
||||
$('<div>')
|
||||
.addClass('card-deck card-deck-responsive card-padless js-asset-list p-3')
|
||||
.append(
|
||||
...pillar.templates.Nodes.createListOf$nodeItems(res['result'], 10, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
return agg;
|
||||
}, [])
|
||||
$results.push(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search...')
|
||||
)
|
||||
|
||||
return $('<div>')
|
||||
.addClass('m-auto qs-result')
|
||||
.append(...$results)
|
||||
}
|
||||
|
||||
export { create$noHits, create$results, create$input }
|
@ -1 +0,0 @@
|
||||
export { Nodes } from './templates/templates'
|
@ -1,5 +1,4 @@
|
||||
import { Assets } from '../assets'
|
||||
import {} from ''
|
||||
import { Assets } from '../nodes/Assets'
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@ -8,11 +7,16 @@ describe('Assets', () => {
|
||||
let nodeDoc;
|
||||
let spyGet;
|
||||
beforeEach(()=>{
|
||||
// mock now to get a stable pretty printed created
|
||||
Date.now = jest.fn(() => new Date(Date.UTC(2018,
|
||||
10, //November! zero based month!
|
||||
28, 11, 46, 30)).valueOf()); // A Tuesday
|
||||
|
||||
nodeDoc = {
|
||||
_id: 'my-asset-id',
|
||||
name: 'My Asset',
|
||||
pretty_created: '2 hours ago',
|
||||
node_type: 'asset',
|
||||
_created: "Wed, 07 Nov 2018 16:35:09 GMT",
|
||||
project: {
|
||||
name: 'My Project',
|
||||
url: 'url-to-project'
|
||||
@ -52,8 +56,9 @@ describe('Assets', () => {
|
||||
let $card = Assets.create$listItem(nodeDoc);
|
||||
jest.runAllTimers();
|
||||
expect($card.length).toEqual(1);
|
||||
expect($card.prop('tagName')).toEqual('A');
|
||||
expect($card.hasClass('card asset')).toBeTruthy();
|
||||
expect($card.prop('tagName')).toEqual('A'); // <a>
|
||||
expect($card.hasClass('asset')).toBeTruthy();
|
||||
expect($card.hasClass('card')).toBeTruthy();
|
||||
expect($card.attr('href')).toEqual('/nodes/my-asset-id/redir');
|
||||
expect($card.attr('title')).toEqual('My Asset');
|
||||
|
||||
@ -77,14 +82,17 @@ describe('Assets', () => {
|
||||
|
||||
let $watched = $card.find('.card-label');
|
||||
expect($watched.length).toEqual(0);
|
||||
|
||||
expect($card.find(':contains(3 weeks ago)').length).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
test('node without picture', done => {
|
||||
let $card = Assets.create$listItem(nodeDoc);
|
||||
expect($card.length).toEqual(1);
|
||||
expect($card.prop('tagName')).toEqual('A');
|
||||
expect($card.hasClass('card asset')).toBeTruthy();
|
||||
expect($card.prop('tagName')).toEqual('A'); // <a>
|
||||
expect($card.hasClass('asset')).toBeTruthy();
|
||||
expect($card.hasClass('card')).toBeTruthy();
|
||||
expect($card.attr('href')).toEqual('/nodes/my-asset-id/redir');
|
||||
expect($card.attr('title')).toEqual('My Asset');
|
||||
|
||||
@ -107,9 +115,10 @@ describe('Assets', () => {
|
||||
|
||||
let $watched = $card.find('.card-label');
|
||||
expect($watched.length).toEqual(0);
|
||||
|
||||
expect($card.find(':contains(3 weeks ago)').length).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { Assets } from '../nodes/Assets'
|
||||
import { Users } from '../users/Users'
|
||||
import { Component } from '../init' // Component is initialized in init
|
||||
|
||||
describe('Component', () => {
|
||||
test('can create Users listItem', () => {
|
||||
let userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
|
||||
let $user_actual = Component.create$listItem(userDoc);
|
||||
expect($user_actual.length).toBe(1);
|
||||
|
||||
let $user_reference = Users.create$listItem(userDoc);
|
||||
expect($user_actual).toEqual($user_reference);
|
||||
});
|
||||
|
||||
test('can create Asset listItem', () => {
|
||||
let nodeDoc = {
|
||||
_id: 'my-asset-id',
|
||||
name: 'My Asset',
|
||||
node_type: 'asset',
|
||||
project: {
|
||||
name: 'My Project',
|
||||
url: 'url-to-project'
|
||||
},
|
||||
properties: {
|
||||
content_type: 'image'
|
||||
}
|
||||
};
|
||||
|
||||
let $asset_actual = Component.create$listItem(nodeDoc);
|
||||
expect($asset_actual.length).toBe(1);
|
||||
|
||||
let $asset_reference = Assets.create$listItem(nodeDoc);
|
||||
expect($asset_actual).toEqual($asset_reference);
|
||||
});
|
||||
|
||||
test('fail to create unknown', () => {
|
||||
expect(()=>Component.create$listItem({})).toThrow('Can not create component using: {}')
|
||||
expect(()=>Component.create$listItem()).toThrow('Can not create component using: undefined')
|
||||
expect(()=>Component.create$listItem({strange: 'value'}))
|
||||
.toThrow('Can not create component using: {"strange":"value"}')
|
||||
});
|
||||
});
|
67
src/scripts/js/es6/common/templates/__tests__/utils.test.js
Normal file
67
src/scripts/js/es6/common/templates/__tests__/utils.test.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { prettyDate } from '../utils'
|
||||
|
||||
describe('prettydate', () => {
|
||||
beforeEach(() => {
|
||||
Date.now = jest.fn(() => new Date(Date.UTC(2016,
|
||||
10, //November! zero based month!
|
||||
8, 11, 46, 30)).valueOf()); // A Tuesday
|
||||
});
|
||||
|
||||
test('bad input', () => {
|
||||
expect(prettyDate(undefined)).toBeUndefined();
|
||||
expect(prettyDate(null)).toBeUndefined();
|
||||
expect(prettyDate('my birthday')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('past dates',() => {
|
||||
expect(pd({seconds: -5})).toBe('just now');
|
||||
expect(pd({minutes: -5})).toBe('5m ago')
|
||||
expect(pd({days: -7})).toBe('last Tuesday')
|
||||
expect(pd({days: -8})).toBe('1 week ago')
|
||||
expect(pd({days: -14})).toBe('2 weeks ago')
|
||||
expect(pd({days: -31})).toBe('8 Oct')
|
||||
expect(pd({days: -(31 + 366)})).toBe('8 Oct 2015')
|
||||
});
|
||||
|
||||
test('past dates with time',() => {
|
||||
expect(pd({seconds: -5, detailed: true})).toBe('just now');
|
||||
expect(pd({minutes: -5, detailed: true})).toBe('5m ago')
|
||||
expect(pd({days: -7, detailed: true})).toBe('last Tuesday at 11:46')
|
||||
expect(pd({days: -8, detailed: true})).toBe('1 week ago at 11:46')
|
||||
// summer time bellow
|
||||
expect(pd({days: -14, detailed: true})).toBe('2 weeks ago at 10:46')
|
||||
expect(pd({days: -31, detailed: true})).toBe('8 Oct at 10:46')
|
||||
expect(pd({days: -(31 + 366), detailed: true})).toBe('8 Oct 2015 at 10:46')
|
||||
});
|
||||
|
||||
test('future dates',() => {
|
||||
expect(pd({seconds: 5})).toBe('just now')
|
||||
expect(pd({minutes: 5})).toBe('in 5m')
|
||||
expect(pd({days: 7})).toBe('next Tuesday')
|
||||
expect(pd({days: 8})).toBe('in 1 week')
|
||||
expect(pd({days: 14})).toBe('in 2 weeks')
|
||||
expect(pd({days: 30})).toBe('8 Dec')
|
||||
expect(pd({days: 30 + 365})).toBe('8 Dec 2017')
|
||||
});
|
||||
|
||||
test('future dates',() => {
|
||||
expect(pd({seconds: 5, detailed: true})).toBe('just now')
|
||||
expect(pd({minutes: 5, detailed: true})).toBe('in 5m')
|
||||
expect(pd({days: 7, detailed: true})).toBe('next Tuesday at 11:46')
|
||||
expect(pd({days: 8, detailed: true})).toBe('in 1 week at 11:46')
|
||||
expect(pd({days: 14, detailed: true})).toBe('in 2 weeks at 11:46')
|
||||
expect(pd({days: 30, detailed: true})).toBe('8 Dec at 11:46')
|
||||
expect(pd({days: 30 + 365, detailed: true})).toBe('8 Dec 2017 at 11:46')
|
||||
});
|
||||
|
||||
function pd(params) {
|
||||
let theDate = new Date(Date.now());
|
||||
theDate.setFullYear(theDate.getFullYear() + (params['years'] || 0));
|
||||
theDate.setMonth(theDate.getMonth() + (params['months'] || 0));
|
||||
theDate.setDate(theDate.getDate() + (params['days'] || 0));
|
||||
theDate.setHours(theDate.getHours() + (params['hours'] || 0));
|
||||
theDate.setMinutes(theDate.getMinutes() + (params['minutes'] || 0));
|
||||
theDate.setSeconds(theDate.getSeconds() + (params['seconds'] || 0));
|
||||
return prettyDate(theDate, (params['detailed'] || false))
|
||||
}
|
||||
});
|
@ -1,97 +0,0 @@
|
||||
import { NodesFactoryInterface } from './nodes'
|
||||
import { thenLoadImage, thenLoadVideoProgress } from './utils';
|
||||
|
||||
class Assets extends NodesFactoryInterface{
|
||||
static create$listItem(node) {
|
||||
var markIfPublic = true;
|
||||
let $card = $('<a class="card asset card-image-fade pr-0 mx-0 mb-2">')
|
||||
.addClass('js-tagged-asset')
|
||||
.attr('href', '/nodes/' + node._id + '/redir')
|
||||
.attr('title', node.name);
|
||||
|
||||
let $thumbnailContainer = $('<div class="embed-responsive embed-responsive-16by9">');
|
||||
|
||||
function warnNoPicture() {
|
||||
let $cardIcon = $('<div class="card-img-top card-icon embed-responsive-item">');
|
||||
$cardIcon.html('<i class="pi-' + node.node_type + '">');
|
||||
$thumbnailContainer.append($cardIcon);
|
||||
}
|
||||
|
||||
if (!node.picture) {
|
||||
warnNoPicture();
|
||||
} else {
|
||||
$(window).trigger('pillar:workStart');
|
||||
|
||||
thenLoadImage(node.picture)
|
||||
.fail(warnNoPicture)
|
||||
.then((imgVariation)=>{
|
||||
let img = $('<img class="card-img-top embed-responsive-item">')
|
||||
.attr('alt', node.name)
|
||||
.attr('src', imgVariation.link)
|
||||
.attr('width', imgVariation.width)
|
||||
.attr('height', imgVariation.height);
|
||||
$thumbnailContainer.append(img);
|
||||
})
|
||||
.always(function(){
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
$card.append($thumbnailContainer);
|
||||
|
||||
/* Card body for title and meta info. */
|
||||
let $cardBody = $('<div class="card-body py-2 d-flex flex-column">');
|
||||
let $cardTitle = $('<div class="card-title mb-1 font-weight-bold">');
|
||||
$cardTitle.text(node.name);
|
||||
$cardBody.append($cardTitle);
|
||||
|
||||
let $cardMeta = $('<ul class="card-text list-unstyled d-flex text-black-50 mt-auto">');
|
||||
let $cardProject = $('<a class="font-weight-bold pr-2">')
|
||||
.attr('href', '/p/' + node.project.url)
|
||||
.attr('title', node.project.name)
|
||||
.text(node.project.name);
|
||||
|
||||
$cardMeta.append($cardProject);
|
||||
$cardMeta.append('<li>' + node.pretty_created + '</li>');
|
||||
$cardBody.append($cardMeta);
|
||||
|
||||
if (node.properties.duration){
|
||||
let $cardDuration = $('<div class="card-label right">' + node.properties.duration + '</div>');
|
||||
$thumbnailContainer.append($cardDuration);
|
||||
|
||||
/* Video progress and 'watched' label. */
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadVideoProgress(node._id)
|
||||
.fail(console.log)
|
||||
.then((view_progress)=>{
|
||||
if (!view_progress) return
|
||||
|
||||
let $cardProgress = $('<div class="progress rounded-0">');
|
||||
let $cardProgressBar = $('<div class="progress-bar">');
|
||||
$cardProgressBar.css('width', view_progress.progress_in_percent + '%');
|
||||
$cardProgress.append($cardProgressBar);
|
||||
$thumbnailContainer.append($cardProgress);
|
||||
|
||||
if (view_progress.done){
|
||||
let card_progress_done = $('<div class="card-label">WATCHED</div>');
|
||||
$thumbnailContainer.append(card_progress_done);
|
||||
}
|
||||
})
|
||||
.always(function() {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
/* 'Free' ribbon for public assets. */
|
||||
if (markIfPublic && node.permissions && node.permissions.world){
|
||||
$card.addClass('free');
|
||||
}
|
||||
|
||||
$card.append($cardBody);
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { Assets };
|
34
src/scripts/js/es6/common/templates/component/Component.js
Normal file
34
src/scripts/js/es6/common/templates/component/Component.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { ComponentCreatorInterface } from './ComponentCreatorInterface'
|
||||
|
||||
const REGISTERED_CREATORS = []
|
||||
|
||||
export class Component extends ComponentCreatorInterface {
|
||||
static create$listItem(doc) {
|
||||
let creator = Component.getCreator(doc);
|
||||
return creator.create$listItem(doc);
|
||||
}
|
||||
|
||||
static create$item(doc) {
|
||||
let creator = Component.getCreator(doc);
|
||||
return creator.create$item(doc);
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!Component.getCreator(candidate);
|
||||
}
|
||||
|
||||
static regiseterCreator(creator) {
|
||||
REGISTERED_CREATORS.push(creator);
|
||||
}
|
||||
|
||||
static getCreator(doc) {
|
||||
if (doc) {
|
||||
for (let candidate of REGISTERED_CREATORS) {
|
||||
if (candidate.canCreate(doc)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'Can not create component using: ' + JSON.stringify(doc);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
export class ComponentCreatorInterface {
|
||||
/**
|
||||
* @param {JSON} doc
|
||||
* @returns {$element}
|
||||
*/
|
||||
static create$listItem(doc) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSON} doc
|
||||
* @returns {$element}
|
||||
*/
|
||||
static create$item(doc) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSON} candidate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canCreate(candidate) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
}
|
18
src/scripts/js/es6/common/templates/init.js
Normal file
18
src/scripts/js/es6/common/templates/init.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { Nodes } from './nodes/Nodes';
|
||||
import { Assets } from './nodes/Assets';
|
||||
import { Posts } from './nodes/Posts';
|
||||
|
||||
import { Users } from './users/Users';
|
||||
import { Component } from './component/Component';
|
||||
|
||||
Nodes.registerTemplate('asset', Assets);
|
||||
Nodes.registerTemplate('post', Posts);
|
||||
|
||||
Component.regiseterCreator(Nodes);
|
||||
Component.regiseterCreator(Users);
|
||||
|
||||
export {
|
||||
Nodes,
|
||||
Users,
|
||||
Component
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
|
||||
let CREATE_NODE_ITEM_MAP = {}
|
||||
|
||||
class Nodes {
|
||||
static create$listItem(node) {
|
||||
return CREATE_NODE_ITEM_MAP[node.node_type].create$listItem(node);
|
||||
}
|
||||
|
||||
static create$item(node) {
|
||||
return CREATE_NODE_ITEM_MAP[node.node_type].create$item(node);
|
||||
}
|
||||
|
||||
static createListOf$nodeItems(nodes, initial=8, loadNext=8) {
|
||||
let nodesLeftToRender = nodes.slice();
|
||||
let nodesToCreate = nodesLeftToRender.splice(0, initial);
|
||||
let listOf$items = nodesToCreate.map(Nodes.create$listItem);
|
||||
|
||||
if (loadNext > 0 && nodesLeftToRender.length) {
|
||||
let $link = $('<a>')
|
||||
.addClass('btn btn-outline-primary px-5 mb-auto btn-block js-load-next')
|
||||
.attr('href', 'javascript:void(0);')
|
||||
.click((e)=> {
|
||||
let $target = $(e.target);
|
||||
$target.replaceWith(Nodes.createListOf$nodeItems(nodesLeftToRender, loadNext, loadNext));
|
||||
})
|
||||
.text('Load More');
|
||||
|
||||
listOf$items.push($link);
|
||||
}
|
||||
return listOf$items;
|
||||
}
|
||||
|
||||
static registerTemplate(key, klass) {
|
||||
CREATE_NODE_ITEM_MAP[key] = klass;
|
||||
}
|
||||
}
|
||||
|
||||
class NodesFactoryInterface{
|
||||
static create$listItem(node) {
|
||||
throw 'Not Implemented'
|
||||
}
|
||||
|
||||
static create$item(node) {
|
||||
throw 'Not Implemented'
|
||||
}
|
||||
}
|
||||
|
||||
export { Nodes, NodesFactoryInterface };
|
45
src/scripts/js/es6/common/templates/nodes/Assets.js
Normal file
45
src/scripts/js/es6/common/templates/nodes/Assets.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { NodesBase } from "./NodesBase";
|
||||
import { thenLoadVideoProgress } from '../utils';
|
||||
|
||||
export class Assets extends NodesBase{
|
||||
static create$listItem(node) {
|
||||
var markIfPublic = true;
|
||||
let $card = super.create$listItem(node);
|
||||
$card.addClass('asset');
|
||||
|
||||
if (node.properties && node.properties.duration){
|
||||
let $thumbnailContainer = $card.find('.js-thumbnail-container')
|
||||
let $cardDuration = $('<div class="card-label right">' + node.properties.duration + '</div>');
|
||||
$thumbnailContainer.append($cardDuration);
|
||||
|
||||
/* Video progress and 'watched' label. */
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadVideoProgress(node._id)
|
||||
.fail(console.log)
|
||||
.then((view_progress)=>{
|
||||
if (!view_progress) return
|
||||
|
||||
let $cardProgress = $('<div class="progress rounded-0">');
|
||||
let $cardProgressBar = $('<div class="progress-bar">');
|
||||
$cardProgressBar.css('width', view_progress.progress_in_percent + '%');
|
||||
$cardProgress.append($cardProgressBar);
|
||||
$thumbnailContainer.append($cardProgress);
|
||||
|
||||
if (view_progress.done){
|
||||
let card_progress_done = $('<div class="card-label">WATCHED</div>');
|
||||
$thumbnailContainer.append(card_progress_done);
|
||||
}
|
||||
})
|
||||
.always(function() {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
|
||||
/* 'Free' ribbon for public assets. */
|
||||
if (markIfPublic && node.permissions && node.permissions.world){
|
||||
$card.addClass('free');
|
||||
}
|
||||
|
||||
return $card;
|
||||
}
|
||||
}
|
63
src/scripts/js/es6/common/templates/nodes/Nodes.js
Normal file
63
src/scripts/js/es6/common/templates/nodes/Nodes.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { NodesBase } from './NodesBase';
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
let CREATE_NODE_ITEM_MAP = {}
|
||||
|
||||
export class Nodes extends ComponentCreatorInterface {
|
||||
/**
|
||||
* Creates a small list item out of a node document
|
||||
* @param {NodeDoc} node mongodb or elastic node document
|
||||
*/
|
||||
static create$listItem(node) {
|
||||
let factory = CREATE_NODE_ITEM_MAP[node.node_type] || NodesBase;
|
||||
return factory.create$listItem(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a full view out of a node document
|
||||
* @param {NodeDoc} node mongodb or elastic node document
|
||||
*/
|
||||
static create$item(node) {
|
||||
let factory = CREATE_NODE_ITEM_MAP[node.node_type] || NodesBase;
|
||||
return factory.create$item(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of items and a 'Load More' button
|
||||
* @param {List} nodes A list of nodes to be created
|
||||
* @param {Int} initial Number of nodes to show initially
|
||||
* @param {Int} loadNext Number of nodes to show when clicking 'Load More'. If 0, no load more button will be shown
|
||||
*/
|
||||
static createListOf$nodeItems(nodes, initial=8, loadNext=8) {
|
||||
let nodesLeftToRender = nodes.slice();
|
||||
let nodesToCreate = nodesLeftToRender.splice(0, initial);
|
||||
let listOf$items = nodesToCreate.map(Nodes.create$listItem);
|
||||
|
||||
if (loadNext > 0 && nodesLeftToRender.length) {
|
||||
let $link = $('<a>')
|
||||
.addClass('btn btn-outline-primary px-5 mb-auto btn-block js-load-next')
|
||||
.attr('href', 'javascript:void(0);')
|
||||
.click((e)=> {
|
||||
let $target = $(e.target);
|
||||
$target.replaceWith(Nodes.createListOf$nodeItems(nodesLeftToRender, loadNext, loadNext));
|
||||
})
|
||||
.text('Load More');
|
||||
|
||||
listOf$items.push($link);
|
||||
}
|
||||
return listOf$items;
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.node_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register template classes to handle the cunstruction of diffrent node types
|
||||
* @param { String } node_type The node type whose template that is registered
|
||||
* @param { NodesBase } klass The class to handle the creation of jQuery objects
|
||||
*/
|
||||
static registerTemplate(node_type, klass) {
|
||||
CREATE_NODE_ITEM_MAP[node_type] = klass;
|
||||
}
|
||||
}
|
58
src/scripts/js/es6/common/templates/nodes/NodesBase.js
Normal file
58
src/scripts/js/es6/common/templates/nodes/NodesBase.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { thenLoadImage, prettyDate } from '../utils';
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
export class NodesBase extends ComponentCreatorInterface {
|
||||
static create$listItem(node) {
|
||||
let nid = (node._id || node.objectID); // To support both mongo and elastic nodes
|
||||
let $card = $('<a class="card node card-image-fade asset">')
|
||||
.attr('data-node-id', nid)
|
||||
.attr('href', '/nodes/' + nid + '/redir')
|
||||
.attr('title', node.name);
|
||||
let $thumbnailContainer = $('<div class="card-thumbnail js-thumbnail-container">');
|
||||
function warnNoPicture() {
|
||||
let $cardIcon = $('<div class="card-img-top card-icon">');
|
||||
$cardIcon.html('<i class="pi-' + node.node_type + '">');
|
||||
$thumbnailContainer.append($cardIcon);
|
||||
}
|
||||
if (!node.picture) {
|
||||
warnNoPicture();
|
||||
}
|
||||
else {
|
||||
$(window).trigger('pillar:workStart');
|
||||
thenLoadImage(node.picture)
|
||||
.fail(warnNoPicture)
|
||||
.then((imgVariation) => {
|
||||
let img = $('<img class="card-img-top">')
|
||||
.attr('alt', node.name)
|
||||
.attr('src', imgVariation.link)
|
||||
.attr('width', imgVariation.width)
|
||||
.attr('height', imgVariation.height);
|
||||
$thumbnailContainer.append(img);
|
||||
})
|
||||
.always(function () {
|
||||
$(window).trigger('pillar:workStop');
|
||||
});
|
||||
}
|
||||
$card.append($thumbnailContainer);
|
||||
/* Card body for title and meta info. */
|
||||
let $cardBody = $('<div class="card-body p-2 d-flex flex-column">');
|
||||
let $cardTitle = $('<div class="card-title px-2 mb-2 font-weight-bold">');
|
||||
$cardTitle.text(node.name);
|
||||
$cardBody.append($cardTitle);
|
||||
let $cardMeta = $('<ul class="card-text px-2 list-unstyled d-flex text-black-50 mt-auto">');
|
||||
let $cardProject = $('<a class="font-weight-bold pr-2">')
|
||||
.attr('href', '/p/' + node.project.url)
|
||||
.attr('title', node.project.name)
|
||||
.text(node.project.name);
|
||||
$cardMeta.append($cardProject);
|
||||
let created = node._created || node.created_at; // mongodb + elastic
|
||||
$cardMeta.append('<li>' + prettyDate(created) + '</li>');
|
||||
$cardBody.append($cardMeta);
|
||||
$card.append($cardBody);
|
||||
return $card;
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.node_type;
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import { NodesFactoryInterface } from './nodes'
|
||||
import { NodesBase } from "./NodesBase";
|
||||
|
||||
class Posts extends NodesFactoryInterface {
|
||||
export class Posts extends NodesBase {
|
||||
static create$item(post) {
|
||||
let content = [];
|
||||
let $title = $('<a>')
|
||||
.attr('href', '/nodes/' + post._id + '/redir')
|
||||
.addClass('h2 text-uppercase font-weight-bold d-block pb-3')
|
||||
let $title = $('<div>')
|
||||
.addClass('display-4 text-uppercase font-weight-bold')
|
||||
.text(post.name);
|
||||
content.push($title);
|
||||
let $post = $('<div>')
|
||||
@ -20,5 +19,3 @@ class Posts extends NodesFactoryInterface {
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
export { Posts };
|
@ -1,8 +0,0 @@
|
||||
import { Nodes } from './nodes';
|
||||
import { Assets } from './assets';
|
||||
import { Posts } from './posts';
|
||||
|
||||
Nodes.registerTemplate('asset', Assets);
|
||||
Nodes.registerTemplate('post', Posts);
|
||||
|
||||
export { Nodes };
|
23
src/scripts/js/es6/common/templates/users/Users.js
Normal file
23
src/scripts/js/es6/common/templates/users/Users.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentCreatorInterface } from '../component/ComponentCreatorInterface'
|
||||
|
||||
export class Users extends ComponentCreatorInterface {
|
||||
static create$listItem(userDoc) {
|
||||
return $('<div>')
|
||||
.addClass('users p-2 border-bottom')
|
||||
.attr('data-user-id', userDoc._id || userDoc.objectID )
|
||||
.append(
|
||||
$('<h6>')
|
||||
.addClass('mb-0 font-weight-bold')
|
||||
.text(userDoc.full_name),
|
||||
$('<small>')
|
||||
.text(userDoc.username),
|
||||
$('<small>')
|
||||
.addClass('d-block roles text-info')
|
||||
.text(userDoc.roles.join(', '))
|
||||
)
|
||||
}
|
||||
|
||||
static canCreate(candidate) {
|
||||
return !!candidate.username;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { Users } from '../Users'
|
||||
|
||||
describe('Users', () => {
|
||||
let userDoc;
|
||||
describe('create$listItem', () => {
|
||||
beforeEach(()=>{
|
||||
userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
});
|
||||
test('happy case', () => {
|
||||
let $user = Users.create$listItem(userDoc);
|
||||
expect($user.length).toBe(1);
|
||||
expect($user.hasClass('users')).toBeTruthy();
|
||||
expect($user.data('user-id')).toBe('my-user-id');
|
||||
|
||||
let $username = $user.find(':contains(My User Name)');
|
||||
expect($username.length).toBe(1);
|
||||
|
||||
let $fullName = $user.find(':contains(My full name)');
|
||||
expect($fullName.length).toBe(1);
|
||||
|
||||
let $roles = $user.find('.roles');
|
||||
expect($roles.length).toBe(1);
|
||||
expect($roles.text()).toBe('admin, subscriber')
|
||||
});
|
||||
})
|
||||
|
||||
describe('create$item', () => {
|
||||
beforeEach(()=>{
|
||||
userDoc = {
|
||||
_id: 'my-user-id',
|
||||
username: 'My User Name',
|
||||
full_name: 'My full name',
|
||||
roles: ['admin', 'subscriber']
|
||||
};
|
||||
});
|
||||
test('Not Implemented', () => {
|
||||
// Replace with proper test once implemented
|
||||
expect(()=>Users.create$item(userDoc)).toThrow('Not Implemented');
|
||||
});
|
||||
})
|
||||
});
|
@ -21,4 +21,102 @@ function thenLoadVideoProgress(nodeId) {
|
||||
return $.get('/api/users/video/' + nodeId + '/progress')
|
||||
}
|
||||
|
||||
export { thenLoadImage, thenLoadVideoProgress };
|
||||
function prettyDate(time, detail=false) {
|
||||
/**
|
||||
* time is anything Date can parse, and we return a
|
||||
pretty string like 'an hour ago', 'Yesterday', '3 months ago',
|
||||
'just now', etc
|
||||
*/
|
||||
let theDate = new Date(time);
|
||||
if (!time || isNaN(theDate)) {
|
||||
return
|
||||
}
|
||||
let pretty = '';
|
||||
let now = new Date(Date.now()); // Easier to mock Date.now() in tests
|
||||
let second_diff = Math.round((now - theDate) / 1000);
|
||||
|
||||
let day_diff = Math.round(second_diff / 86400); // seconds per day (60*60*24)
|
||||
|
||||
if ((day_diff < 0) && (theDate.getFullYear() !== now.getFullYear())) {
|
||||
// "Jul 16, 2018"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short', year: 'numeric'});
|
||||
}
|
||||
else if ((day_diff < -21) && (theDate.getFullYear() == now.getFullYear())) {
|
||||
// "Jul 16"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short'});
|
||||
}
|
||||
else if (day_diff < -7){
|
||||
let week_count = Math.round(-day_diff / 7);
|
||||
if (week_count == 1)
|
||||
pretty = "in 1 week";
|
||||
else
|
||||
pretty = "in " + week_count +" weeks";
|
||||
}
|
||||
else if (day_diff < -1)
|
||||
// "next Tuesday"
|
||||
pretty = 'next ' + theDate.toLocaleDateString('en-NL',{weekday: 'long'});
|
||||
else if (day_diff === 0) {
|
||||
if (second_diff < 0) {
|
||||
let seconds = Math.abs(second_diff);
|
||||
if (seconds < 10)
|
||||
return 'just now';
|
||||
if (seconds < 60)
|
||||
return 'in ' + seconds +'s';
|
||||
if (seconds < 120)
|
||||
return 'in a minute';
|
||||
if (seconds < 3600)
|
||||
return 'in ' + Math.round(seconds / 60) + 'm';
|
||||
if (seconds < 7200)
|
||||
return 'in an hour';
|
||||
if (seconds < 86400)
|
||||
return 'in ' + Math.round(seconds / 3600) + 'h';
|
||||
} else {
|
||||
let seconds = second_diff;
|
||||
if (seconds < 10)
|
||||
return "just now";
|
||||
if (seconds < 60)
|
||||
return seconds + "s ago";
|
||||
if (seconds < 120)
|
||||
return "a minute ago";
|
||||
if (seconds < 3600)
|
||||
return Math.round(seconds / 60) + "m ago";
|
||||
if (seconds < 7200)
|
||||
return "an hour ago";
|
||||
if (seconds < 86400)
|
||||
return Math.round(seconds / 3600) + "h ago";
|
||||
}
|
||||
|
||||
}
|
||||
else if (day_diff == 1)
|
||||
pretty = "yesterday";
|
||||
|
||||
else if (day_diff <= 7)
|
||||
// "last Tuesday"
|
||||
pretty = 'last ' + theDate.toLocaleDateString('en-NL',{weekday: 'long'});
|
||||
|
||||
else if (day_diff <= 22) {
|
||||
let week_count = Math.round(day_diff / 7);
|
||||
if (week_count == 1)
|
||||
pretty = "1 week ago";
|
||||
else
|
||||
pretty = week_count + " weeks ago";
|
||||
}
|
||||
else if (theDate.getFullYear() === now.getFullYear())
|
||||
// "Jul 16"
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short'});
|
||||
|
||||
else
|
||||
// "Jul 16", 2009
|
||||
pretty = theDate.toLocaleDateString('en-NL',{day: 'numeric', month: 'short', year: 'numeric'});
|
||||
|
||||
if (detail){
|
||||
// "Tuesday at 04:20"
|
||||
let paddedHour = ('00' + theDate.getUTCHours()).substr(-2);
|
||||
let paddedMin = ('00' + theDate.getUTCMinutes()).substr(-2);
|
||||
return pretty + ' at ' + paddedHour + ':' + paddedMin;
|
||||
}
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
export { thenLoadImage, thenLoadVideoProgress, prettyDate };
|
@ -1 +0,0 @@
|
||||
export { transformPlaceholder } from './utils/placeholder'
|
1
src/scripts/js/es6/common/utils/init.js
Normal file
1
src/scripts/js/es6/common/utils/init.js
Normal file
@ -0,0 +1 @@
|
||||
export { transformPlaceholder } from './placeholder'
|
@ -20,7 +20,7 @@
|
||||
const DEFAULT_URL = '/api/timeline';
|
||||
const transformPlaceholder = pillar.utils.transformPlaceholder;
|
||||
|
||||
class Timeline {
|
||||
export class Timeline {
|
||||
constructor(target, builder) {
|
||||
this._$targetDom = $(target);
|
||||
this._url;
|
||||
@ -197,5 +197,3 @@ $.fn.extend({
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
export { Timeline };
|
@ -1,4 +1,4 @@
|
||||
export { Timeline } from './timeline/timeline';
|
||||
export { Timeline } from './Timeline';
|
||||
|
||||
// Init timelines on document ready
|
||||
$(function() {
|
@ -65,18 +65,21 @@ var elasticSearcher = (function() {
|
||||
return false;
|
||||
}),
|
||||
|
||||
//get response from elastic and rebuild json
|
||||
//so we can be a drop in of angolia
|
||||
execute: (function(){
|
||||
params = {
|
||||
getParams:(function(){
|
||||
var params = {
|
||||
q: deze.query,
|
||||
page: deze.page,
|
||||
project: deze.project_id,
|
||||
};
|
||||
//add term filters
|
||||
Object.assign(params, deze.terms);
|
||||
return params;
|
||||
}),
|
||||
|
||||
var pstr = jQuery.param( params );
|
||||
//get response from elastic and rebuild json
|
||||
//so we can be a drop in of angolia
|
||||
execute: (function(){
|
||||
var pstr = jQuery.param( deze.getParams() );
|
||||
if (pstr === deze.last_query) return;
|
||||
|
||||
$.getJSON("/api/newsearch" + deze.url + "?"+ pstr)
|
||||
@ -117,6 +120,7 @@ var elasticSearcher = (function() {
|
||||
page: deze.page,
|
||||
toggleTerm: deze.toggleTerm,
|
||||
isRefined: deze.isRefined,
|
||||
getParams: deze.getParams,
|
||||
};
|
||||
|
||||
})();
|
||||
@ -155,114 +159,3 @@ var elasticSearch = (function($, url) {
|
||||
|
||||
}(jQuery));
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var searchInput = $('#cloud-search');
|
||||
if (!searchInput.length) return;
|
||||
|
||||
var tu = searchInput.typeahead({hint: true}, {
|
||||
//source: algoliaIndex.ttAdapter(),
|
||||
source: elasticSearch($),
|
||||
async: true,
|
||||
displayKey: 'name',
|
||||
limit: 9, // Above 10 it stops working from
|
||||
// some magic reason
|
||||
minLength: 0,
|
||||
templates: {
|
||||
suggestion: function(hit) {
|
||||
var hitFree = (hit.is_free ? '<div class="search-hit-ribbon"><span>free</span></div>' : '');
|
||||
var hitPicture;
|
||||
|
||||
if (hit.picture){
|
||||
hitPicture = '<img src="' + hit.picture + '"/>';
|
||||
} else {
|
||||
hitPicture = '<div class="search-hit-thumbnail-icon">';
|
||||
hitPicture += (hit.media ? '<i class="pi-' + hit.media + '"></i>' : '<i class="dark pi-'+ hit.node_type + '"></i>');
|
||||
hitPicture += '</div>';
|
||||
}
|
||||
var $span = $('<span>').addClass('project').text(hit.project.name);
|
||||
var $searchHitName = $('<div>').addClass('search-hit-name')
|
||||
.attr('title', hit.name)
|
||||
.text(hit.name);
|
||||
|
||||
const $nodeType = $('<span>').addClass('node_type').text(hit.node_type);
|
||||
const hitMedia = (hit.media ? ' · ' + $('<span>').addClass('media').text(hit.media)[0].outerHTML : '');
|
||||
|
||||
return $('<a/>', {
|
||||
href: '/nodes/'+ hit.objectID + '/redir',
|
||||
class: "search-site-result",
|
||||
id: hit.objectID
|
||||
}).append(
|
||||
'<div class="search-hit">' +
|
||||
'<div class="search-hit-thumbnail">' +
|
||||
hitPicture +
|
||||
hitFree +
|
||||
'</div>' +
|
||||
$searchHitName.html() +
|
||||
'<div class="search-hit-meta">' +
|
||||
$span.html() + ' · ' +
|
||||
$nodeType.html() +
|
||||
hitMedia +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('.search-site-result.advanced, .search-icon').on('click', function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.location.href = '/search?q='+ $("#cloud-search").val() + '&page=1';
|
||||
});
|
||||
|
||||
|
||||
searchInput.bind('typeahead:select', function(ev, hit) {
|
||||
$('.search-icon').removeClass('pi-search').addClass('pi-spin spin');
|
||||
|
||||
window.location.href = '/nodes/'+ hit.objectID + '/redir';
|
||||
});
|
||||
|
||||
searchInput.bind('typeahead:active', function() {
|
||||
$('#search-overlay').addClass('active');
|
||||
$('.page-body').addClass('blur');
|
||||
});
|
||||
|
||||
searchInput.bind('typeahead:close', function() {
|
||||
$('#search-overlay').removeClass('active');
|
||||
$('.page-body').removeClass('blur');
|
||||
});
|
||||
|
||||
searchInput.keyup(function(e) {
|
||||
if ( $('.tt-dataset').is(':empty') ){
|
||||
if(e.keyCode == 13){
|
||||
window.location.href = '/search#q='+ $("#cloud-search").val() + '&page=1';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.bind('typeahead:render', function(event, suggestions, async, dataset) {
|
||||
if( suggestions != undefined && $('.tt-all-results').length <= 0){
|
||||
$('.tt-dataset').append(
|
||||
$("<a/>", {
|
||||
id: "search-advanced",
|
||||
href: '/search?q='+ $("#cloud-search").val() + '&page=1',
|
||||
class: "search-site-result advanced tt-suggestion",
|
||||
}).append(
|
||||
'<div class="search-hit">' +
|
||||
'<div class="search-hit-thumbnail">' +
|
||||
'<div class="search-hit-thumbnail-icon">' +
|
||||
'<i class="pi-search"></i>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="search-hit-name">' +
|
||||
'Use Advanced Search' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -42,6 +42,9 @@ $color-info: #68B3C8 !default
|
||||
$color-success: #27AE60 !default
|
||||
$color-danger: #EB5E28 !default
|
||||
|
||||
$short-transition: 150ms
|
||||
$long-transition: 650ms
|
||||
|
||||
/* Borrowed from dillo.space :) */
|
||||
$color_upvote: #ff8b60 !default
|
||||
$color_downvote: #74a4ff !default
|
||||
|
@ -6,6 +6,7 @@ body.edit, body.sharing, body.attract, body.flamenco,
|
||||
body.svnman, body.edit_node_types, body.search-project
|
||||
nav.navbar
|
||||
@extend .navbar-dark
|
||||
padding-right: 5px
|
||||
|
||||
#project-container
|
||||
display: flex
|
||||
|
@ -311,6 +311,9 @@ $search-hit-width_grid: 100px
|
||||
display: none
|
||||
|
||||
#search-sidebar
|
||||
.facet-title
|
||||
text-transform: capitalize
|
||||
|
||||
.toggleRefine
|
||||
display: block
|
||||
padding-left: 7px
|
||||
@ -480,4 +483,4 @@ $search-hit-width_grid: 100px
|
||||
&.active
|
||||
color: white
|
||||
background-color: $primary
|
||||
border-color: transparent
|
||||
border-color: transparent
|
@ -143,6 +143,7 @@
|
||||
border-color: $primary
|
||||
box-shadow: none
|
||||
color: $color-text
|
||||
outline: 0
|
||||
|
||||
=label-generic
|
||||
color: $color-text-dark-primary
|
||||
|
@ -46,6 +46,7 @@
|
||||
@import "components/buttons"
|
||||
@import "components/tooltip"
|
||||
@import "components/overlay"
|
||||
@import "components/search"
|
||||
|
||||
@import _comments
|
||||
@import _notifications
|
||||
|
@ -11,6 +11,17 @@ body
|
||||
max-width: 100%
|
||||
min-width: auto
|
||||
|
||||
body.has-overlay
|
||||
overflow: hidden
|
||||
padding-right: 5px
|
||||
|
||||
.page-body
|
||||
filter: blur(15px)
|
||||
transition: filter $short-transition
|
||||
|
||||
.card
|
||||
box-shadow: 1px 1px rgba(black, .1), 1px 10px 25px rgba(black, .05)
|
||||
|
||||
.page-content
|
||||
background-color: $white
|
||||
|
||||
|
@ -1,38 +1,45 @@
|
||||
.card-deck
|
||||
// Custom, as of bootstrap 4.1.3 there is no way to do this.
|
||||
&.card-deck-responsive
|
||||
@extend .row
|
||||
|
||||
.card
|
||||
@extend .col-md-4
|
||||
margin: 0 0 20px 0
|
||||
$card-width-percentage: 30%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
|
||||
+media-xs
|
||||
$card-width-percentage: 100%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
|
||||
+media-sm
|
||||
flex: 1 0 50%
|
||||
max-width: 50%
|
||||
$card-width-percentage: 46%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
margin-right: 100% / ($card-width-percentage / 1%)
|
||||
|
||||
+media-md
|
||||
flex: 1 0 33%
|
||||
max-width: 33%
|
||||
$card-width-percentage: 30%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
margin-right: 100% / ($card-width-percentage / 1%)
|
||||
|
||||
+media-lg
|
||||
flex: 1 0 33%
|
||||
max-width: 33%
|
||||
$card-width-percentage: 30%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
margin-right: 100% / ($card-width-percentage / 1%)
|
||||
|
||||
+media-xl
|
||||
flex: 1 0 25%
|
||||
max-width: 25%
|
||||
$card-width-percentage: 22%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
margin-right: ($card-width-percentage * 2) / ($card-width-percentage / 1%)
|
||||
|
||||
+media-xxl
|
||||
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-width-percentage: 22%
|
||||
flex: 1 0 $card-width-percentage
|
||||
max-width: $card-width-percentage
|
||||
margin-right: ($card-width-percentage * 2) / ($card-width-percentage / 1%)
|
||||
|
||||
&.card-deck-vertical
|
||||
@extend .flex-column
|
||||
@ -43,6 +50,7 @@
|
||||
@extend .w-100
|
||||
@extend .flex-row
|
||||
@extend .p-0
|
||||
@extend .mb-2
|
||||
|
||||
flex: initial
|
||||
flex-wrap: wrap
|
||||
@ -109,12 +117,10 @@ $card-progress-height: 5px
|
||||
font-size: $font-size-xs
|
||||
|
||||
.card-img-top
|
||||
background-color: $color-background
|
||||
background-position: center
|
||||
background-size: cover
|
||||
object-fit: cover
|
||||
|
||||
|
||||
.progress
|
||||
height: $card-progress-height
|
||||
position: absolute
|
||||
@ -122,15 +128,30 @@ $card-progress-height: 5px
|
||||
width: 100%
|
||||
z-index: 1
|
||||
|
||||
.card-img-top
|
||||
&.card-icon
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
font-size: 2em
|
||||
.card-thumbnail
|
||||
@extend .embed-responsive
|
||||
background-color: rgba($dark, .2)
|
||||
border-top-left-radius: $card-inner-border-radius
|
||||
border-top-right-radius: $card-inner-border-radius
|
||||
color: $dark
|
||||
|
||||
i
|
||||
opacity: .2
|
||||
&:before
|
||||
padding-top: 56.25%
|
||||
|
||||
.card-img-top
|
||||
@extend .align-items-center
|
||||
@extend .d-flex
|
||||
@extend .h-100
|
||||
@extend .position-absolute
|
||||
bottom: 0
|
||||
left: 50% !important
|
||||
top: 0
|
||||
transform: translateX(-50%)
|
||||
width: auto !important
|
||||
|
||||
i
|
||||
font-size: 3em
|
||||
opacity: .2
|
||||
|
||||
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
|
||||
.card-label
|
||||
|
@ -21,7 +21,7 @@ ul.dropdown-menu
|
||||
|
||||
// When not in mobile, open menus on mouse hover .dropdown in the navbar.
|
||||
body:not(.is-mobile)
|
||||
nav .dropdown:hover
|
||||
nav .dropdown:not(.quick-search):hover
|
||||
ul.dropdown-menu
|
||||
display: block
|
||||
|
||||
|
@ -291,3 +291,18 @@ body.is-mobile
|
||||
|
||||
.navbar+.page-content
|
||||
padding-top: $nav-link-height
|
||||
|
||||
body.has-overlay
|
||||
nav.navbar
|
||||
background-color: $white !important
|
||||
box-shadow: none !important
|
||||
padding: 5px
|
||||
transition: padding-top $short-transition ease-in-out, padding-bottom $short-transition ease-in-out
|
||||
|
||||
.nav-secondary
|
||||
&:not(.keep-when-overlay)
|
||||
opacity: 0
|
||||
transition: opacity $short-transition, visibility $short-transition
|
||||
visibility: hidden
|
||||
display: none !important
|
||||
|
||||
|
@ -1,87 +1,67 @@
|
||||
#search-overlay
|
||||
position: absolute
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
pointer-events: none
|
||||
visibility: hidden
|
||||
opacity: 0
|
||||
z-index: $z-index-base + 4
|
||||
transition: opacity 150ms ease-in-out
|
||||
overflow-y: scroll
|
||||
z-index: $zindex-sticky + 1
|
||||
transition: visibility $long-transition, opacity $long-transition
|
||||
|
||||
&.active
|
||||
&.show
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
background-color: rgba($color-background-nav, .7)
|
||||
background-color: rgba($body-bg, .7)
|
||||
|
||||
.search-input
|
||||
+media-lg
|
||||
max-width: 350px
|
||||
+media-md
|
||||
max-width: 350px
|
||||
+media-sm
|
||||
max-width: 120px
|
||||
+media-xs
|
||||
display: block
|
||||
margin: 0 10px
|
||||
position: absolute
|
||||
z-index: $z-index-base
|
||||
right: 5px
|
||||
position: relative
|
||||
float: left
|
||||
padding: 0
|
||||
margin: 0
|
||||
.qs-result
|
||||
max-width: 80vw
|
||||
|
||||
.search-icon
|
||||
position: absolute
|
||||
top: 4px
|
||||
left: 10px
|
||||
cursor: pointer
|
||||
#qs-toggle
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
|
||||
&:after
|
||||
@extend .tooltip-inner
|
||||
.quick-search
|
||||
opacity: 0
|
||||
transition: opacity $short-transition
|
||||
visibility: hidden
|
||||
|
||||
content: 'Use advanced search...'
|
||||
font-size: .85em
|
||||
font-style: normal
|
||||
left: -10px
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
position: absolute
|
||||
top: 30px
|
||||
transition: top 150ms ease-in-out, opacity 150ms ease-in-out
|
||||
width: 150px
|
||||
.quick-search.show
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
|
||||
&:hover
|
||||
&:after
|
||||
opacity: 1
|
||||
top: 35px
|
||||
|
||||
|
||||
#cloud-search, .tt-hint
|
||||
+text-overflow-ellipsis
|
||||
border: thin solid $color-background
|
||||
border-radius: 3px
|
||||
font:
|
||||
size: 1em
|
||||
weight: 400
|
||||
margin: 0
|
||||
min-height: 32px
|
||||
outline: none
|
||||
padding: 0 20px 0 40px
|
||||
transition: border 100ms ease-in-out
|
||||
.qs-input
|
||||
input
|
||||
border-style: solid
|
||||
border-radius: $border-radius
|
||||
height: $input-height-inner
|
||||
padding: $input-padding-y 25px $input-padding-y $input-padding-x
|
||||
width: 30vw
|
||||
|
||||
&:focus
|
||||
box-shadow: none
|
||||
border: none
|
||||
&+i+select
|
||||
border-color: $color-primary
|
||||
|
||||
&::placeholder
|
||||
color: rgba($color-text, .5)
|
||||
transition: color 150ms ease-in-out
|
||||
&.multi-scope // has a select next to it
|
||||
border-right: none
|
||||
border-bottom-right-radius: 0
|
||||
border-top-right-radius: 0
|
||||
|
||||
&:hover
|
||||
&::placeholder
|
||||
color: rgba($color-text, .6)
|
||||
select
|
||||
border-width: 2px
|
||||
border-left: none
|
||||
border-bottom-right-radius: $border-radius
|
||||
border-top-right-radius: $border-radius
|
||||
height: $input-height-inner
|
||||
margin-left: 5px
|
||||
padding: $input-padding-y $input-padding-x
|
||||
|
||||
&:focus, option
|
||||
// background-color: $dark
|
||||
// color: $light
|
||||
|
||||
.qs-busy-symbol
|
||||
margin-left: -2em
|
||||
|
@ -40,6 +40,7 @@
|
||||
@import "components/checkbox"
|
||||
@import "components/overlay"
|
||||
@import "components/card"
|
||||
@import "components/search"
|
||||
|
||||
@import "comments"
|
||||
@import "notifications"
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
| {% set node_type = asset.properties.content_type if asset.properties.content_type else asset.node_type %}
|
||||
|
||||
a.card.asset.card-image-fade.pr-0.mx-0.mb-2(
|
||||
a.card.asset.card-image-fade.mb-2(
|
||||
class="js-item-open {% if asset.permissions.world and not current_user.has_cap('subscriber') %}free{% endif %}",
|
||||
data-node_id="{{ asset._id }}",
|
||||
title="{{ asset.name }}",
|
||||
href='{{ url_for_node(node=asset) }}')
|
||||
.embed-responsive.embed-responsive-16by9
|
||||
.card-thumbnail
|
||||
| {% if asset.picture %}
|
||||
.card-img-top.embed-responsive-item(style="background-image: url({{ asset.picture.thumbnail('m', api=api) }})")
|
||||
img.card-img-top(src="{{ asset.picture.thumbnail('m', api=api) }}", alt="{{ asset.name }}")
|
||||
| {% else %}
|
||||
.card-img-top.card-icon.embed-responsive-item
|
||||
.card-img-top.card-icon
|
||||
i(class="pi-{{ node_type }}")
|
||||
| {% endif %}
|
||||
|
||||
|
@ -23,7 +23,7 @@ include ../../../mixins/components
|
||||
data-placement="top")
|
||||
i.pi-list
|
||||
|
||||
+card-deck(id="asset_list_{{node._id}}",class="px-2")
|
||||
+card-deck(id="asset_list_{{node._id}}",class="pl-4")
|
||||
| {% for child in children %}
|
||||
| {{ asset_list_item(child, current_user) }}
|
||||
| {% endfor %}
|
||||
|
@ -17,6 +17,8 @@ include ../mixins/components
|
||||
| {% endif %}
|
||||
| {% endblock navigation_tabs %}
|
||||
|
||||
| {% block navigation_search %}{% endblock navigation_search %}
|
||||
|
||||
| {% block page_title %}Search{% if project %} {{ project.name }}{% endif %}{% endblock %}
|
||||
|
||||
| {% block head %}
|
||||
@ -82,7 +84,7 @@ script.
|
||||
|
||||
.border-left.search-list
|
||||
|
||||
+card-deck(0)(id='hits', class="m-0 px-2 card-deck-vertical")
|
||||
+card-deck()(id='hits', class="m-0 px-2 card-deck-vertical")
|
||||
|
||||
#search-details.border-left.search-details
|
||||
#search-error
|
||||
@ -94,7 +96,7 @@ script.
|
||||
script(type="text/template", id="facet-template")
|
||||
.card.border-0.p-0.m-2
|
||||
.card-body.p-3.m-0
|
||||
h6.text-muted {{ title }}
|
||||
h6.text-muted.facet-title {{ title }}
|
||||
| {{#values}}
|
||||
a.facet_link.toggleRefine(
|
||||
class='{{#refined}}refined{{/refined}}',
|
||||
@ -106,38 +108,6 @@ script(type="text/template", id="facet-template")
|
||||
small.text-black-50.float-right {{ count }}
|
||||
| {{/values}}
|
||||
|
||||
|
||||
// Hit template
|
||||
script(type="text/template", id="hit-template")
|
||||
a.card.asset.card-image-fade.pl-0.mx-0.mb-1(
|
||||
data-hit-id='{{ objectID }}',
|
||||
href="/nodes/{{ objectID }}/redir",
|
||||
class="js-search-hit {{#is_free}}free{{/is_free}}")
|
||||
.embed-responsive.embed-responsive-16by9
|
||||
| {{#picture}}
|
||||
.card-img-top.embed-responsive-item(style="background-image: url({{{ picture }}})")
|
||||
| {{/picture}}
|
||||
| {{^picture}}
|
||||
.card-img-top.card-icon.embed-responsive-item
|
||||
| {{#media}}
|
||||
i(class="pi-{{{ media }}}")
|
||||
| {{/media}}
|
||||
| {{^media}}
|
||||
i(class="pi-{{{ node_type }}}")
|
||||
| {{/media}}
|
||||
| {{/picture}}
|
||||
.card-body.py-2.d-flex.flex-column
|
||||
.card-title.mb-1.font-weight-bold
|
||||
| {{ name }}
|
||||
|
||||
ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto
|
||||
li.pr-2.project {{ project.name }}
|
||||
| {{#media}}
|
||||
li.pr-2.text-capitalize {{{ media }}}
|
||||
| {{/media}}
|
||||
li.pr-2 {{{ created_at }}}
|
||||
|
||||
|
||||
// Pagination template
|
||||
script(type="text/template", id="pagination-template")
|
||||
ul.search-pagination.
|
||||
@ -184,7 +154,7 @@ script.
|
||||
loadingBarHide();
|
||||
loadingBarShow();
|
||||
|
||||
displayNode($(this).data('hit-id'));
|
||||
displayNode($(this).data('node-id'));
|
||||
$('.js-search-hit').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
});
|
||||
|
@ -21,15 +21,6 @@ include ../mixins/components
|
||||
a(href="{{url_for('projects.view', project_url=project.url, _external=True)}}")
|
||||
i.pi-folder
|
||||
|
||||
| {% if not project.is_private %}
|
||||
li.tabs-search(
|
||||
title="Search",
|
||||
data-toggle="tooltip",
|
||||
data-placement="left")
|
||||
a(href="{{url_for('projects.search', project_url=project.url, _external=True)}}")
|
||||
i.pi-search
|
||||
| {% endif %}
|
||||
|
||||
| {% if project.has_method('PUT') %}
|
||||
li.active(
|
||||
title="Edit Project",
|
||||
|
@ -49,7 +49,7 @@ style.
|
||||
script(type="text/template", id="facet-template")
|
||||
.card.border-0.p-0.m-2
|
||||
.card-body.p-3.m-0
|
||||
h6.text-muted {{ title }}
|
||||
h6.text-muted.facet-title {{ title }}
|
||||
| {{#values}}
|
||||
a.facet_link.toggleRefine(
|
||||
class='{{#refined}}refined{{/refined}}',
|
||||
@ -61,16 +61,6 @@ script(type="text/template", id="facet-template")
|
||||
small.text-black-50.float-right {{ count }}
|
||||
| {{/values}}
|
||||
|
||||
|
||||
// Hit template
|
||||
script(type="text/template", id="hit-template")
|
||||
.search-hit.users.cursor-pointer.p-2.border-bottom(data-user-id='{{ objectID }}')
|
||||
h6.mb-0.font-weight-bold {{ full_name }}
|
||||
small {{ username }}
|
||||
small.d-block.search-hit-roles.text-info
|
||||
| {{ roles }}
|
||||
|
||||
|
||||
// Pagination template
|
||||
script(type="text/template", id="pagination-template")
|
||||
ul.search-pagination.
|
||||
|
335
tests/test_api/test_search_queries_nodes.py
Normal file
335
tests/test_api/test_search_queries_nodes.py
Normal file
@ -0,0 +1,335 @@
|
||||
import copy
|
||||
|
||||
from pillar.api.search.queries import create_node_search, create_multi_node_search
|
||||
from pillar.tests import AbstractPillarTest
|
||||
|
||||
EMPTY_QUERY = {
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"bool": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AGGREGATIONS = {
|
||||
"aggs": {
|
||||
"node_type": {
|
||||
"terms": {
|
||||
"field": "node_type"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"terms": {
|
||||
"field": "media"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"terms": {
|
||||
"field": "tags"
|
||||
}
|
||||
},
|
||||
"is_free": {
|
||||
"terms": {
|
||||
"field": "is_free"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SORTED_DESC_BY_CREATED = {
|
||||
"sort": [{
|
||||
"created_at": {"order": "desc"}
|
||||
}]
|
||||
}
|
||||
|
||||
PAGE_1 = {
|
||||
"from": 0,
|
||||
"size": 10
|
||||
}
|
||||
|
||||
IN_PROJECT = {
|
||||
"term": {
|
||||
"project.id": "MyProjectId"
|
||||
}
|
||||
}
|
||||
|
||||
NODE_INDEX = {
|
||||
"index": ["test_nodes"]
|
||||
}
|
||||
|
||||
|
||||
class TestSearchNodesGlobal(AbstractPillarTest):
|
||||
def test_empty_query(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='', terms={}, page=0)
|
||||
expected = {
|
||||
**EMPTY_QUERY,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**PAGE_1
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_empty_query_page_2(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='', terms={}, page=1)
|
||||
page_2 = copy.deepcopy(PAGE_1)
|
||||
page_2['from'] = 10
|
||||
|
||||
expected = {
|
||||
**EMPTY_QUERY,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**page_2
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_empty_query_with_terms(self):
|
||||
with self.app.app_context():
|
||||
terms = {
|
||||
'is_free': '0',
|
||||
'node_type': 'asset',
|
||||
}
|
||||
search = create_node_search(query='', terms=terms, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['filter'] = [
|
||||
{
|
||||
"term": {
|
||||
"is_free": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"node_type": "asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**PAGE_1
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_query(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='is there life on mars?', terms={}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match": {
|
||||
"name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"project.name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"user.name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"description": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"media": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"tags": "is there life on mars?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
|
||||
class TestSearchNodesInProject(AbstractPillarTest):
|
||||
def test_empty_query(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='', terms={}, page=0, project_id='MyProjectId')
|
||||
project_query = copy.deepcopy(EMPTY_QUERY)
|
||||
project_query['query']['bool']['filter'] = [IN_PROJECT]
|
||||
|
||||
expected = {
|
||||
**project_query,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**PAGE_1
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_empty_query_page_2(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='', terms={}, page=1, project_id='MyProjectId')
|
||||
project_query = copy.deepcopy(EMPTY_QUERY)
|
||||
project_query['query']['bool']['filter'] = [IN_PROJECT]
|
||||
page_2 = copy.deepcopy(PAGE_1)
|
||||
page_2['from'] = 10
|
||||
|
||||
expected = {
|
||||
**project_query,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**page_2
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_empty_query_with_terms(self):
|
||||
with self.app.app_context():
|
||||
terms = {
|
||||
'is_free': '1',
|
||||
'media': 'video',
|
||||
}
|
||||
search = create_node_search(query='', terms=terms, page=1, project_id='MyProjectId')
|
||||
project_query = copy.deepcopy(EMPTY_QUERY)
|
||||
project_query['query']['bool']['filter'] = [
|
||||
IN_PROJECT,
|
||||
{
|
||||
"term": {
|
||||
"is_free": True
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"media": "video"
|
||||
}
|
||||
}
|
||||
]
|
||||
page_2 = copy.deepcopy(PAGE_1)
|
||||
page_2['from'] = 10
|
||||
|
||||
expected = {
|
||||
**project_query,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**page_2
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_query(self):
|
||||
with self.app.app_context():
|
||||
search = create_node_search(query='is there life on mars?', terms={}, page=0, project_id='MyProjectId')
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['filter'] = [IN_PROJECT]
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match": {
|
||||
"name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"project.name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"user.name": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"description": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"media": "is there life on mars?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"tags": "is there life on mars?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
|
||||
class TestSearchMultiNodes(AbstractPillarTest):
|
||||
def test(self):
|
||||
with self.app.app_context():
|
||||
queries = [{
|
||||
'query': '',
|
||||
'terms': {},
|
||||
'page': 0,
|
||||
'project_id': ''
|
||||
}, {
|
||||
'query': '',
|
||||
'terms': {},
|
||||
'page': 1,
|
||||
'project_id': 'MyProjectId'
|
||||
}]
|
||||
search = create_multi_node_search(queries)
|
||||
|
||||
first = {
|
||||
**EMPTY_QUERY,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**PAGE_1
|
||||
}
|
||||
project_query = copy.deepcopy(EMPTY_QUERY)
|
||||
project_query['query']['bool']['filter'] = [IN_PROJECT]
|
||||
page_2 = copy.deepcopy(PAGE_1)
|
||||
page_2['from'] = 10
|
||||
|
||||
second = {
|
||||
** project_query,
|
||||
**AGGREGATIONS,
|
||||
**SORTED_DESC_BY_CREATED,
|
||||
**page_2,
|
||||
}
|
||||
|
||||
expected = [
|
||||
NODE_INDEX,
|
||||
first,
|
||||
NODE_INDEX,
|
||||
second
|
||||
]
|
||||
|
||||
self.assertEquals(expected, search.to_dict())
|
285
tests/test_api/test_search_queries_users.py
Normal file
285
tests/test_api/test_search_queries_users.py
Normal file
@ -0,0 +1,285 @@
|
||||
import copy
|
||||
|
||||
from pillar.api.search import queries
|
||||
from pillar.tests import AbstractPillarTest
|
||||
|
||||
SOURCE = {
|
||||
"_source": {
|
||||
"include": ["full_name", "objectID", "username"]
|
||||
}
|
||||
}
|
||||
|
||||
EMPTY_QUERY = {
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{
|
||||
"bool": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AGGREGATIONS = {
|
||||
"aggs": {
|
||||
"roles": {
|
||||
"terms": {
|
||||
"field": "roles"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PAGE_1 = {
|
||||
"from": 0,
|
||||
"size": 10
|
||||
}
|
||||
|
||||
|
||||
class TestSearchUsers(AbstractPillarTest):
|
||||
def test_empty_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_search(query='', terms={}, page=0)
|
||||
|
||||
expected = {
|
||||
**SOURCE,
|
||||
**EMPTY_QUERY,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_search(query='Jens', terms={}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match": {
|
||||
"username": "Jens"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"full_name": "Jens"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"email": {
|
||||
"query": "Jens",
|
||||
"boost": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"username_exact": {
|
||||
"value": "Jens",
|
||||
"boost": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**SOURCE,
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_email_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_search(query='mail@mail.com', terms={}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"term": {
|
||||
"email_exact": {
|
||||
"value": "mail@mail.com",
|
||||
"boost": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"username": "mail@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"full_name": "mail@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"email": {
|
||||
"query": "mail@mail.com",
|
||||
"boost": 25
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"username_exact": {
|
||||
"value": "mail@mail.com",
|
||||
"boost": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**SOURCE,
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
|
||||
class TestSearchUsersAdmin(AbstractPillarTest):
|
||||
def test_empty_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_admin_search(query='', terms={}, page=0)
|
||||
|
||||
expected = {
|
||||
**EMPTY_QUERY,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_admin_search(query='Jens', terms={}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match": {
|
||||
"username": "Jens"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"full_name": "Jens"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"email": {
|
||||
"query": "Jens",
|
||||
"boost": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"username_exact": {
|
||||
"value": "Jens",
|
||||
"boost": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_terms(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_admin_search(query='', terms={'roles': 'Admin'}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['filter'] = [
|
||||
{
|
||||
"term": {
|
||||
"roles": "Admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
||||
|
||||
def test_object_id_query(self):
|
||||
with self.app.app_context():
|
||||
search = queries.create_user_admin_search(query='563aca02c379cf0005e8e17d', terms={}, page=0)
|
||||
query = copy.deepcopy(EMPTY_QUERY)
|
||||
query['query']['bool']['must'] = [
|
||||
{
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match": {
|
||||
"username": "563aca02c379cf0005e8e17d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"full_name": "563aca02c379cf0005e8e17d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"email": {
|
||||
"query": "563aca02c379cf0005e8e17d",
|
||||
"boost": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"username_exact": {
|
||||
"value": "563aca02c379cf0005e8e17d",
|
||||
"boost": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"term": {
|
||||
"objectID": {
|
||||
"value": "563aca02c379cf0005e8e17d",
|
||||
"boost": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expected = {
|
||||
**query,
|
||||
**AGGREGATIONS,
|
||||
**PAGE_1
|
||||
}
|
||||
self.assertEquals(expected, search.to_dict())
|
Loading…
x
Reference in New Issue
Block a user