소스 검색

Initial working term search UI with AJAX response.

Stefano Cossu 6 년 전
부모
커밋
85b4b88b19

+ 79 - 5
lakesuperior/api/query.py

@@ -2,6 +2,8 @@ import logging
 
 from io import BytesIO
 
+from rdflib import URIRef
+
 from lakesuperior import env
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
@@ -12,8 +14,16 @@ logger = logging.getLogger(__name__)
 rdfly = env.app_globals.rdfly
 rdf_store = env.app_globals.rdf_store
 
+operands = ('_id', '=', '!=', '<', '>', '<=', '>=')
+"""
+Available term comparators for term query.
+
+The ``_uri`` term is used to match URIRef terms, all other comparators are
+used against literals.
+"""
 
-def term_query(s=None, p=None, o=None):
+
+def triple_match(s=None, p=None, o=None, return_full=False):
     """
     Query store by matching triple patterns.
 
@@ -25,19 +35,83 @@ def term_query(s=None, p=None, o=None):
     :param rdflib.term.Identifier s: Subject term.
     :param rdflib.term.Identifier p: Predicate term.
     :param rdflib.term.Identifier o: Object term.
+    :param bool return_full: if ``False`` (the default), the returned values
+        in the set are the URIs of the resources found. If True, the full set
+        of matching triples is returned.
+
+    :rtype: set(tuple(rdflib.term.Identifier){3}) or set(rdflib.URIRef)
+    :return: Matching resource URIs if ``return_full`` is false, or
+        matching triples otherwise.
     """
     with TxnManager(rdf_store) as txn:
+        matches = rdf_store.triples((s, p, o), None)
         # Strip contexts and de-duplicate.
-        qres = {match[0] for match in rdf_store.triples((s, p, o), None)}
+        qres = (
+            {match[0] for match in matches} if return_full
+            else {match[0][0] for match in matches})
 
     return qres
 
 
-def lookup_literal(pattern):
+def term_query(terms, or_logic=False):
+    """
+    Query resources by predicates, comparators and values.
+
+    Comparators can be against literal or URIRef objects. For a list of
+    comparators and their meanings, see the documentation and source for
+    :py:data:`~lakesuperior.api.query.operands`.
+
+    :param list(tuple{3}) terms: List of 3-tuples containing:
+
+        - Predicate URI (rdflib.URIRef)
+        - Comparator value (str)
+        - Value to compare to (rdflib.URIRef or rdflib.Literal or str)
+
+    :param bool or_logic: Whether to concatenate multiple query terms with OR
+        logic (uses SPARQL ``UNION`` statements). The default is False (i.e.
+        terms are concatenated as standard SPARQL statements).
+    """
+    qry_term_ls = []
+    for i, term in enumerate(terms):
+        pred_uri, comp, val = term
+        if comp not in operands:
+            raise ValueError('Not a valid operand: {}'.format(comp))
+
+        oname = '?o_{}'.format(i)
+        if comp == '_id':
+            qry_term = '?s {} {} .'.format(pred_uri.n3(), val)
+        else:
+            if type(val) == str:
+                filter_stmt = 'str({}) {} "{}"'.format(oname, comp, val)
+            else:
+                filter_stmt = '{} {} {}'.format(oname, comp, val.n3())
+            qry_term = '?s {} {}\nFILTER ({}) .'.format(
+                    oname, pred_uri.n3(), filter_stmt)
+
+        qry_term_ls.append(qry_term)
+
+    if or_logic:
+        qry_terms = 'UNION {\n' + '\n} UNION {\n'.join(qry_term_ls) + '\n}'
+    else:
+        qry_terms = '\n'.join(qry_term_ls)
+    qry_str = '''
+    SELECT ?s WHERE {{
+      {}
+    }}
+    '''.format(qry_terms)
+    logger.debug('Query: {}'.format(qry_str))
+
+    with TxnManager(rdf_store) as txn:
+        qres = rdfly.raw_query(qry_str)
+        return {row[0] for row in qres}
+
+
+def fulltext_lookup(pattern):
     """
-    Look up one literal term by partial match.
+    Look up one term by partial match.
 
-    *TODO: reserved for future use. A Whoosh or similar full-text index is
+    *TODO: reserved for future use. A `Whoosh
+    <https://whoosh.readthedocs.io/>`__ or similar full-text index is
     necessary for this.*
     """
     pass

+ 42 - 12
lakesuperior/endpoints/query.py

@@ -1,11 +1,14 @@
 import logging
 
 from flask import Blueprint, current_app, request, render_template, send_file
+from rdflib import URIRef
 from rdflib.plugin import PluginException
 
 from lakesuperior import env
-from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
 from lakesuperior.api import query as query_api
+from lakesuperior.dictionaries.namespaces import ns_collection as nsc
+from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
+from lakesuperior.toolbox import Toolbox
 
 # Query endpoint. raw SPARQL queries exposing the underlying layout can be made
 # available. Also convenience methods that allow simple lookups based on simple
@@ -18,24 +21,51 @@ rdfly = env.app_globals.rdfly
 query = Blueprint('query', __name__)
 
 
-@query.route('/term_search', methods=['GET'])
+@query.route('/term_search', methods=['GET', 'POST'])
 def term_search():
     """
     Search by entering a search term and optional property and comparison term.
     """
-    valid_operands = (
-        ('=', 'Equals'),
-        ('>', 'Greater Than'),
-        ('<', 'Less Than'),
-        ('<>', 'Not Equal'),
-        ('a', 'RDF Type'),
+    operands = (
+        ('_id', 'Has Type'),
+        ('_id', 'Matches Term'),
+        ('=', 'Is Equal To'),
+        ('!=', 'Is Not Equal To'),
+        ('<', 'Is Less Than'),
+        ('>', 'Is Greater Than'),
+        ('<=', 'Is Less Than Or Equal To'),
+        ('>=', 'Is Greater Than Or Equal To'),
     )
+    qres = term_list = []
 
-    term = request.args.get('term')
-    prop = request.args.get('prop', default=1)
-    cmp = request.args.get('cmp', default='=')
+    if request.method == 'POST':
+        # Some magic needed to associate pseudo-array field notation with
+        # an actual dict. Flask does not fully support this syntax as Rails
+        # or other frameworks do: https://stackoverflow.com/q/24808660
+        fnames = ('pred_ns', 'pred', 'op', 'val')
+        term_list = [
+                request.form.getlist('{}[]'.format(tn))
+                for tn in fnames]
+        # Transpose matrix.
+        txm = list(zip(*term_list))
+        logger.info('transposed matrix: {}'.format(txm))
+        terms = []
+        for row in txm:
+            fmt_row = list(row)
+            ns = fmt_row.pop(0)
+            fmt_row[0] = nsc[ns][fmt_row[0]] if ns else URIRef(fmt_row[0])
+            terms.append(fmt_row)
+        logger.info('Terms: {}'.format(terms))
 
-    return render_template('term_search.html')
+        or_logic = request.form.get('logic') == 'or'
+        qres = query_api.term_query(terms, or_logic)
+
+        def gl(uri):
+            return uri.replace(nsc['fcres'], '/ldp')
+        return render_template('term_search_results.html', qres=qres, gl=gl)
+    else:
+        return render_template(
+            'term_search.html', operands=operands, qres=qres, nsm=nsm)
 
 
 @query.route('/sparql', methods=['GET', 'POST'])

+ 1 - 0
lakesuperior/endpoints/templates/base.html

@@ -58,5 +58,6 @@
         <script src="{{url_for('ldp.static', filename='assets/js/jquery-3.2.1.min.js')}}"></script>
         <!-- Include all compiled plugins (below), or include individual files as needed -->
         <script src="{{url_for('ldp.static', filename='assets/js/bootstrap.min.js')}}"></script>
+        {% block tail_js %}{% endblock %}
     </body>
 </html>

+ 1 - 27
lakesuperior/endpoints/templates/resource.html

@@ -130,31 +130,5 @@
     {% endfor %}
     </tbody>
 </table>
-{% block namespaces %}
-            <h2>Namespaces</h2>
-             <button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#nslist" aria-expanded="false" aria-controls="nsList">
-                Expand/Collapse
-            </button>
-            <div class="collapse" id="nslist">
-                <div class="card card-body">
-                    <table class="table table-striped">
-                        <thead>
-                            <tr>
-                                <td>Prefix</td>
-                                <td>URI</td>
-                            </tr>
-                        </thead>
-                        <tbody>
-                        {% for ns in nsm.namespaces() | sort %}
-                            <tr>
-                                <td>{{ ns[0] }}</td>
-                                <td>{{ ns[1] }}</td>
-                            </tr>
-                        {% endfor %}
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-
-{% endblock %}
+{% include 'namespaces.html' %}
 {% endblock %}

+ 86 - 1
lakesuperior/endpoints/templates/term_search.html

@@ -1,6 +1,91 @@
 {% extends 'base.html' %}
 {% block title %}Term Search{% endblock %}
 {% block content %}
-    <p>Some bright day this page will be populated with a fantastic term search.</p>
+    <p>Enter terms to query.</p>
+    <form id="term-search-form" class="form-inline" method="POST" action="">
+        <div class="row">
+            <label for="logic">Logic</label>
+            <select class="form-control" name="logic">
+                <option value="and" selected>AND</option>
+                <option value="or">OR</option>
+            </select>
+        </div>
+        <div id="term-cont">
+            <div class="row term-block">
+                <div class="form-group">
+                    <label for="pred_ns[]">Prefix</label>
+                    <select class="form-control" name="pred_ns[]">
+                        <option selected value="">(none)</option>
+                    {% for ns in nsm.namespaces() | sort %}
+                        <option value="{{ns[0]}}">{{ns[0]}}</option>
+                    {% endfor %}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <label for="pred[]">Predicate</label>
+                    <input type="text" class="form-control" name="pred[]">
+                </div>
+                <div class="form-group">
+                    <label for="op[]">Operand</label>
+                    <select class="form-control" name="op[]">
+                    {% for op in operands %}
+                        <option value="{{op[0]}}">{{op[1]}}</option>
+                    {% endfor %}
+                    </select>
+                </div>
+                <div class="form-group">
+                    <label for="val[]">Value</label>
+                    <input type="text" class="form-control" name="val[]">
+                </div>
+                <div class="form-group">
+                    <a class="delete-row btn btn-danger" href="#">- Remove</a>
+                </div>
+            </div>
+        </div>
+        <div class="row">
+            <a class="add-row btn btn-success" id="add-row" href="#">+ Add Row</a>
+        </div>
+        <div class="row">
+            <input type="submit" class="btn btn-primary btn-lg">
+        </div>
+    </form>
+    <div id="search-results-wrap"></div>
+    {% include 'namespaces.html' %}
+{% endblock %}
+{% block tail_js %}
+    <script>
+        $(function(){
+            $('.term-block').first().find('.delete-row').hide();
+            $('#add-row').on('click', function(){
+                var term = $('.term-block').last().clone(true, true);
+                term.find('input[type="text"]').val('');
+                term.find('select').val('');
+                term.find('.delete-row').show();
+                term.appendTo('#term-cont');
+            });
+            $('.delete-row').on('click', function(){
+                $(this).closest('.term-block').remove();
+            });
+
+            $('#term-search-form').submit(function(ev) {
+                $.ajax({
+                    type: 'POST',
+                    url: '',
+                    data: $(this).serialize(),
+                    encode: true
+                })
+                    .done(function(data) {
+                        //var cont = $('#search-results-wrap')
+                        $('#search-results-wrap').removeClass('bg-danger')
+                            .html(data);
+                    })
+                    .fail(function(data) {
+                        $('#search-results-wrap').addClass('bg-danger')
+                            .html(data.responseText);
+                    });
+                ev.preventDefault();
+            });
+        });
+    </script>
 {% endblock %}
 

+ 13 - 0
lakesuperior/endpoints/templates/term_search_results.html

@@ -0,0 +1,13 @@
+<div id="search-results">
+{% if qres %}
+    <h2>Search Results</h2>
+    <ol>
+    {% for uri in qres %}
+        <li><a href="{{gl(uri)}}">{{uri}}</a></li>
+    {% else %}
+        <p>No resources match the given criteria.</p>
+    {% endfor %}
+    </ol>
+{% endif %}
+</div>
+