]>
Commit | Line | Data |
---|---|---|
181d08f3 MT |
1 | #!/usr/bin/python3 |
2 | ||
3 | import logging | |
6ac7e934 | 4 | import os.path |
181d08f3 MT |
5 | import re |
6 | ||
7 | from . import misc | |
9523790a | 8 | from . import util |
181d08f3 MT |
9 | from .decorators import * |
10 | ||
181d08f3 MT |
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 | ||
d398ca08 MT |
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 | ||
6ac7e934 MT |
24 | def get_page_title(self, page, default=None): |
25 | doc = self.get_page(page) | |
26 | if doc: | |
27 | return doc.title | |
28 | ||
0b62a7f9 | 29 | return default or os.path.basename(page) |
6ac7e934 | 30 | |
181d08f3 MT |
31 | def get_page(self, page, revision=None): |
32 | page = Page.sanitise_page_name(page) | |
33 | assert page | |
34 | ||
35 | if revision: | |
d398ca08 | 36 | return self._get_page("SELECT * FROM wiki WHERE page = %s \ |
181d08f3 MT |
37 | AND timestamp = %s", page, revision) |
38 | else: | |
d398ca08 | 39 | return self._get_page("SELECT * FROM wiki WHERE page = %s \ |
181d08f3 MT |
40 | ORDER BY timestamp DESC LIMIT 1", page) |
41 | ||
f9db574a | 42 | def get_recent_changes(self, limit=None): |
181d08f3 | 43 | return self._get_pages("SELECT * FROM wiki \ |
f9db574a MT |
44 | WHERE timestamp >= NOW() - INTERVAL '4 weeks' \ |
45 | ORDER BY timestamp DESC LIMIT %s", limit) | |
181d08f3 | 46 | |
495e9dc4 | 47 | def create_page(self, page, author, content, changes=None, address=None): |
181d08f3 MT |
48 | page = Page.sanitise_page_name(page) |
49 | ||
495e9dc4 | 50 | return self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \ |
df01767e | 51 | VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address) |
181d08f3 | 52 | |
495e9dc4 | 53 | def delete_page(self, page, author, **kwargs): |
181d08f3 MT |
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 | |
495e9dc4 | 59 | self.create_page(page, author=author, content=None, **kwargs) |
181d08f3 | 60 | |
3168788e MT |
61 | def make_breadcrumbs(self, url): |
62 | # Split and strip all empty elements (double slashes) | |
181d08f3 MT |
63 | parts = list(e for e in url.split("/") if e) |
64 | ||
3168788e | 65 | ret = [] |
b1bf7d48 | 66 | for part in ("/".join(parts[:i]) for i in range(1, len(parts))): |
3168788e | 67 | ret.append(("/%s" % part, self.get_page_title(part, os.path.basename(part)))) |
181d08f3 | 68 | |
3168788e | 69 | return ret |
181d08f3 | 70 | |
9523790a MT |
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 | ||
181d08f3 MT |
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): | |
db8448d9 | 121 | return self.page |
181d08f3 MT |
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 | ||
3b05ef6e MT |
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 | ||
181d08f3 MT |
148 | def _render(self, text): |
149 | logging.debug("Rendering %s" % self) | |
150 | ||
574794da MT |
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 | ||
045ea3db MT |
172 | # Borrow this from the blog |
173 | return self.backend.blog._render_text(text, lang="markdown") | |
181d08f3 MT |
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): | |
7d699684 MT |
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) | |
091ac36b | 204 | |
d398ca08 MT |
205 | @property |
206 | def changes(self): | |
207 | return self.data.changes | |
208 | ||
091ac36b MT |
209 | # Sidebar |
210 | ||
211 | @lazy_property | |
212 | def sidebar(self): | |
213 | parts = self.page.split("/") | |
214 | ||
215 | while parts: | |
528340a8 | 216 | sidebar = self.backend.wiki.get_page("%s/sidebar" % os.path.join(parts)) |
091ac36b MT |
217 | if sidebar: |
218 | return sidebar | |
219 | ||
220 | parts.pop() |