Skip to content

Commit

Permalink
Adds search (for #438) (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
jace authored Jun 17, 2019
1 parent 4e65ce6 commit 6ae7596
Show file tree
Hide file tree
Showing 33 changed files with 1,701 additions and 283 deletions.
11 changes: 8 additions & 3 deletions funnel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@

baseframe.init_app(app, requires=['funnel'], ext_requires=[
'pygments', 'toastr', 'baseframe-mui'], theme='mui')
baseframe.init_app(funnelapp, requires=['funnel'], ext_requires=[
'pygments', 'toastr', 'baseframe-mui'], theme='mui')

# Register JS and CSS assets on both apps
app.assets.register('js_fullcalendar',
Bundle(assets.require('!jquery.js', 'jquery.fullcalendar.js', 'spectrum.js', 'jquery.ui.sortable.touch.js'),
output='js/fullcalendar.packed.js', filters='uglipyjs'))
Expand Down Expand Up @@ -110,9 +114,6 @@
Bundle(assets.require('!jquery.js', 'jquery.ui.js', 'jquery.ui.sortable.touch.js'),
output='js/sortable.packed.js', filters='uglipyjs'))


baseframe.init_app(funnelapp, requires=['funnel'], ext_requires=[
'pygments', 'toastr', 'baseframe-mui'], theme='mui')
funnelapp.assets.register('js_fullcalendar',
Bundle(assets.require('!jquery.js', 'jquery.fullcalendar.js', 'spectrum.js', 'jquery.ui.sortable.touch.js'),
output='js/fullcalendar.packed.js', filters='uglipyjs'))
Expand Down Expand Up @@ -159,3 +160,7 @@
view_func=funnelapp.send_static_file, subdomain=None)
funnelapp.add_url_rule('/static/<path:filename>', endpoint='static',
view_func=funnelapp.send_static_file, subdomain='<subdomain>')

# Database model loading (from Funnel or extensions) is complete.
# Configure database mappers now, before the process is forked for workers.
db.configure_mappers()
18 changes: 17 additions & 1 deletion funnel/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ $(() => {
LazyloadImg.init('js-lazyload-img');
};

if(document.querySelector('#page-navbar') || document.querySelector('.js-lazyload-img')) {
if(document.querySelector('#page-navbar') || document.querySelector('.js-lazyload-img') ||
document.querySelector('.js-lazyload-results')) {
if (!('IntersectionObserver' in global &&
'IntersectionObserverEntry' in global &&
'intersectionRatio' in IntersectionObserverEntry.prototype)) {
Expand Down Expand Up @@ -46,4 +47,19 @@ $(() => {
$(projectElemClass).removeClass('mui--hide');
$(this).addClass('mui--hide');
});

$('.js-search-show').on('click', function toggleSearchForm(event) {
event.preventDefault();
$('.js-search-form').toggleClass('search-form--show');
$('.js-search-field').focus();
});

// Clicking outside close search form if open
$('body').on('click', function closeSearchForm(event) {
if($('.js-search-form').hasClass('search-form--show') &&
!$(event.target).is('.js-search-field') &&
!$.contains($('.js-search-show').parent('.header__nav-list__item')[0], event.target)) {
$('.js-search-form').removeClass('search-form--show');
}
});
});
148 changes: 148 additions & 0 deletions funnel/assets/js/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import Ractive from "ractive";

const Search = {
init(config) {
Ractive.DEBUG = false;
let widget = new Ractive({
el: '#search-wrapper',
template: '#search-template',
data: {
tabs: config.counts,
results: '',
activeTab: '',
pagePath: window.location.pathname,
queryString: '',
defaultImage: config.defaultImage,
formatTime: function (date) {
let d = new Date(date);
return d.toLocaleTimeString('default', { hour: 'numeric', minute: 'numeric' });
},
formatDate: function (date) {
let d = new Date(date);
return d.toLocaleDateString('default', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
},
},
getQueryString(paramName) {
let searchStr = window.location.search.substring(1).split('&');
let queryString = searchStr.map((param) => {
let paramSplit = param.split('=');
if (paramSplit[0] === paramName) {
return paramSplit[1];
} else {
return false;
}
}).filter(val => val && val !== "");
return queryString[0];
},
updateTabContent(event, searchType) {
event.original.preventDefault();
if(this.get('results.' + searchType)) {
let url = `${this.get('pagePath')}?q=${this.get('queryString')}&type=${searchType}`;
this.activateTab(searchType, '', url);
} else {
this.fetchResult(searchType);
}
},
fetchResult(searchType, page=1) {
let url = `${this.get('pagePath')}?q=${this.get('queryString')}&type=${searchType}`
$.ajax({
type: 'GET',
url: `${url}&page=${page}`,
timeout: 5000,
dataType: 'json',
success: function(data) {
widget.activateTab(searchType, data.results, url, page);
}
});
},
activateTab(searchType, result='', url='', page) {
if(result) {
if(page > 1) {
let existingResults = this.get('results.' + searchType);
let searchResults = [];
searchResults.push(...existingResults.items);
searchResults.push(...result.items);
result.items = searchResults
this.set('results.' + searchType, result);
} else {
this.set('results.' + searchType, result);
}
}
this.set('activeTab', searchType);
if (url) {
this.handleBrowserHistory(url);
}
this.updateMetaTags(searchType, url);
this.lazyoad();
},
handleBrowserHistory(url='') {
window.history.replaceState('', '', url);
},
updateMetaTags: function(searchType, url='') {
let q = this.get('queryString');
let { count } = this.get('results.' + searchType);
let title = `Search results: ${q}`;
let description = `${count} results found for "${q}"`;

$('title').html(title);
$('meta[name=DC\\.title]').attr('content', title);
$('meta[property=og\\:title]').attr('content', title);
$('meta[name=description]').attr('content', description);
$('meta[property=og\\:description]').attr('content', description);
if(url) {
$('link[rel=canonical]').attr('href', url);
$('meta[property=og\\:url]').attr('content', url);
}
},
lazyoad: function() {
let lazyLoader = document.querySelector('.js-lazy-loader');
if(lazyLoader) {
this.handleObserver = this.handleObserver.bind(this);

let observer = new IntersectionObserver(
this.handleObserver,
{
rootMargin: '0px',
threshold: 0.5
},
);
observer.observe(lazyLoader);
}
},
handleObserver(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
let nextPage = entry.target.getAttribute('data-next-page');
if(nextPage) {
this.fetchResult(this.get('activeTab'), nextPage);
}
}
return;
});
},
initTab() {
let queryString = this.getQueryString('q');
this.set('queryString', queryString);
// Fill the search box with queryString
document.querySelector('.js-search-field').value = queryString;

let searchType = this.getQueryString('type');
if(searchType && config.results) {
this.activateTab(searchType, config.results);
} else {
searchType = this.get('tabs')[0]['type'];
this.fetchResult(searchType);
}
},
onrender() {
this.initTab();
}
});
}
};

$(() => {
window.HasGeek.Search = function (config) {
Search.init(config);
}
});
1 change: 1 addition & 0 deletions funnel/assets/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {
"scan_badge": path.resolve(__dirname, "js/scan_badge.js"),
"scan_contact": path.resolve(__dirname, "js/scan_contact.js"),
"contact": path.resolve(__dirname, "js/contact.js"),
"search": path.resolve(__dirname, "js/search.js"),
},
output: {
path: path.resolve(__dirname, "../static/build"),
Expand Down
3 changes: 3 additions & 0 deletions funnel/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
# flake8: noqa

from sqlalchemy_utils import TSVectorType
from coaster.sqlalchemy import (TimestampMixin, UuidMixin, BaseMixin, BaseNameMixin,
BaseScopedNameMixin, BaseScopedIdNameMixin, BaseIdNameMixin, MarkdownColumn,
JsonDict, NoIdMixin, CoordinatesMixin, UrlType)
from coaster.db import db


TimestampMixin.__with_timezone__ = True


from .commentvote import *
from .contact_exchange import *
from .draft import *
Expand Down
47 changes: 44 additions & 3 deletions funnel/models/commentvote.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-

from . import db, BaseMixin, MarkdownColumn, UuidMixin
from .user import User
from coaster.utils import LabeledEnum
from coaster.sqlalchemy import cached, StateManager
from baseframe import __
from baseframe import _, __

from . import db, BaseMixin, MarkdownColumn, UuidMixin, TSVectorType
from .user import User
from .helpers import add_search_trigger


__all__ = ['Voteset', 'Vote', 'Commentset', 'Comment']

Expand Down Expand Up @@ -85,6 +88,7 @@ def __init__(self, **kwargs):

class Comment(UuidMixin, BaseMixin, db.Model):
__tablename__ = 'comment'

user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True)
user = db.relationship(User, primaryjoin=user_id == User.id,
backref=db.backref('comments', lazy='dynamic', cascade="all, delete-orphan"))
Expand All @@ -106,10 +110,44 @@ class Comment(UuidMixin, BaseMixin, db.Model):

edited_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)

__roles__ = {
'all': {
'read': {'absolute_url', 'created_at', 'edited_at', 'user', 'title', 'message'}
}
}

search_vector = db.deferred(db.Column(
TSVectorType(
'message_text',
weights={'message_text': 'A'},
regconfig='english',
hltext=lambda: Comment.message_html,
),
nullable=False))

__table_args__ = (
db.Index('ix_comment_search_vector', 'search_vector', postgresql_using='gin'),
)

def __init__(self, **kwargs):
super(Comment, self).__init__(**kwargs)
self.voteset = Voteset(type=SET_TYPE.COMMENT)

@property
def absolute_url(self):
if self.commentset.proposal:
return self.commentset.proposal.absolute_url + '#c' + self.suuid

@property
def title(self):
obj = self.commentset.proposal
if obj:
return _("{user} commented on {obj}").format(
user=self.user.pickername,
obj=self.commentset.proposal.title)
else:
return _("{user} commented").format(user=self.user.pickername)

@state.transition(None, state.DELETED)
def delete(self):
"""
Expand Down Expand Up @@ -142,3 +180,6 @@ def permissions(self, user, inherited=None):
'delete_comment'
])
return perms


add_search_trigger(Comment, 'search_vector')
66 changes: 0 additions & 66 deletions funnel/models/helper.py

This file was deleted.

Loading

0 comments on commit 6ae7596

Please sign in to comment.