diff --git a/web/controller.py b/web/controller.py index 7e56b1e..842cdeb 100644 --- a/web/controller.py +++ b/web/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Packages2 CherryPy Application and launcher""" -import os, sys, math +import os, sys, math, string, re, types import cherrypy @@ -20,9 +20,11 @@ class Root(object): """Packages2 CherryPy Application""" database = None + regexp_atomic = None def __init__(self, db): self.database = db + self.regexp_atomic = re.compile(r"(>|>=|=|<=|<)?([a-z0-9-]*/)?([a-z-]*)(-[0-9.]*)?(-r[0-9])?$", re.IGNORECASE) @property @@ -409,6 +411,78 @@ class Root(object): @cherrypy.expose @template.expire_on_30_min() + @template.output('index.html', method='xhtml') + def search(self, *args, **kwds): + """Search for ebuilds""" + return self._search(*args, **kwds) + + def _search(self, *args, **kwds): + """Search for ebuilds""" + sstring = string.strip(kwds["searchstring"]) + + db = self.database + entry_filter = filters.EntryFilters(db) + + sinfo = { + "comparator": None, + "categories": [], + "pkgname": None, + "version": None, + "anyfield": None, + } + + # process search string + match = self.regexp_atomic.match(sstring) + if match: + [comparator, category, pkgname, version, revision] = match.groups() + sinfo["pkgname"] = pkgname + + # regexp matches single word too, so a single word must not be a package name + if pkgname and not (category or version): + sinfo["anyfield"] = pkgname + sinfo["pkgname"] = None + + if version: # remove leading divis + version = version[1:] + if revision: + sinfo["version"] = version+revision + else: + sinfo["version"] = version + if not comparator: + sinfo["comparator"] = "=" + else: + sinfo["comparator"] = comparator + if category: + sinfo["categories"].append(category[:len(category)-1]) #remove tailing slash + else: + sinfo["anyfield"] = sstring + + limit = filters.limit_centercount(kwds) + + search_entries = entry_filter.search_filter(sinfo, limit=None) + center_pkgs = build_centerpkg_list(search_entries, + db.get_package_details_cpv, None) + + pagetitle = "/search" + + arches = filters.limit_arches(kwds) + kwds = sanitize_query_string(kwds) + latest_entries = self.cache_latest + left_daycount = filters.limit_leftcount(kwds) + day_list = latest_per_day(latest_entries, left_daycount) + left_daycount = filters.limit_leftcount(kwds) + arches = filters.limit_arches(kwds) + latest_entry = entry_filter.latest_entry() + + db.close_mc() + return template.render(arches = arches, + daylist = day_list, center_pkgs = center_pkgs, + lastupdate = latest_entry, safeqs = kwds, + pagetitle = pagetitle) + + + @cherrypy.expose + @template.expire_on_30_min() @template.output('index.xml', method='xml') def feed(self, *args, **kwds): """Render the /feed/ page as Atom""" @@ -427,6 +501,8 @@ class Root(object): return self._category(*args, **kwds) elif base == "package": return self._package(*args, **kwds) + elif base == "search": + return self._search(*args, **kwds) elif base == "verbump": return self._verbump(*args, **kwds) elif base == "newpackage": diff --git a/web/lib/filters.py b/web/lib/filters.py index 6afacc1..d7290b5 100644 --- a/web/lib/filters.py +++ b/web/lib/filters.py @@ -82,6 +82,14 @@ class EntryFilters(object): return [] return self.package_source.get_latest_cpvs_by_pkgname(pkgname, limit) + def search_filter(self, sinfo, limit=None): + """we don't use cache here""" + return self._search_filter(sinfo, limit) + + def _search_filter(self, sinfo, limit=None): + """filter packages by search criteria (uncached)""" + return self.package_source.get_cpvs_by_searchcriteria(sinfo, limit) + def category_package_filter(self, category, pkgname, limit=None): """Skip packages not matching pkgname and category""" key = 'category_package_filter_%r%r%r' % (category, pkgname, limit) diff --git a/web/lib/query_filter.py b/web/lib/query_filter.py index afa6fad..5bb72da 100644 --- a/web/lib/query_filter.py +++ b/web/lib/query_filter.py @@ -102,6 +102,8 @@ def create_rel(path): t = '%s %s' % (m[3], m[2]) elif path.startswith('/verbump'): t = 'Version bumps' + elif path.startswith('/search'): + t = 'Search' elif path.startswith('/newpackage'): t = 'New packages' elif path.startswith('/faq'): diff --git a/web/model.py b/web/model.py index dc1301e..418d1e5 100644 --- a/web/model.py +++ b/web/model.py @@ -1,6 +1,6 @@ from time import localtime, strftime, time import datetime -import re, sys +import re, sys, types import operator import cherrypy @@ -82,6 +82,7 @@ class PackageDB(object): columns_category_pn_pv = None sql = {} mc = None + valid_modes = ['+', '~', 'M', 'M+', 'M~', ''] def __init__(self, config=None): # Do not complain about correct usage of ** magic @@ -348,6 +349,123 @@ class PackageDB(object): cursor.close() return entries + sql['SELECT_get_cpvs_by_searchcriteria'] = """ + SELECT __CPV__, versions.mtime + FROM versions + JOIN packages USING (cp) + JOIN categories USING (c) + __JOINS__ + WHERE __WHERE__ + ORDER by versions.mtime DESC + LIMIT 0, ? + """ + def get_cpvs_by_searchcriteria(self, sinfo, limit=None): + """return cpvs matching the search criteria""" + criteria = [] + params = [] + joins = [] + + # strrep produces strings like "(?,?,?,?)" + def strrep(chr, delimiter, count): + ret = chr + for i in range(count-1): + ret += delimiter + ret += chr + return ret + + def add_criterion(arg, sql, join_info): + if type(arg)==types.ListType: + if replacer: + sql = sql % replacer + criteria.append(sql) + params.extend(arg) + else: + criteria.append(sql) + params.append(arg) + if join_info: + if join_info not in joins: + joins.append(join_info) + return + + sql_criteria = { + "pkgname": "pn LIKE "+self.syntax_placeholder, + "categories": "category IN (%s)", + "category": "category LIKE "+self.syntax_placeholder, + "version_eq": "pv = "+self.syntax_placeholder, + "version_gt": "pv > "+self.syntax_placeholder, + "version_ge": "pv >= "+self.syntax_placeholder, + "version_lt": "pv < "+self.syntax_placeholder, + "version_le": "pv <= "+self.syntax_placeholder, + "anyfield": "((MATCH (description, changelog) AGAINST ("+self.syntax_placeholder+")) OR (MATCH (pn) AGAINST("+self.syntax_placeholder+")))" + } + + if sinfo["pkgname"]: + add_criterion("%"+sinfo["pkgname"]+"%", sql_criteria["pkgname"], None) + + if sinfo["categories"]: + if len(sinfo["categories"]) > 1: + add_criterion(sinfo["categories"], sql_criteria["categories"], None, strrep(self.syntax_placeholder, ",", len(sinfo["categories"]))) + else: + add_criterion("%"+sinfo["categories"][0]+"%", sql_criteria["category"], None) + + if sinfo["version"]: + available_comparators = { + ">": sql_criteria["version_gt"], + ">=": sql_criteria["version_ge"], + "=": sql_criteria["version_eq"], + "<=": sql_criteria["version_le"], + "<": sql_criteria["version_lt"] + } + if not sinfo["comparator"]: + sinfo["comparator"] = "=" + if sinfo["comparator"] not in available_comparators.keys(): + return [] + add_criterion(sinfo["version"], available_comparators[sinfo["comparator"]], None) + + if sinfo["anyfield"]: + add_criterion(sinfo["anyfield"], sql_criteria["anyfield"], ["metadata", "cp"]) + params.append(sinfo["anyfield"]) + + if limit is None: + limit = 1000 + try: + limit = int(limit) + except ValueError: + return [] + params.append(limit) + + + # build sql query string + criteria_str = "" + i = 0 + for c in criteria: + if (i > 0) and i < len(criteria): + criteria_str += " AND " + criteria_str += c + i += 1 + joins_str = "" + for j in joins: + joins_str += "JOIN %s USING (%s) " % (j[0], j[1]) + print joins_str + + sql = self.sql['SELECT_get_cpvs_by_searchcriteria'] + + reps = [] + reps.append(('__JOINS__', joins_str)) + reps.append(('__WHERE__', criteria_str)) + # Only used once + spacematch = re.compile(r'(\s+|\n)') + for o, n in reps: + sql = sql.replace(o, n) + sql = spacematch.sub(' ', sql) + + + cursor = self.cursor() + cursor.execute(sql, params) + entries = cursor.fetchall() + cursor.close() + return entries + sql['SELECT_get_latest_cpvs_by_arch_mode'] = """ SELECT __CPV__, versions.mtime FROM arches @@ -373,7 +491,7 @@ class PackageDB(object): def get_latest_cpvs_by_arch(self, arch, mode, limit=None): """return modified cpvs limited by arch and mode""" - valid_modes = ['+', '~', 'M', 'M+', 'M~', ''] + valid_modes = self.valid_modes params = () if mode not in valid_modes or arch not in self.arches: return [] diff --git a/web/static/style.css b/web/static/style.css index 84a0174..a0e0476 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -212,6 +212,10 @@ a:visited { background: #dddaec; } +#rightcontent input.search { + width: 8em; +} + #rightcontent p, #rightcontent ul { /* margin: 1em; */ } diff --git a/web/templates/layout.html b/web/templates/layout.html index 6ca22ee..2aca77d 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -56,7 +56,16 @@ def alpha_url(baseurl):

Last update:
${HTML(lastmodified_rightcontent(lastupdate))}

- + +
+ Search + +
+ + +
+
+
Legend