#466 Implement a Basic search of hubs
Merged 6 years ago by abompard. Opened 6 years ago by ryanlerch.
ryanlerch/fedora-hubs search  into  develop

file modified
+65
@@ -412,3 +412,68 @@ 

  .text-fedora-green-dark{color: #488b18;}

  .text-fedora-purple-dark{color: #70488f;}

  

+ .typeahead, .twitter-typeahead {

+   width: 100%!important;

+ }

+ 

+ #bloodhound .twitter-typeahead:after{

+   font-family: FontAwesome;

+   content: "\f002";

+   position: absolute;

+   z-index: 100;

+   right: 25px;

+   top: 7px;

+   color: #bbb;

+   width: 0;

+ }

+ 

+ .tt-hint {

+   color: #999

+ }

+ 

+ .tt-menu {

+   width: 100%;

+   margin-top: 0px;

+ 

+   /* Make it scrollable */

+   max-height: 550px;

+   overflow-y: auto;

+ 

+   background-color: #fff;

+   border: 1px solid #ccc;

+   border: 1px solid rgba(0, 0, 0, 0.2);

+   -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);

+      -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);

+           box-shadow: 0 5px 10px rgba(0,0,0,.2);

+ }

+ 

+ .tt-suggestion {

+   padding-left: 10px;

+   padding-right: 10px;

+   font-size: 14px;

+   line-height: 18px;

+   margin:0;

+ }

+ 

+ .tt-suggestion.tt-cursor, .tt-suggestion:hover {

+   color: #fff;

+   background-color: #0097cf;

+   cursor: pointer;

+ }

+ 

+ .tt-suggestion.tt-cursor a, .tt-suggestion:hover a{

+   color:#fff;

+ }

+ 

+ .tt-suggestion.tt-cursor span.text-muted, .tt-suggestion:hover span.text-muted{

+   color: rgba(255,255,255,0.6);

+ }

+ 

+ .tt-suggestion a{

+   color:black;

+   display:block;

+ }

+ 

+ .tt-suggestion p {

+   margin: 0;

+ }

@@ -0,0 +1,64 @@ 

+ $(document).ready(function() {

+     var hubs = new Bloodhound({

+       datumTokenizer: function (datum) {

+           return Bloodhound.tokenizers.whitespace(datum.value);

+       },

+         queryTokenizer: Bloodhound.tokenizers.whitespace,

+         remote: {

+             wildcard: '%QUERY',

+             url: window.searchAPIurl + '%QUERY',

+             transform: function(response) { return response.data; },

+         }

+     });

+ 

+     hubs.initialize();

+ 

+     $('#bloodhound .typeahead').typeahead({

+         hint: true,

+         highlight: true,

+         minLength: 2,

+     },

+     {

+         name: 'hubs',

+         displayKey: 'name',

+         source: hubs.ttAdapter(),

+         templates: {

+             pending: [

+                 '<div class="py-1 text-center">',

+                 '<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>',

+                 '</div>'

+             ].join('\n'),

+             empty: [

+                 '<div class="empty-message text-muted py-1 text-center">',

+                 'no matching hubs',

+                 '</div>'

+             ].join('\n'),

+             suggestion: function(datum) {

+                 output = '<div class="py-1 media">'

+                 if (datum.user_hub){

+                     output = output+'<img width="64px" height="64px" src="'+datum.config.avatar+'"/>'

+                 } else {

+                     output = output+'<div class="monogram-avatar bg-fedora-'+datum.monogram_colour+' text-fedora-'+datum.monogram_colour+'-dark">'+datum.name.charAt(0).toUpperCase()+'</div>'                   

+                 }

+                 output = output +'<div class="media-body pl-3">'

+                 output = output +'<div><strong>'+datum.name+'</strong></div>'

+                 output = output +'<div>'+datum.config.summary+'</div>'

+                 output = output +'<div>'

+                 if (datum.user_hub){

+                     output = output +'<span class="badge badge-primary">User</span>'

+                 } else {

+                     output = output +'<span class="badge badge-info">Group</span>'                    

+                 }

+                 output = output +'</div>'

+                 output = output +'</div>'

+                 output = output +'</div>'

+                 return output;

+             },

+         },

+     },

+     );

+ 

+     $('#bloodhound input.typeahead').on('typeahead:selected', function (e, datum) {

+         window.location.href = datum.hub_url;

+     });

+ }); 

\ No newline at end of file

The added file is too large to be shown here, see it at: hubs/static/js/typeahead.bundle.js
file modified
+13 -13
@@ -18,7 +18,7 @@ 

  

  <nav class="navbar navbar-expand-md navbar-light" id="navbar-top">

  

-   <a class="navbar-brand" href="{{ url_for('index') }}">

+   <a class="navbar-brand col-md-4" href="{{ url_for('index') }}">

      <img src="{{ url_for('static', filename='img/logo-hubs.png') }}" alt="Hubs">

    </a>

  
@@ -26,19 +26,14 @@ 

      <span class="navbar-toggler-icon"></span>

    </button>

  

-   <div class="collapse navbar-collapse" id="navbar-top-menu">

-     <!-- NOT HOOKED UP TO ANYTHING XOXO -->

-     <form class="form-inline mx-auto">

-       <div class="input-group">

-         <input class="form-control" type="text" placeholder="Search across all hubs...">

-         <div class="input-group-btn">

-           <button class="btn btn-secondary" type="submit">

-             <span><i class="fa fa-search" aria-hidden="true"></i></span>

-           </button>

-         </div>

+   <div class="col-md-4">

+     <div class="form mx-auto" autocomplete="off">

+         <form id="bloodhound" class="input-group">

+           <input class="typeahead form-control" name="term" type="text" placeholder="Search hubs..." autocomplete="off"/>        

+         </form>

        </div>

-     </form>

- 

+   </div>

+   <div class="collapse navbar-collapse" id="navbar-top-menu">

      {% if g.auth.logged_in %}

      <ul class="navbar-nav ml-auto">

        <li class="nav-item mr-4">
@@ -170,6 +165,11 @@ 

  </script>

  <script src="{{ url_for('static', filename='js/build/common.js') }}"></script>

  <script src="{{ url_for('static', filename='js/build/main.js') }}"></script>

+ <script type="text/javascript"  src="{{ url_for('static', filename='js/typeahead.bundle.js') }}"></script>

+ <script>

+     window.searchAPIurl = {{ url_for('api_query_hubs', searchterm="")| tojson }}

+ </script>

+ <script type="text/javascript"  src="{{ url_for('static', filename='js/search.js') }}"></script> 

  {% endblock %}

  

  </body>

file modified
+7 -1
@@ -1,7 +1,7 @@ 

  from __future__ import unicode_literals

  

  import os

- from hashlib import sha256

+ from hashlib import sha256, md5

  

  import fedmsg.config

  import fedmsg.meta
@@ -31,3 +31,9 @@ 

      hash = sha256(openid.encode('utf-8')).hexdigest()

      avatar = "https://seccdn.libravatar.org/avatar/%s?%s" % (hash, query)

      return avatar

+ 

+ 

+ def hubname2monogramcolour(hubname):

+     colours = ["blue", "green", "magenta", "orange", "purple"]

+     colour_index = int(md5(hubname.encode('utf8')).hexdigest(), 16) % 5

+     return colours[colour_index]

file modified
+11 -1
@@ -10,10 +10,11 @@ 

  

  import flask

  from six.moves.urllib import parse as urlparse

+ from sqlalchemy import or_

  from sqlalchemy.orm import joinedload

  from sqlalchemy.orm.exc import NoResultFound

  

- from hubs.models import Hub, Widget

+ from hubs.models import Hub, HubConfig, Widget

  

  

  log = logging.getLogger(__name__)
@@ -32,6 +33,15 @@ 

          flask.abort(404)

  

  

+ def query_hubs(querystring, session=None):

+     if session is None:

+         session = flask.g.db

+     query = session.query(Hub).join(HubConfig)

+     query = query.filter(or_(HubConfig.summary.ilike('%%%s%%' % querystring),

+                              Hub.name.ilike('%%%s%%' % querystring)))

+     return query.all()

+ 

+ 

  def get_widget_instance(hub, idx, session=None):

      """ Utility shorthand to get a widget and 404 if not found. """

      if session is None:

file modified
+16 -1
@@ -3,7 +3,22 @@ 

  import flask

  

  from hubs.app import app

- from hubs.utils.views import get_hub, get_user_permissions, require_hub_access

+ from hubs.utils import hubname2monogramcolour

+ from hubs.utils.views import (get_hub, get_user_permissions,

+                               require_hub_access, query_hubs)

+ 

+ 

+ @app.route('/api/hubs/query/<searchterm>', methods=['GET'])

+ def api_query_hubs(searchterm):

+     data = []

+     for hub in query_hubs(searchterm):

+         props = hub.get_props()

+         if not hub.user_hub:

+             props["monogram_colour"] = hubname2monogramcolour(hub.name)

+         props["hub_url"] = flask.url_for('hub', name=hub.name)

+         data.append(props)

+     result = {"status": "OK", "data": data}

+     return flask.jsonify(result)

  

  

  @app.route('/api/hubs/<name>/', methods=['GET'])

@@ -1,9 +1,7 @@ 

  from __future__ import unicode_literals

  

- import hashlib

- 

  import hubs.models

- from hubs.utils import get_fedmsg_config

+ from hubs.utils import get_fedmsg_config, hubname2monogramcolour

  from hubs.widgets.base import Widget

  from hubs.widgets.view import RootWidgetView

  
@@ -57,9 +55,7 @@ 

  

  

  def generate_monogram(hubname):

-     colours = ["blue", "green", "magenta", "orange", "purple"]

-     colour_index = int(hashlib.md5(hubname.encode('utf8')).hexdigest(), 16) % 5

      return ("<div class='monogram-avatar bg-fedora-%s text-fedora-%s-dark'>"

-             "%s</div>") % (colours[colour_index],

-                            colours[colour_index],

+             "%s</div>") % (hubname2monogramcolour(hubname),

+                            hubname2monogramcolour(hubname),

                             hubname[0].upper())

This implements a basic search of hubs, and puts the search
in the search box at the top of the page

Fixes #461

Signed-off-by: Ryan Lerch rlerch@redhat.com

Hmm, I'd love it if we could get that URL from the backend, so we don't have to change it in both places if we need to (for example if Hubs is not installed at the domain root).
Could you define a function that would take the url as the argument, and call that function from a script tag at the bottom of the HTML template, passing it something like {{ url_for('api_search')|tojson }} ? This pattern has already been used elsewhere in the code (for example to setup React).

There! A missing space! ;-)

This would also be a good candidate for a URL passed as a template. Or even better, return the Hub URL in the datum so you can just use it.

Did you just add a space a the end of the line here? Typo maybe?

Also add the hub's URL here so you can use it to redirect on selection.

rebased onto 93ca4543cedfd3e1259d630f534699da375fdff5

6 years ago

Okies! fixed the typos, and got the URLs from the url_for flask calls!

It looks great, thanks! I think it would be pretty cool if we could search in the hub's summary too, don't you think? Could you add that to the backend view?

rebased onto 434dafffe7084d1ddd54e57bbdfbed65fba24012

6 years ago

@abompard done! now searching on the summary as well as the hubname.

I'm thinking of something else: in the API endpoints we have as a convention to return {"status": "OK", "data": the_data} as JSON. I think it would be best to make the api_query_hubs endpoint adhere to that (and thus do some minor changes in the javascript to accomodate for the change).

Sorry for being so nitpicky! :-)

I'm thinking of something else: in the API endpoints we have as a convention to return {"status": "OK", "data": the_data} as JSON. I think it would be best to make the api_query_hubs endpoint adhere to that (and thus do some minor changes in the javascript to accomodate for the change).
Sorry for being so nitpicky! :-)

Don't be sorry! it's good to get this right!

updated the commit with the status and data elements, and changed the search.js to pull from the new elements!

rebased onto 30323f0

6 years ago

Pull-Request has been merged by abompard

6 years ago