import datetime
import feedparser
+import markdown2
import re
import textile
import unicodedata
from . import misc
+from .decorators import *
+
+# Used to automatically link some things
+link_patterns = (
+ # Find bug reports
+ (re.compile(r"(?:#(\d+))", re.I), r"https://bugzilla.ipfire.org/show_bug.cgi?id=\1"),
+
+ # Email Addresses
+ (re.compile(r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"), r"mailto:\1"),
+
+ # CVE Numbers
+ (re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)"), r"http://cve.mitre.org/cgi-bin/cvename.cgi?name=\1"),
+)
class Blog(misc.Object):
def _get_post(self, query, *args):
ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s", limit)
def search(self, query, limit=None):
+ query = self._parse_search_query(query)
+
return self._get_posts("SELECT blog.* FROM blog \
LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
WHERE search_index.document @@ to_tsquery('english', %s) \
ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
LIMIT %s", query, query, limit)
- def create_post(self, title, text, author, tags=[]):
+ def _parse_search_query(self, query):
+ q = []
+ for word in query.split():
+ # Is this lexeme negated?
+ negated = word.startswith("!")
+
+ # Remove any special characters
+ word = re.sub(r"\W+", "", word, flags=re.UNICODE)
+ if not word:
+ continue
+
+ # Restore negation
+ if negated:
+ word = "!%s" % word
+
+ q.append(word)
+
+ return " & ".join(q)
+
+ def create_post(self, title, text, author, tags=[], lang="markdown"):
"""
Creates a new post and returns the resulting Post object
"""
- return self._get_post("INSERT INTO blog(title, slug, text, author_uid, tags) \
- VALUES(%s, %s, %s, %s, %s) RETURNING *", title, self._make_slug(title),
- text, author.uid, list(tags))
+ # Pre-render HTML
+ html = self._render_text(text, lang=lang)
+
+ return self._get_post("INSERT INTO blog(title, slug, text, html, lang, author_uid, tags) \
+ VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", title, self._make_slug(title), text,
+ html, lang, author.uid, list(tags))
def _make_slug(self, s):
# Remove any non-ASCII characters
return slug
+ def _render_text(self, text, lang="markdown"):
+ if lang == "markdown":
+ return markdown2.markdown(text, link_patterns=link_patterns,
+ extras=["footnotes", "link-patterns", "wiki-tables"])
+
+ elif lang == "textile":
+ return textile.textile(text)
+
+ return text
+
def refresh(self):
"""
Needs to be called after a post has been changed
# Title
- def get_title(self):
+ @property
+ def title(self):
return self.data.title
- def set_title(self, title):
- self.db.execute("UPDATE blog SET title = %s \
- WHERE id = %s", title, self.id)
-
- # Update slug if post is not published, yet
- if not self.is_published():
- self.db.execute("UPDATE blog SET slug = %s \
- WHERE id = %s", self.backend.blog._make_slug(title), self.id)
-
- title = property(get_title, set_title)
-
@property
def slug(self):
return self.data.slug
- # XXX needs caching
- @property
+ @lazy_property
def author(self):
if self.data.author_uid:
return self.backend.accounts.get_by_uid(self.data.author_uid)
def created_at(self):
return self.data.created_at
+ @property
+ def lang(self):
+ return self.data.lang
+
# Published?
@property
"""
return self.published_at and self.published_at <= datetime.datetime.now()
- def publish(self):
+ def publish(self, when=None):
if self.is_published():
return
- self.db.execute("UPDATE blog SET published_at = NOW() \
- WHERE id = %s", self.id)
+ self.db.execute("UPDATE blog SET published_at = COALESCE(%s, CURRENT_TIMESTAMP) \
+ WHERE id = %s", when, self.id)
# Update search indices
self.backend.blog.refresh()
def updated_at(self):
return self.data.updated_at
- def updated(self):
- self.db.execute("UPDATE blog SET updated_at = NOW() \
- WHERE id = %s", self.id)
-
- # Update search indices
- self.backend.blog.refresh()
-
# Text
- def get_text(self):
+ @property
+ def text(self):
return self.data.text
- def set_text(self, text):
- self.db.execute("UPDATE blog SET text = %s \
- WHERE id = %s", text, self.id)
-
- text = property(get_text, set_text)
-
# HTML
- @property
+ @lazy_property
def html(self):
"""
Returns this post as rendered HTML
"""
- return self.data.html or textile.textile(self.text.decode("utf-8"))
+ return self.data.html or self.backend.blog._render_text(self.text, lang=self.lang)
# Tags
- def get_tags(self):
+ @property
+ def tags(self):
return self.data.tags
- def set_tags(self, tags):
- self.db.execute("UPDATE blog SET tags = %s \
- WHERE id = %s", list(tags), self.id)
-
- tags = property(get_tags, set_tags)
+ # Link
@property
def link(self):
return self.data.link
- # XXX needs caching
- @property
+ @lazy_property
def release(self):
return self.backend.releases._get_release("SELECT * FROM releases \
WHERE published IS NOT NULL AND published <= NOW() AND blog_id = %s", self.id)
def is_editable(self, editor):
# Authors can edit their own posts
return self.author == editor
+
+ def update(self, title, text, tags=[]):
+ """
+ Called to update the content of this post
+ """
+ # Update slug when post isn't published yet
+ slug = self.backend.blog._make_slug(title) \
+ if not self.is_published() and not self.title == title else self.slug
+
+ # Render and cache HTML
+ html = self.backend.blog._render_text(text, lang=self.lang)
+
+ self.db.execute("UPDATE blog SET title = %s, slug = %s, text = %s, html = %s, \
+ tags = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
+ title, slug, text, html, list(tags), self.id)
+
+ # Update cache
+ self.data.update({
+ "title" : title,
+ "slug" : slug,
+ "text" : text,
+ "html" : html,
+ "tags" : tags,
+ })
+
+ # Update search index if post is published
+ if self.is_published():
+ self.backend.blog.refresh()
+
+ def delete(self):
+ self.db.execute("DELETE FROM blog WHERE id = %s", self.id)
+
+ # Update search indices
+ self.backend.blog.refresh()