Author: ahorincar
Date: Sun Aug 17 20:22:10 2014
New Revision: 1618520

URL: http://svn.apache.org/r1618520
Log:
Added docstrings, refactored code, added unit test for checking if the schema 
is valid, added README file containing the installation guide.

Added:
    bloodhound/branches/bep_0014_solr/bloodhound_solr/README.wiki
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/
    
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/schema_test_file.xml
Removed:
    bloodhound/branches/bep_0014_solr/bloodhound_solr/README
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/backend.py
    
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/search_resources/
Modified:
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py
    bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py
    bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py
    
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html

Added: bloodhound/branches/bep_0014_solr/bloodhound_solr/README.wiki
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/README.wiki?rev=1618520&view=auto
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/README.wiki (added)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/README.wiki Sun Aug 17 
20:22:10 2014
@@ -0,0 +1,149 @@
+= Plugin to add Apache Solr as a search alternative for Apache Bloodhound
+
+== Description
+
+The plugin enhances Apache Bloodhound with Apache Solr functionality, namely
+it allows using Apache Solr as a search alternative.
+
+== Dependencies
+
+This plugin depends on the following components to be installed:
+
+  - [http://trac.edgewall.org Trac]  ,,Since version ''' 1.0 ''',, .
+  - [http://lucene.apache.org/solr/index.html Apache Solr] ,,Version '''
+  4.7.2 ''',, .
+  - [http://lxml.de/ Lxml]
+  - [https://github.com/tow/sunburnt Sunburnt] ,,Since version '''0.6''',, .
+  - [https://code.google.com/p/httplib2/ Httplib2]
+
+
+== Latest Version
+The latest version of the source code for the plugin can be found
+[https://svn.apache.org/repos/asf/bloodhound/branches/bep_0014_solr/ here].
+
+== Installation
+
+== Apache Solr
+This plugin has been tested with Apache Solr, '''version 4.7.2.'''[[BR]]
+
+Before proceeding to install Apache Solr, you need to have '''Java 1.7''' or
+greater installed.
+
+Apache Solr can be downloaded from
+[http://www.apache.org/dyn/closer.cgi/lucene/solr here].[[BR]]
+
+Once you've downloaded Apache Solr, unzip it in your chosen directory and
+navigate (in a terminal) to the
+
+{{{
+<path_to_solr_dir>/example directory
+}}}
+
+The Solr Server can be started by issuing the following command:
+[[BR]]
+
+'''On UNIX''':
+{{{
+java -jar start.jar
+}}}
+
+
+'''On Windows''':
+{{{
+java -Dsolr.solr.home=<path_to_solr_dir> -jar start.jar
+}}}
+
+
+You can test whether this worked by loading
+{{{
+http://localhost:8983/solr/
+}}}
+
+in your browser.
+
+== Bloodhound Solr plugin
+Once downloaded, the plugin should be placed in the directory where you have
+installed your Bloodhound instance (e.g. /home/bloodhound).
+[[BR]]
+
+You should then make the following changes in the base.ini file:
+
+Under the '''[bhsearch]''' section, add the following:
+
+{{{
+search_backend = SolrBackend
+solr_server_url = http://localhost:8983/solr/
+}}}
+
+
+Under the '''[components]''' section, add the following:
+
+{{{
+bhsolr.* = enabled
+}}}
+
+
+'''Note:''' For compatibility with Apache Solr, you should choose a default
+field to be used when
+no query is specified. This can be updated in the '''solrconfig.xml''' file
+found at:
+
+{{{
+<your_solr_directory>/libexec/example/solr/collection1/conf
+}}}
+
+Search for the request handler having the name set to ''''/select'''', and
+modify the line
+
+{{{
+       <str name="df">text</str>
+}}}
+to
+{{{
+       <str name="df">your_chosen_default_field</str>
+}}}
+
+Make sure your chosen default field exists in the Bloodhound schema (see below
+the existing fields in the Bloodhound schema).[[BR]]
+Current Bloodhound schema: '''unique_id''', '''id''', '''type''',
+'''product''', '''milestone''', '''time''', '''due''', '''completed''',
+'''author''', '''component''', '''status''', '''resolution''', '''keywords''',
+'''summary''', '''content''', '''changes''', '''owner''', '''repository''',
+'''revision''', '''message''', '''required_permission''', '''name''',
+'''query_suggestion_basket''', '''relations'''.
+
+Once all these steps have been accomplished, install the Bloodhound Solr
+plugin as described
+[http://trac.edgewall.org/wiki/TracPlugins#InstallingaTracplugin here].
+
+The next and final step is to generate the '''schema.xml''' file to be used
+with your Solr instance. From the '''bloodhound/installer''' directory,
+issue the following '''trac-admin''' command:
+
+{{{
+trac-admin <path_to_your_bloodhound_environment> bhsolr generate_schema
+<path_to_your_solr_directory>/libexec/example/solr/collection1/conf
+}}}
+
+'''Note:''' Make sure your Bloodhound environment is activated and your Solr
+server is running. Also, if you don't provide the path to where the schema
+should be generated, the program will generate in your current working
+directory.
+
+In order for Solr to use the newly generated schema, you need to restart your
+Solr server.
+
+At this point you should be able to use Apache Solr as a search platform for
+Bloodhound.
+
+== Licensing
+
+The plugin is licensed under the [http://www.apache.org/licenses/LICENSE-2.0
+Apache License] ,,Version '''2.0''',, .
+
+== Bug / feature requests
+
+Existing bugs and feature requests for the Bloodhound Solr plugin are
+[query:status=new|assigned|reopened&keywords=~bep-0014 here].
+If you have any issues, please create a
+[/newticket?keywords=bep-0014 new ticket].

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py (original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/admin.py Sun Aug 
17 20:22:10 2014
@@ -17,16 +17,35 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
+
+"""
+Module holding the implementation of the IAdminCommandProvider
+interface, so that a new trac-admin command is available to be used.
+"""
+
 from trac.core import Component, implements
 from trac.admin import IAdminCommandProvider
 from bhsolr.schema import SolrSchema
 
 class BloodhoundSolrAdmin(Component):
-
     implements(IAdminCommandProvider)
 
-    # IAdminCommandProvider methods
+    """Class implementing the IAdminCommandProvider.
+
+    Provide a new trac-admin command, suitable for use with the
+    Bloodhound Solr plugin.
+    """
+
     def get_admin_commands(self):
+        """Generate a new trac-admin command.
+
+        Create a trac-admin command for generating a schema.xml file.
+        Yields a touple containing the command name for generating a
+        Solr schema, the argument description (i.e. the path to where
+        the Solr schema should be generated, a help text and the
+        callback function that generates the Solr schema.
+        """
+
         yield ('bhsolr generate_schema', '<path>',
                'Generate Solr schema',
                None, SolrSchema(self.env).generate_schema)

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py 
(original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/schema.py Sun Aug 
17 20:22:10 2014
@@ -17,6 +17,11 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
+"""
+Module providing a feature to generate a schema.xml file containing
+the definition of a Solr schema that can be used for Bloodhound.
+"""
+
 import os
 
 from lxml import etree
@@ -27,12 +32,30 @@ from bhsolr.solr_backend import SolrBack
 
 class SolrSchema(Component):
 
+    """Class for creating a schema.xml file.
+
+    Define the fields to be written to the schema.xml file, and write
+    it at a specified path.
+
+    Class Attributes:
+    REQUIRED_FIELDS -- the fields required for each searchable object
+    FIELDS_TYPE_DICT -- the types each field can have, along with its
+    equivalent name to be used in the Solr schema.xml file
+
+    Instance Attributes:
+    path -- the path to where tha schema should be saved
+    schema -- the schema definition from bhsearch.WhooshBackend
+    schema_element -- the main etree.Element for the schema.xml file
+    fields_element -- the etree.SubElement for defining the fields
+    unique_key_element -- the etree.SubElement for defining the unique
+    key
+    """
+
     REQUIRED_FIELDS = {
             "id": True,
             "unique_id": True,
             "type": True
             }
-
     FIELDS_TYPE_DICT = {
         "ID": "string",
         "DATETIME": "date",
@@ -41,8 +64,12 @@ class SolrSchema(Component):
         }
 
     def __init__(self):
+        """Initialise main XML elements and set their values."""
         self.path = None
+
+        # The main parent elements.
         self.schema = WhooshBackend.SCHEMA
+
         self.schema_element = etree.Element("schema")
         self.schema_element.set("name", "Bloodhound Solr Schema")
         self.schema_element.set("version", "1")
@@ -53,6 +80,9 @@ class SolrSchema(Component):
                                                    "uniqueKey")
         self.unique_key_element.text = SolrBackend.UNIQUE_ID
 
+        # Children of the element containing the definitions of fields;
+        # these are required so that Solr is able to validate the
+        # schema.
         version_field = etree.SubElement(self.fields_element, "field")
         version_field.set("name", "_version_")
         version_field.set("type", "long")
@@ -75,13 +105,29 @@ class SolrSchema(Component):
         stored_name.set("multiValued", "false")
 
     def generate_schema(self, path=None):
+        """Write a schema.xml file.
+
+        Create all XML elements needed in the schema.xml file, along
+        with their values and write the file.
+
+        Keyword Arguments:
+        path -- the path to where the schema.xml file should be saved
+        (default None)
+        """
+
+        # If the user hasn't provided a path to where the schema.xml
+        # file should be saved, then the schema.xml file will be saved
+        # in the current directory.
         if not path:
           path = os.getcwd()
         self.path = os.path.join(path, 'schema.xml')
 
-        self.add_all_fields()
+        # Adds all fields and all type definitions to the main
+        # etree.Element
+        self.prepare_all_fields()
         self.add_type_definitions()
 
+        # Writes the schema file.
         doc = etree.ElementTree(self.schema_element)
         out_file = open(os.path.join(path, 'schema.xml'), 'w')
         doc.write(out_file, xml_declaration=True, encoding='UTF-8',
@@ -91,6 +137,25 @@ class SolrSchema(Component):
     def add_field(
                 self, field_name, name_attr, type_attr, indexed_attr,
                 stored_attr, required_attr, multivalued_attr):
+        """Add field to the etree.Sublement object.
+
+        Add a child to the 'fields' SubElement, and set the
+        attributes for this child.
+
+        Keyword Arguments:
+        field_name -- the name of the XML element
+        name_attr -- the name of the schema field
+        type_attr -- the type of the schema field
+        indexed_attr -- boolean for keeping track whether the schema
+        field should be indexed or not
+        stored_attr -- boolean for keeping track whether the schema
+        field should be stored or not
+        required_attr -- boolean for keeping track whether the schema
+        field is required or not
+        multivalued_attr -- boolean for keeping track whether the
+        schema field is multivalued or not
+        """
+
         field = etree.SubElement(self.fields_element, field_name)
         field.set("name", name_attr)
         field.set("type", type_attr)
@@ -99,7 +164,13 @@ class SolrSchema(Component):
         field.set("required", required_attr)
         field.set("multiValued", multivalued_attr)
 
-    def add_all_fields(self):
+    def prepare_all_fields(self):
+        """Prepare the attributes for each field in the schema.
+
+        Iterate through all the schema fields, and get the values for
+        each attribute needed.
+        """
+
         for (field_name, field_attrs) in self.schema.items():
             class_name = str(field_attrs.__class__.__name__)
             type_attr = self.FIELDS_TYPE_DICT[class_name]
@@ -111,10 +182,12 @@ class SolrSchema(Component):
             else:
                 required_attr = "false"
 
+            # Add field to the etree.Subelement holding all fields.
             self.add_field("field", field_name, type_attr, indexed_attr,
                            stored_attr, required_attr, "false")
 
     def add_type_definitions(self):
+        """Add definitions of all types used for the schema fields."""
         self.types_element = etree.SubElement(self.schema_element, "types")
         self._add_string_type_definition()
         self._add_text_general_type_definition()
@@ -123,12 +196,14 @@ class SolrSchema(Component):
         self._add_lowercase_type_definition()
 
     def _add_string_type_definition(self):
+        """Create the XML definition of the 'string' type."""
         field_type = etree.SubElement(self.types_element, "fieldType")
         field_type.set("name", "string")
         field_type.set("class", "solr.StrField")
         field_type.set("sortMissingLast", "true")
 
     def _add_text_general_type_definition(self):
+        """Create the XML definition of the 'text_general' type."""
         field_type = etree.SubElement(self.types_element, "fieldType")
         field_type.set("name", "text_general")
         field_type.set("class", "solr.TextField")
@@ -169,6 +244,7 @@ class SolrSchema(Component):
         analyzer_query_filter_l.set("class", "solr.LowerCaseFilterFactory")
 
     def _add_date_type_definition(self):
+        """Create the XML definition of the 'date' type."""
         field_type = etree.SubElement(self.types_element, "fieldType")
         field_type.set("name", "date")
         field_type.set("class", "solr.TrieDateField")
@@ -176,6 +252,7 @@ class SolrSchema(Component):
         field_type.set("positionIncrementGap", "0")
 
     def _add_long_type_definition(self):
+        """Create the XML definition of the 'long' type."""
         field_type = etree.SubElement(self.types_element, "fieldType")
         field_type.set("name", "long")
         field_type.set("class", "solr.TrieLongField")
@@ -183,6 +260,7 @@ class SolrSchema(Component):
         field_type.set("positionIncrementGap", "0")
 
     def _add_lowercase_type_definition(self):
+        """Create the XML definition of the 'lowercase' type."""
         field_type = etree.SubElement(self.types_element, "fieldType")
         field_type.set("name", "lowercase")
         field_type.set("class", "solr.TextField")

Modified: 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py 
(original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/solr_backend.py 
Sun Aug 17 20:22:10 2014
@@ -17,6 +17,11 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
+"""
+Module providing an implementation of the bhsearch.api.ISearchBackend
+interface, suitable for use with Apache Solr.
+"""
+
 import re
 import hashlib
 
@@ -40,8 +45,27 @@ from trac.util.datefmt import utc
 class SolrBackend(Component):
     implements(ISearchBackend)
 
-    UNIQUE_ID = "unique_id"
+    """Class for implementing the ISearchBackend interface.
+
+    Define the ISearchBackend methods, so that these methods can be
+    appropriately used for communicating with the Solr server (via
+    Sunburnt).
+
+    Class Attributes:
+    UNIQUE_ID -- the field to be used for distinguishing between
+    searchable objects
+    HIGHLIGHTABLE_FIELDS -- the fields that can be highlighted in
+    search results
+    server_url -- Option object for adding a new configuration option
+    (that lets the user set the Solr server URL) to the trac.ini
+    configuration file
+
+    Instance Attributes:
+    solr_interface -- a SolrInterface object for communicating with the
+    Solr server through Sunburnt
+    """
 
+    UNIQUE_ID = "unique_id"
     HIGHLIGHTABLE_FIELDS = {
         "unique_id" : True,
         "id" : True,
@@ -62,7 +86,6 @@ class SolrBackend(Component):
         "message" : True,
         "name" : True
         }
-
     server_url = Option(
             BHSEARCH_CONFIG_SECTION,
             'solr_server_url',
@@ -70,31 +93,86 @@ class SolrBackend(Component):
             doc_domain='bhsearch')
 
     def __init__(self):
+        """Initialise a SolrInterface object.
+
+        Initialise the SolrInterface object with the Solr server
+        URL provided in trac.ini.
+        """
+
         self.solr_interface = SolrInterface(str(self.server_url))
 
     def add_doc(self, doc, operation_context=None):
+        """Add a new document to the Solr index.
+
+        The contents should be a dict with fields matching the search
+        schema. The only required fields are type and id, everything
+        else is optional.
+
+        Keyword Arguments:
+        doc -- the document to be added
+        operation_context -- required by the ISearchBackend API
+        (default None)
+        """
+
         self._reformat_doc(doc)
+        # Create a unique ID for distinguishing between the searchable
+        # objects.
         doc[self.UNIQUE_ID] = self._create_unique_id(doc.get("product", ''),
-                                                doc["type"], doc["id"])
+                                                     doc["type"], doc["id"])
         self.solr_interface.add(doc)
         self.solr_interface.commit()
 
-    def delete_doc(product, doc_type, doc_id, operation_context=None):
+    def delete_doc(self, product, doc_type, doc_id, operation_context=None):
+        """Delete a document from the Solr index.
+
+        The product, type and ID of the document must be provided.
+
+        Keyword Arguments:
+        product -- the product associated with the searchable object
+        doc_type -- the type of the searchable object
+        doc_id -- the ID of the searchable object
+        operation_context -- required by the ISearchBackend API
+        (default None)
+        """
+
         unique_id = self._create_unique_id(product, doc_type, doc_id)
         self.solr_interface.delete(unique_id)
 
     def optimize(self):
+        """Optimise the Solr index."""
         self.solr_interface.optimize()
 
     def query(
             self, query, query_string, sort = None, fields = None,
             filter = None, facets = None, pagenum = 1, pagelen = 20,
             highlight = False, highlight_fields = None, context = None):
+        """Process the query to be made to the Solr server.
+
+        Create a query chain, and execute the query to the Solr server.
+        Return the results, the More Like This results and their
+        associated hexdigests.
+
+        Keyword Arguments:
+        query -- a whoosh.Query object holding the parsed query
+        query_string -- the original query string
+        sort -- a list of SortInstruction objects (default None)
+        fields -- a list of fields to select (default None)
+        filter -- a filter query object (default None)
+        facets -- a list of facet fields (default None)
+        pagenum -- the number of pages (default 1)
+        pagelen -- the maximum page length (default 20)
+        highlight -- highlight matched terms in field (default False)
+        highlight_fields -- a list of fields to highlight
+        (default None)
+        context -- required by the ISearchBackend API
+        (default None)
+        """
 
         if not query_string:
             query_string = "*.*"
 
-        final_query_chain = self._create_query_chain(query, query_string)
+        # Create the query chain to be queried against Solr.
+        final_query_chain = self._create_query_chain(query_string)
         solr_query = self.solr_interface.query(final_query_chain)
         faceted_solr_query = solr_query.facet_by(facets)
         highlighted_solr_query = faceted_solr_query.highlight(
@@ -105,16 +183,32 @@ class SolrBackend(Component):
                             start=start, rows=pagelen)
         results = paginated_solr_query.execute()
 
+        # Process the More Like This results for the original query.
         mlt, hexdigests = self.query_more_like_this(paginated_solr_query,
                                                     fields="type", mindf=1,
                                                     mintf=1)
 
+        # Process the query result so that it is compatible with the
+        # bhsearch.web_ui internal methods that display the results.
         query_result = self._create_query_result(highlighted_solr_query,
                                                  results, fields, pagenum,
                                                  pagelen)
         return query_result, mlt, hexdigests
 
     def query_more_like_this(self, query_chain, **kwargs):
+        """Retrieve and process More Like These results.
+
+        Execute the query_chain against the Solr server, create a
+        hexdigest for each document retrieved (for interface purposes),
+        process the result accordingly and return a dictionary
+        containing the More Like This results and a dictionary holding
+        the hexdigests for each document.
+
+        Keyword Arguments:
+        query_chain -- the query chain to be executed to the Solr server
+        **kwargs -- remaining keyword arguments
+        """
+
         mlt_results = query_chain.mlt(**kwargs).execute().more_like_these
         mlt_dict = {}
         hexdigests = {}
@@ -125,13 +219,22 @@ class SolrBackend(Component):
 
             for mlt_doc in results.docs:
                 if doc not in mlt_dict:
-                    mlt_dict[doc] = [self._process_doc(mlt_doc)]
+                    mlt_dict[doc] = [self._process_mlt_doc(mlt_doc)]
                 else:
-                    mlt_dict[doc].append(self._process_doc(mlt_doc))
+                    mlt_dict[doc].append(self._process_mlt_doc(mlt_doc))
 
         return mlt_dict, hexdigests
 
-    def _process_doc(self, doc):
+    def _process_mlt_doc(self, doc):
+        """Build a dictionary containing required details for the doc.
+
+        Store the required field values for the doc and return this
+        dictionary.
+
+        Keyword Arguments:
+        doc -- a SolrResult object holding the document's details
+        """
+
         ui_doc = dict(doc)
 
         if doc.get('product'):
@@ -147,9 +250,19 @@ class SolrBackend(Component):
 
     def _create_query_result(
                         self, query, results, fields, pagenum, pagelen):
+        """Create a QueryResult object with the retrieved results.
+
+        Keyword Arguments:
+        query -- a whoosh.Query object holding the parsed query
+        results -- a SolrResponse object holding the retrieve results
+        fields -- a list of fields to select
+        pagenum -- the number of pages
+        pagelen -- the maximum page length
+        """
+
         total_num, total_page_count, page_num, offset = \
-                    self._prepare_query_result_attributes(query, results,
-                                                          pagenum, pagelen)
+                    self._prepare_query_result_attributes(query, pagenum,
+                                                          pagelen)
 
         query_results = QueryResult()
         query_results.hits = total_num
@@ -172,7 +285,19 @@ class SolrBackend(Component):
 
         return query_results
 
-    def _create_query_chain(self, query, query_string):
+    def _create_query_chain(self, query_string):
+        """Create the final query chain.
+
+        For each token in the query string, create a query with the
+        token against each field. Concatenate all created queries, and
+        return the final query chain.
+
+        Keyword Arguments:
+        query_string -- the original query string
+        """
+
+        # Match the regex against the query string, in order to find
+        # all the words in the query.
         matches = re.findall(re.compile(r'([\w\*]+)'), query_string)
         tokens = set([match for match in matches])
 
@@ -187,6 +312,17 @@ class SolrBackend(Component):
         return final_query_chain
 
     def _process_record(self, fields, retrieved_record):
+        """Process attributes for the retrieved document or record.
+
+        Create a dictionary holding all fields with their retrieved
+        value from the record.
+
+        Keyword Arguments:
+        retrieved_record -- a dictionary holding the retrieved record's
+        attributes
+        fields -- a list of fields to select
+        """
+
         result_doc = dict()
         if fields:
             for field in fields:
@@ -202,12 +338,22 @@ class SolrBackend(Component):
         return result_doc
 
     def _from_whoosh_format(self, value):
+        """Find the timezone for the datetime instance."""
         if isinstance(value, datetime):
             value = utc.localize(value)
         return value
 
-    def _prepare_query_result_attributes(
-                                    self, query, results, pagenum, pagelen):
+    def _prepare_query_result_attributes(self, query, pagenum, pagelen):
+        """Prepare the QueryResult attributes.
+
+        Prepare the attributes needed for creating a QueryResult object.
+
+        Keyword Arguments:
+        query -- the query to be executed against Solr
+        pagenum -- the number of pages
+        pagelen -- the maximum number of results per page
+        """
+
         results_total_num = query.execute().result.numFound
         total_page_count = int(ceil(results_total_num / pagelen))
         pagenum = min(total_page_count, pagenum)
@@ -219,9 +365,12 @@ class SolrBackend(Component):
         return results_total_num, total_page_count, pagenum, offset
 
     def is_index_outdated(self):
+        # Not applicable to SolrBackend.
         return False
 
     def recreate_index(self):
+        # This method is replaced by the trac-admin command to generate
+        # a Solr schema.
         return True
 
     @contextmanager
@@ -229,20 +378,28 @@ class SolrBackend(Component):
         yield
 
     def _search_fields_for_token(self, token):
+        """Create a query chain for each field.
+
+        For each field, create a query chain querying for the token.
+
+        Keyword Arguments:
+        token -- a String holding the current token
+        """
+
         q_chain = None
         field_boosts = DefaultQueryParser(self.env).field_boosts
 
         for field, boost in field_boosts.iteritems():
-            if field != 'query_suggestion_basket' and field != 'relations':
-                field_token_dict = {field: token}
-                if q_chain is None:
-                    q_chain = self.solr_interface.Q(**field_token_dict)**boost
-                else:
-                    q_chain |= self.solr_interface.Q(**field_token_dict)**boost
+            field_token_dict = {field: token}
+            if q_chain is None:
+                q_chain = self.solr_interface.Q(**field_token_dict)**boost
+            else:
+                q_chain |= self.solr_interface.Q(**field_token_dict)**boost
 
         return q_chain
 
     def _reformat_doc(self, doc):
+        # Needed for compatibility with bhsearch
         for key, value in doc.items():
             if key is None:
                 del doc[None]
@@ -254,6 +411,7 @@ class SolrBackend(Component):
                 doc[key] = self._to_whoosh_format(value)
 
     def _to_whoosh_format(self, value):
+        # Needed for compatibility with bhsearch
         if isinstance(value, basestring):
             value = unicode(value)
         elif isinstance(value, datetime):
@@ -261,12 +419,14 @@ class SolrBackend(Component):
         return value
 
     def _convert_date_to_tz_naive_utc(self, value):
+        # Needed for compatibility with bhsearch
         if value.tzinfo:
             utc_time = value.astimezone(utc)
             value = utc_time.replace(tzinfo=None)
         return value
 
     def _create_unique_id(self, product, doc_type, doc_id):
+        """Create a unique ID for the doc."""
         if product:
             return u"%s:%s:%s" % (product, doc_type, doc_id)
         else:

Modified: 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py 
(original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/__init__.py 
Sun Aug 17 20:22:10 2014
@@ -1,15 +1,36 @@
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
 try:
     import unittest2 as unittest
 except ImportError:
     import unittest
 
 from bhsolr.tests import (
-    backend
+    solr_backend, schema
 )
 
 
 def suite():
     test_suite = unittest.TestSuite()
+    test_suite.addTest(solr_backend.suite())
+    test_suite.addTest(schema.suite())
     return test_suite
 
 if __name__ == '__main__':

Added: 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/schema_test_file.xml
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/schema_test_file.xml?rev=1618520&view=auto
==============================================================================
--- 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/schema_test_file.xml
 (added)
+++ 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/data/schema_test_file.xml
 Sun Aug 17 20:22:10 2014
@@ -0,0 +1,57 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<schema name="Bloodhound Solr Schema" version="1">
+  <fields>
+    <field name="_version_" type="long" indexed="true" stored="true" 
multiValued="false"/>
+    <field name="_root_" type="string" indexed="true" stored="false"/>
+    <field name="_stored_name" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="author" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="changes" type="text_general" indexed="true" stored="false" 
required="false" multiValued="false"/>
+    <field name="completed" type="date" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="component" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="content" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="due" type="date" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="id" type="string" indexed="true" stored="true" 
required="true" multiValued="false"/>
+    <field name="keywords" type="string" indexed="true" stored="false" 
required="false" multiValued="false"/>
+    <field name="message" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="milestone" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="name" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="owner" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="product" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="query_suggestion_basket" type="text_general" indexed="true" 
stored="false" required="false" multiValued="false"/>
+    <field name="relations" type="string" indexed="true" stored="false" 
required="false" multiValued="false"/>
+    <field name="repository" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="required_permission" type="string" indexed="true" 
stored="false" required="false" multiValued="false"/>
+    <field name="resolution" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="revision" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="status" type="string" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="summary" type="text_general" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="time" type="date" indexed="true" stored="true" 
required="false" multiValued="false"/>
+    <field name="type" type="string" indexed="true" stored="true" 
required="true" multiValued="false"/>
+    <field name="unique_id" type="string" indexed="true" stored="true" 
required="true" multiValued="false"/>
+  </fields>
+  <uniqueKey>unique_id</uniqueKey>
+  <types>
+    <fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
+    <fieldType name="text_general" class="solr.TextField" 
positionIncrementGap="100">
+      <analyzer type="index">
+        <tokenizer class="solr.StandardTokenizerFactory"/>
+        <filter class="solr.StopFilterFactory" ignoreCase="true" 
words="stopwords.txt"/>
+        <filter class="solr.LowerCaseFilterFactory"/>
+      </analyzer>
+      <analyzer type="query">
+        <tokenizer class="solr.StandardTokenizerFactory"/>
+        <filter class="solr.StopFilterFactory" ignoreCase="true" 
words="stopwords.txt"/>
+        <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" 
ignoreCase="true" expand="true"/>
+        <filter class="solr.LowerCaseFilterFactory"/>
+      </analyzer>
+    </fieldType>
+    <fieldType name="date" class="solr.TrieDateField" precisionStep="0" 
positionIncrementGap="0"/>
+    <fieldType name="long" class="solr.TrieLongField" precisionStep="0" 
positionIncrementGap="0"/>
+    <fieldType name="lowercase" class="solr.TextField" 
positionIncrementGap="100">
+      <analyzer>
+        <tokenizer class="solr.KeywordTokenizerFactory"/>
+        <filter class="solr.LowerCaseFilterFactory"/>
+      </analyzer>
+    </fieldType>
+  </types>
+</schema>

Modified: 
bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py 
(original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/bhsolr/tests/schema.py 
Sun Aug 17 20:22:10 2014
@@ -0,0 +1,62 @@
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+import unittest
+import difflib
+import pkg_resources
+
+from trac.test import EnvironmentStub
+from bhsolr.schema import SolrSchema
+
+
+class SolrSchemaTestCase(unittest.TestCase):
+    def setUp(self):
+
+        self.env = EnvironmentStub(enable=('trac.*', 'bhsolr.*'))
+        resource_filename = pkg_resources.resource_filename
+        self.data_path = resource_filename(__name__,
+                                           "/data/schema_test_file.xml")
+        self.test_path = "/Users/antonia/Desktop"
+        self.schema = SolrSchema(self.env)
+
+    def test_generates_schema_correctly(self):
+        self.schema.generate_schema(path=self.test_path)
+
+        print self.data_path
+        with open(self.data_path, 'r') as hosts0:
+            with open(self.test_path + "/schema.xml", 'r') as hosts1:
+                diff = difflib.unified_diff(
+                    hosts0.readlines(),
+                    hosts1.readlines(),
+                    fromfile='schema_test_file.xml',
+                    tofile='schema.xml',
+                    )
+
+                for line in diff:
+                    self.assertEqual(line, "",
+                                     "The schema.xml file is not valid.")
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(SolrSchemaTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

Modified: bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py (original)
+++ bloodhound/branches/bep_0014_solr/bloodhound_solr/setup.py Sun Aug 17 
20:22:10 2014
@@ -21,10 +21,9 @@ from setuptools import setup, find_packa
 
 
 PKG_INFO = {
-    'bhsolr': ['../README', '../TESTING_README'],
+    'bhsolr': ['../README.wiki', '../TESTING_README'],
     'bhsolr.search_resources' : [],
-    'bhsolr.tests': ['*.*'],
-    'bhsolr.tests.search_resources': ['*.*'],
+    'bhsolr.tests': ['*.*', 'data/*.*'],
     }
 
 

Modified: 
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html?rev=1618520&r1=1618519&r2=1618520&view=diff
==============================================================================
--- 
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html
 (original)
+++ 
bloodhound/branches/bep_0014_solr/bloodhound_theme/bhtheme/templates/bh_more_like_this.html
 Sun Aug 17 20:22:10 2014
@@ -1,3 +1,22 @@
+<!--!
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";>


Reply via email to