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