]> git.ipfire.org Git - people/shoehn/ipfire.org.git/blobdiff - webapp/backend/planet.py
planet: Add proper full-text search.
[people/shoehn/ipfire.org.git] / webapp / backend / planet.py
index a1ea61e1b4760611e1b4079770b9044b400fd449..47036a10fb0d4af0b766ed11c77c88e544e8c131 100644 (file)
@@ -1,51 +1,48 @@
 #!/usr/bin/python
 
+import datetime
 import re
 import textile
-import tornado.database
 import unicodedata
 
-from accounts import Accounts
-from databases import Databases
+from misc import Object
 
-from misc import Singleton
+class PlanetEntry(Object):
+       def __init__(self, backend, data):
+               Object.__init__(self, backend)
 
-class PlanetEntry(object):
-       def __init__(self, db, entry=None):
-               self.db = db
-
-               if entry:
-                       self.__entry = entry
-               else:
-                       self.__entry = tornado.database.Row({
-                               "id" : None,
-                               "title" : "",
-                               "markdown" : "",
-                               "tags" : [],
-                       })
-
-       def set(self, key, val):
-               self.__entry[key] = val
-
-       @property
-       def planet(self):
-               return Planet()
+               self.data = data
 
        @property
        def id(self):
-               return self.__entry.id
+               return self.data.id
 
        @property
        def slug(self):
-               return self.__entry.slug
+               return self.data.slug
 
-       @property
-       def title(self):
-               return self.__entry.title
+       def set_title(self, title):
+               if self.title == title:
+                       return
+
+               self.db.execute("UPDATE planet SET title = %s WHERE id = %s", title, self.id)
+               self.data["title"] = title
+
+       title = property(lambda s: s.data.title, set_title)
 
        @property
-       def published(self):
-               return self.__entry.published
+       def url(self):
+               return "http://planet.ipfire.org/post/%s" % self.slug
+
+       def set_published(self, published):
+               if self.published == published:
+                       return
+
+               self.db.execute("UPDATE planet SET published = %s WHERE id = %s",
+                       published, self.id)
+               self.data["published"] = published
+
+       published = property(lambda s: s.data.published, set_published)
 
        @property
        def year(self):
@@ -57,11 +54,32 @@ class PlanetEntry(object):
 
        @property
        def updated(self):
-               return self.__entry.updated
+               return self.data.updated
+
+       def get_markdown(self):
+               return self.data.markdown
+
+       def set_markdown(self, markdown):
+               if self.markdown == markdown:
+                       return
+
+               markup = self.render(markdown)
+               self.db.execute("UPDATE planet SET markdown = %s, markup = %s WHERE id = %s",
+                       markdown, markup, self.id)
+
+               self.data.update({
+                       "markdown" : markdown,
+                       "markup"   : markup,
+               })
+
+       markdown = property(get_markdown, set_markdown)
 
        @property
-       def markdown(self):
-               return self.__entry.markdown
+       def markup(self):
+               if self.data.markup:
+                       return self.data.markup
+
+               return self.render(self.markdown)
 
        @property
        def abstract(self):
@@ -72,11 +90,30 @@ class PlanetEntry(object):
 
        @property
        def text(self):
-               return self.render(self.markdown)
+               # Compat for markup
+               return self.markup
 
        @property
        def author(self):
-               return Accounts().search(self.__entry.author_id)
+               if not hasattr(self, "__author"):
+                       self.__author = self.accounts.search(self.data.author_id)
+
+               return self.__author
+
+       def set_status(self, status):
+               if self.status == status:
+                       return
+
+               self.db.execute("UPDATE planet SET status = %s WHERE id = %s", status, self.id)
+               self.data["status"] = status
+
+       status = property(lambda s: s.data.status, set_status)
+
+       def is_draft(self):
+               return self.status == "draft"
+
+       def is_published(self):
+               return self.status == "published"
 
        # Tags
 
@@ -104,81 +141,84 @@ class PlanetEntry(object):
        tags = property(get_tags, set_tags)
 
 
-class Planet(object):
-       __metaclass__ = Singleton
-
-       @property
-       def db(self):
-               return Databases().webapp
-
+class Planet(Object):
        def get_authors(self):
+               query = self.db.query("SELECT DISTINCT author_id FROM planet WHERE status = %s \
+                       AND published IS NOT NULL AND published <= NOW()", "published")
+
                authors = []
-               for author in self.db.query("SELECT DISTINCT author_id FROM planet"):
-                       author = Accounts().search(author.author_id)
+               for author in query:
+                       author = self.accounts.search(author.author_id)
                        if author:
                                authors.append(author)
 
                return sorted(authors)
 
        def get_years(self):
-               res = self.db.query("SELECT DISTINCT YEAR(published) AS year \
-                       FROM planet ORDER BY year DESC")
+               res = self.db.query("SELECT DISTINCT EXTRACT(YEAR FROM published)::integer AS year \
+                       FROM planet WHERE status = %s ORDER BY year DESC", "published")
 
                return [row.year for row in res]
 
        def get_entry_by_slug(self, slug):
                entry = self.db.get("SELECT * FROM planet WHERE slug = %s", slug)
+
                if entry:
-                       return PlanetEntry(self.db, entry)
+                       return PlanetEntry(self.backend, entry)
 
        def get_entry_by_id(self, id):
                entry = self.db.get("SELECT * FROM planet WHERE id = %s", id)
+
                if entry:
-                       return PlanetEntry(self.db, entry)
+                       return PlanetEntry(self.backend, entry)
 
-       def _limit_and_offset_query(self, limit=None, offset=None):
-               query = " "
+       def get_entries(self, limit=3, offset=None, status="published", author_id=None):
+               query = "SELECT * FROM planet"
+               args, clauses = [], []
 
-               if limit:
-                       if offset:
-                               query += "LIMIT %d,%d" % (offset, limit)
-                       else:
-                               query += "LIMIT %d" % limit
+               if status:
+                       clauses.append("status = %s")
+                       args.append(status)
 
-               return query
+               if author_id:
+                       clauses.append("author_id = %s")
+                       args.append(author_id)
 
-       def get_entries(self, limit=3, offset=None):
-               query = "SELECT * FROM planet WHERE acknowledged='Y' ORDER BY published DESC"
+               if clauses:
+                       query += " WHERE %s" % " AND ".join(clauses)
+
+               query += " ORDER BY published DESC"
+
+               # Respect limit and offset
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
 
-               # Respect limit and offset              
-               query += self._limit_and_offset_query(limit=limit, offset=offset)
+                       if offset:
+                               query += " OFFSET %s"
+                               args.append(offset)
 
                entries = []
-               for entry in self.db.query(query):
-                       entries.append(PlanetEntry(self.db, entry))
+               for entry in self.db.query(query, *args):
+                       entry = PlanetEntry(self.backend, entry)
+                       entries.append(entry)
 
                return entries
 
        def get_entries_by_author(self, author_id, limit=None, offset=None):
-               query = "SELECT * FROM planet WHERE author_id = '%s'" % author_id
-               query += " AND acknowledged='Y' ORDER BY published DESC"
-
-               # Respect limit and offset              
-               query += self._limit_and_offset_query(limit=limit, offset=offset)
-
-               entries = self.db.query(query)
-
-               return [PlanetEntry(self.db, e) for e in entries]
+               return self.get_entries(limit=limit, offset=offset, author_id=author_id)
 
        def get_entries_by_year(self, year):
                entries = self.db.query("SELECT * FROM planet \
-                       WHERE YEAR(published) = %s ORDER BY published DESC", year)
+                       WHERE status = %s AND EXTRACT(YEAR FROM published) = %s \
+                       ORDER BY published DESC", "published", year)
 
-               return [PlanetEntry(self.db, e) for e in entries]
+               return [PlanetEntry(self.backend, e) for e in entries]
 
        def render(self, text, limit=0):
                if limit and len(text) >= limit:
                        text = text[:limit] + "..."
+
                return textile.textile(text)
 
        def _generate_slug(self, title):
@@ -197,45 +237,45 @@ class Planet(object):
 
                return slug
 
-       def update_entry(self, entry):
-               self.db.execute("UPDATE planet SET title = %s, markdown = %s WHERE id = %s",
-                       entry.title, entry.markdown, entry.id)
+       def create(self, title, markdown, author, status="published", tags=None, published=None):
+               slug = self._generate_slug(title)
+               markup = self.render(markdown)
 
-       def save_entry(self, entry):
-               slug = self._generate_slug(entry.title)
+               if published is None:
+                       published = datetime.datetime.utcnow()
 
-               self.db.execute("INSERT INTO planet(author_id, title, slug, markdown, published) "
-                       "VALUES(%s, %s, %s, %s, UTC_TIMESTAMP())", entry.author.uid, entry.title,
-                       slug, entry.markdown)
+               id = self.db.execute("INSERT INTO planet(author_id, slug, title, status, \
+                       markdown, markup, published) VALUES(%s, %s, %s, %s, %s, %s, %s)",
+                       author.uid, slug, title, status, markdown, markup, published)
 
-       def search(self, what):
-               # Split tags.
-               tags = what.split()
+               entry = self.get_entry_by_id(id)
 
-               query = "SELECT planet.* FROM planet INNER JOIN ( \
-                               SELECT post_id FROM planet_tags \
-                               INNER JOIN planet ON planet_tags.post_id = planet.id \
-                               WHERE %s GROUP BY post_id HAVING COUNT(post_id) = %%s \
-                       ) pt ON planet.id = pt.post_id ORDER BY published DESC"
-
-               args = (tags, len(tags))
+               if tags:
+                       entry.tags = tags
 
-               clauses, args = [], tags
-               for tag in tags:
-                       clauses.append("planet_tags.tag = %s")
-               args.append(len(tags))
+               return entry
 
-               entries = self.db.query(query % " OR ".join(clauses), *args)
-               return [PlanetEntry(self.db, e) for e in entries]
+       def update_entry(self, entry):
+               self.db.execute("UPDATE planet SET title = %s, markdown = %s WHERE id = %s",
+                       entry.title, entry.markdown, entry.id)
 
-       def search_autocomplete(self, what):
-               tags = what.split()
-               last_tag = tags.pop()
+       def save_entry(self, entry):
+               slug = self._generate_slug(entry.title)
 
-               res = self.db.query("SELECT tag, COUNT(tag) AS count FROM planet_tags \
-                       WHERE tag LIKE %s GROUP BY tag ORDER BY count DESC", "%s%%" % last_tag)
+               id = self.db.execute("INSERT INTO planet(author_id, title, slug, markdown, published) \
+                       VALUES(%s, %s, %s, %s, NOW())", entry.author.uid, entry.title, slug, entry.markdown)
 
-               if tags:
-                       return ["%s %s" % (" ".join(tags), row.tag) for row in res]
+               return id
 
-               return [row.tag for row in res]
+       def search(self, what):
+               res = self.db.query("WITH \
+                       q      AS (SELECT plainto_tsquery(%s, %s) AS query), \
+                       ranked AS (SELECT id, query, ts_rank_cd(to_tsvector(%s, markdown), query) AS rank \
+                               FROM planet, q WHERE markdown @@ query ORDER BY rank DESC) \
+                       SELECT *, ts_headline(markup, ranked.query, 'MinWords=100, MaxWords=110') AS markup FROM planet \
+                               JOIN ranked ON planet.id = ranked.id \
+                               WHERE status = %s AND published IS NOT NULL AND published <= NOW() \
+                               ORDER BY ranked DESC LIMIT 10",
+                       "english", what, "english", "published")
+
+               return [PlanetEntry(self.backend, e) for e in res]