]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/wiki.py
wiki: Make sidebar search on older versions of Python
[ipfire.org.git] / src / backend / wiki.py
1 #!/usr/bin/python3
2
3 import logging
4 import os.path
5 import re
6
7 from . import misc
8 from . import util
9 from .decorators import *
10
11 class Wiki(misc.Object):
12 def _get_pages(self, query, *args):
13 res = self.db.query(query, *args)
14
15 for row in res:
16 yield Page(self.backend, row.id, data=row)
17
18 def _get_page(self, query, *args):
19 res = self.db.get(query, *args)
20
21 if res:
22 return Page(self.backend, res.id, data=res)
23
24 def get_page_title(self, page, default=None):
25 doc = self.get_page(page)
26 if doc:
27 return doc.title
28
29 return default or os.path.basename(page)
30
31 def get_page(self, page, revision=None):
32 page = Page.sanitise_page_name(page)
33 assert page
34
35 if revision:
36 return self._get_page("SELECT * FROM wiki WHERE page = %s \
37 AND timestamp = %s", page, revision)
38 else:
39 return self._get_page("SELECT * FROM wiki WHERE page = %s \
40 ORDER BY timestamp DESC LIMIT 1", page)
41
42 def get_recent_changes(self, limit=None):
43 return self._get_pages("SELECT * FROM wiki \
44 WHERE timestamp >= NOW() - INTERVAL '4 weeks' \
45 ORDER BY timestamp DESC LIMIT %s", limit)
46
47 def create_page(self, page, author, content, changes=None, address=None):
48 page = Page.sanitise_page_name(page)
49
50 return self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
51 VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
52
53 def delete_page(self, page, author, **kwargs):
54 # Do nothing if the page does not exist
55 if not self.get_page(page):
56 return
57
58 # Just creates a blank last version of the page
59 self.create_page(page, author=author, content=None, **kwargs)
60
61 def make_breadcrumbs(self, url):
62 # Split and strip all empty elements (double slashes)
63 parts = list(e for e in url.split("/") if e)
64
65 ret = []
66 for part in ("/".join(parts[:i]) for i in range(1, len(parts))):
67 ret.append(("/%s" % part, self.get_page_title(part, os.path.basename(part))))
68
69 return ret
70
71 def search(self, query, limit=None):
72 query = util.parse_search_query(query)
73
74 res = self._get_pages("SELECT wiki.* FROM wiki_search_index search_index \
75 LEFT JOIN wiki ON search_index.wiki_id = wiki.id \
76 WHERE search_index.document @@ to_tsquery('english', %s) \
77 ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
78 LIMIT %s", query, query, limit)
79
80 return list(res)
81
82 def refresh(self):
83 """
84 Needs to be called after a page has been changed
85 """
86 self.db.execute("REFRESH MATERIALIZED VIEW wiki_search_index")
87
88
89 class Page(misc.Object):
90 def init(self, id, data=None):
91 self.id = id
92 self.data = data
93
94 def __lt__(self, other):
95 if isinstance(other, self.__class__):
96 if self.page == other.page:
97 return self.timestamp < other.timestamp
98
99 return self.page < other.page
100
101 @staticmethod
102 def sanitise_page_name(page):
103 if not page:
104 return "/"
105
106 # Make sure that the page name does NOT end with a /
107 if page.endswith("/"):
108 page = page[:-1]
109
110 # Make sure the page name starts with a /
111 if not page.startswith("/"):
112 page = "/%s" % page
113
114 # Remove any double slashes
115 page = page.replace("//", "/")
116
117 return page
118
119 @property
120 def url(self):
121 return self.page
122
123 @property
124 def page(self):
125 return self.data.page
126
127 @property
128 def title(self):
129 return self._title or self.page[1:]
130
131 @property
132 def _title(self):
133 if not self.markdown:
134 return
135
136 # Find first H1 headline in markdown
137 markdown = self.markdown.splitlines()
138
139 m = re.match(r"^# (.*)( #)?$", markdown[0])
140 if m:
141 return m.group(1)
142
143 @lazy_property
144 def author(self):
145 if self.data.author_uid:
146 return self.backend.accounts.get_by_uid(self.data.author_uid)
147
148 def _render(self, text):
149 logging.debug("Rendering %s" % self)
150
151 patterns = (
152 (r"\[\[([\w\d\/]+)(?:\|([\w\d\s]+))\]\]", r"/\1", r"\2", None, None),
153 (r"\[\[([\w\d\/\-]+)\]\]", r"/\1", r"\1", self.backend.wiki.get_page_title, r"\1"),
154 )
155
156 for pattern, link, title, repl, args in patterns:
157 replacements = []
158
159 for match in re.finditer(pattern, text):
160 l = match.expand(link)
161 t = match.expand(title)
162
163 if callable(repl):
164 t = repl(match.expand(args)) or t
165
166 replacements.append((match.span(), t or l, l))
167
168 # Apply all replacements
169 for (start, end), t, l in reversed(replacements):
170 text = text[:start] + "[%s](%s)" % (t, l) + text[end:]
171
172 # Borrow this from the blog
173 return self.backend.blog._render_text(text, lang="markdown")
174
175 @property
176 def markdown(self):
177 return self.data.markdown
178
179 @property
180 def html(self):
181 return self.data.html or self._render(self.markdown)
182
183 @property
184 def timestamp(self):
185 return self.data.timestamp
186
187 def was_deleted(self):
188 return self.markdown is None
189
190 @lazy_property
191 def breadcrumbs(self):
192 return self.backend.wiki.make_breadcrumbs(self.page)
193
194 def get_latest_revision(self):
195 revisions = self.get_revisions()
196
197 # Return first object
198 for rev in revisions:
199 return rev
200
201 def get_revisions(self):
202 return self.backend.wiki._get_pages("SELECT * FROM wiki \
203 WHERE page = %s ORDER BY timestamp DESC", self.page)
204
205 @property
206 def changes(self):
207 return self.data.changes
208
209 # Sidebar
210
211 @lazy_property
212 def sidebar(self):
213 parts = self.page.split("/")
214
215 while parts:
216 sidebar = self.backend.wiki.get_page("%s/sidebar" % os.path.join(parts))
217 if sidebar:
218 return sidebar
219
220 parts.pop()