Merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/snappy-docs-import into lp:developer-ubuntu-com
- snappy-docs-import
- Merge into stable
Status: | Merged |
---|---|
Approved by: | Daniel Holbach |
Approved revision: | 162 |
Merged at revision: | 126 |
Proposed branch: | lp:~developer-ubuntu-com-dev/developer-ubuntu-com/snappy-docs-import |
Merge into: | lp:developer-ubuntu-com |
Diff against target: |
507 lines (+422/-2) 6 files modified
developer_portal/admin.py (+18/-0) developer_portal/management/commands/import-external-docs-branches.py (+309/-0) developer_portal/migrations/0003_add_external_docs_branches.py (+62/-0) developer_portal/models.py (+20/-1) locale/developer_portal.pot (+11/-1) requirements.txt (+2/-0) |
To merge this branch: | bzr merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/snappy-docs-import |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Daniel Holbach (community) | Approve | ||
David Callé | Pending | ||
Review via email: mp+265004@code.launchpad.net |
Commit message
Add functionality to import external markdown docs (like snappy and snapcraft) automatically into developer site.
Description of the change
Daniel Holbach (dholbach) wrote : | # |
Daniel Holbach (dholbach) wrote : | # |
David Callé noticed that SnappyMarkdownF
Daniel Holbach (dholbach) wrote : | # |
<davidcalle> dholbach, well, I'm not sure about the "Snappy" name for landing pages. I haven't found anything else. Oh, maybe the script itself should be renamed to not have snappy in its name.
David Callé (davidc3) wrote : | # |
Added a few changes.
Lots of testing done with a dummy branch on the whole publication, page removal, doc name changes, etc. processes. Everything works great.
Daniel Holbach (dholbach) wrote : | # |
<dholbach> davidcalle, I read the code again and I think I'm fine with it
parts of it could be a bit simpler I feel, but for now I think it's good enough
Daniel Holbach (dholbach) wrote : | # |
<davidcalle> dholbach, same for me. Let's merge it :) I'll try the staging deploy this afternoon.
Preview Diff
1 | === modified file 'developer_portal/admin.py' |
2 | --- developer_portal/admin.py 2014-12-01 15:11:25 +0000 |
3 | +++ developer_portal/admin.py 2015-08-11 12:06:30 +0000 |
4 | @@ -3,10 +3,20 @@ |
5 | from reversion.models import Revision, Version |
6 | from reversion.admin import VersionAdmin |
7 | |
8 | +from .models import ExternalDocsBranch |
9 | +from django.core.management import call_command |
10 | + |
11 | __all__ = ( |
12 | ) |
13 | |
14 | |
15 | +def import_selected_external_docs_branches(modeladmin, request, queryset): |
16 | + for branch in queryset: |
17 | + call_command('import-external-docs-branches', branch.docs_namespace) |
18 | + import_selected_external_docs_branches.short_description = \ |
19 | + "Import selected branches" |
20 | + |
21 | + |
22 | class RevisionAdmin(admin.ModelAdmin): |
23 | list_display = ('date_created', 'user', 'comment') |
24 | list_display_links = ('date_created', ) |
25 | @@ -15,6 +25,7 @@ |
26 | |
27 | admin.site.register(Revision, RevisionAdmin) |
28 | |
29 | + |
30 | class VersionAdmin(admin.ModelAdmin): |
31 | list_display = ('content_type', 'object_id') |
32 | list_display_links = ('object_id', ) |
33 | @@ -22,3 +33,10 @@ |
34 | |
35 | admin.site.register(Version, VersionAdmin) |
36 | |
37 | + |
38 | +class ExternalDocsBranchAdmin(admin.ModelAdmin): |
39 | + list_display = ('lp_origin', 'docs_namespace') |
40 | + list_filter = ('lp_origin', 'docs_namespace') |
41 | + actions = [import_selected_external_docs_branches] |
42 | + |
43 | +admin.site.register(ExternalDocsBranch, ExternalDocsBranchAdmin) |
44 | |
45 | === added file 'developer_portal/management/commands/import-external-docs-branches.py' |
46 | --- developer_portal/management/commands/import-external-docs-branches.py 1970-01-01 00:00:00 +0000 |
47 | +++ developer_portal/management/commands/import-external-docs-branches.py 2015-08-11 12:06:30 +0000 |
48 | @@ -0,0 +1,309 @@ |
49 | +from django.core.management.base import BaseCommand |
50 | + |
51 | +from cms.api import create_page, add_plugin |
52 | +from cms.models import Page, Title |
53 | +from cms.utils import page_resolver |
54 | + |
55 | +from bs4 import BeautifulSoup |
56 | +import codecs |
57 | +import glob |
58 | +import logging |
59 | +import markdown |
60 | +import os |
61 | +import re |
62 | +import shutil |
63 | +import subprocess |
64 | +import sys |
65 | +import tempfile |
66 | + |
67 | +from developer_portal.models import ExternalDocsBranch |
68 | + |
69 | +DOCS_DIRNAME = 'docs' |
70 | + |
71 | + |
72 | +class MarkdownFile: |
73 | + html = None |
74 | + |
75 | + def __init__(self, fn, docs_namespace, slug_override=None): |
76 | + self.fn = fn |
77 | + self.docs_namespace = docs_namespace |
78 | + if slug_override: |
79 | + self.slug = slug_override |
80 | + else: |
81 | + self.slug = slugify(self.fn) |
82 | + self.full_url = os.path.join(self.docs_namespace, self.slug) |
83 | + with codecs.open(self.fn, 'r', encoding='utf-8') as f: |
84 | + self.html = markdown.markdown( |
85 | + f.read(), |
86 | + output_format="html5", |
87 | + extensions=['markdown.extensions.tables']) |
88 | + self.release_alias = self._get_release_alias() |
89 | + self.title = self._read_title() |
90 | + self._remove_body_and_html_tags() |
91 | + self._use_developer_site_style() |
92 | + |
93 | + def _get_release_alias(self): |
94 | + alias = re.findall(r'/tmp/tmp\S+?/(\S+?)/%s/\S+?' % DOCS_DIRNAME, |
95 | + self.fn) |
96 | + return alias[0] |
97 | + |
98 | + def _read_title(self): |
99 | + soup = BeautifulSoup(self.html, 'html5lib') |
100 | + if soup.title: |
101 | + return soup.title.text |
102 | + if soup.h1: |
103 | + return soup.h1.text |
104 | + return slugify(self.fn).replace('-', ' ').title() |
105 | + |
106 | + def _remove_body_and_html_tags(self): |
107 | + self.html = re.sub(r"<html>\n\s<body>\n", "", self.html, |
108 | + flags=re.MULTILINE) |
109 | + self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html, |
110 | + flags=re.MULTILINE) |
111 | + |
112 | + def _use_developer_site_style(self): |
113 | + begin = (u"<div class=\"row no-border\">" |
114 | + "\n<div class=\"eight-col\">\n") |
115 | + end = u"</div>\n</div>" |
116 | + self.html = begin + self.html + end |
117 | + self.html = self.html.replace( |
118 | + "<pre><code>", |
119 | + "</div><div class=\"twelve-col\"><pre><code>") |
120 | + self.html = self.html.replace( |
121 | + "</code></pre>", |
122 | + "</code></pre></div><div class=\"eight-col\">") |
123 | + |
124 | + def replace_links(self, titles): |
125 | + for title in titles: |
126 | + url = u"/snappy/guides/%s/%s" % ( |
127 | + self.release_alias, slugify(title)) |
128 | + link = u"<a href=\"%s\">%s</a>" % (url, titles[title]) |
129 | + self.html = self.html.replace(os.path.basename(title), link) |
130 | + |
131 | + def publish(self): |
132 | + '''Publishes pages in their branch alias namespace.''' |
133 | + page = get_or_create_page( |
134 | + self.title, full_url=self.full_url, menu_title=self.title, |
135 | + html=self.html) |
136 | + page.publish('en') |
137 | + |
138 | + |
139 | +class SnappyMarkdownFile(MarkdownFile): |
140 | + def __init__(self, fn, docs_namespace): |
141 | + MarkdownFile.__init__(self, fn, docs_namespace) |
142 | + self._make_snappy_mods() |
143 | + |
144 | + def _make_snappy_mods(self): |
145 | + # Make sure the reader knows which documentation she is browsing |
146 | + if self.release_alias != 'current': |
147 | + before = (u"<div class=\"row no-border\">\n" |
148 | + "<div class=\"eight-col\">\n") |
149 | + after = (u"<div class=\"row no-border\">\n" |
150 | + "<div class=\"box pull-three three-col\">" |
151 | + "<p>You are browsing the Snappy <code>%s</code> " |
152 | + "documentation.</p>" |
153 | + "<p><a href=\"/snappy/guides/current/%s\">" |
154 | + "Back to the latest stable release ›" |
155 | + "</a></p></div>\n" |
156 | + "<div class=\"eight-col\">\n") % (self.release_alias, |
157 | + self.slug, ) |
158 | + self.html = self.html.replace(before, after) |
159 | + |
160 | + def publish(self): |
161 | + if self.release_alias == "current": |
162 | + # Add a guides/<page> redirect to guides/current/<page> |
163 | + page = get_or_create_page( |
164 | + self.title, full_url=self.full_url.replace('/current', ''), |
165 | + redirect="/snappy/guides/current/%s" % (self.slug)) |
166 | + page.publish('en') |
167 | + else: |
168 | + self.title += " (%s)" % (self.release_alias,) |
169 | + MarkdownFile.publish(self) |
170 | + |
171 | + |
172 | +def slugify(filename): |
173 | + return os.path.basename(filename).replace('.md', '') |
174 | + |
175 | + |
176 | +def get_branch_from_lp(origin, alias): |
177 | + return subprocess.call([ |
178 | + 'bzr', 'checkout', '--lightweight', origin, alias]) |
179 | + |
180 | + |
181 | +class LocalBranch: |
182 | + titles = {} |
183 | + |
184 | + def __init__(self, dirname, external_branch): |
185 | + self.dirname = dirname |
186 | + self.docs_path = os.path.join(self.dirname, DOCS_DIRNAME) |
187 | + self.doc_fns = glob.glob(self.docs_path+'/*.md') |
188 | + self.md_files = [] |
189 | + self.external_branch = external_branch |
190 | + self.docs_namespace = self.external_branch.docs_namespace |
191 | + self.release_alias = os.path.basename(self.docs_namespace) |
192 | + self.index_doc_title = self.release_alias.title() |
193 | + self.index_doc = self.external_branch.index_doc |
194 | + self.markdown_class = MarkdownFile |
195 | + |
196 | + def import_markdown(self): |
197 | + for doc_fn in self.doc_fns: |
198 | + if self.index_doc and os.path.basename(doc_fn) == self.index_doc: |
199 | + md_file = self.markdown_class( |
200 | + doc_fn, |
201 | + os.path.dirname(self.docs_namespace), |
202 | + slug_override=os.path.basename(self.docs_namespace)) |
203 | + self.md_files.insert(0, md_file) |
204 | + else: |
205 | + md_file = self.markdown_class(doc_fn, self.docs_namespace) |
206 | + self.md_files += [md_file] |
207 | + self.titles[md_file.fn] = md_file.title |
208 | + if not self.index_doc: |
209 | + self._create_fake_index_doc() |
210 | + |
211 | + def remove_old_pages(self): |
212 | + imported_page_urls = set([md_file.full_url |
213 | + for md_file in self.md_files]) |
214 | + index_doc = page_resolver.get_page_queryset_from_path( |
215 | + self.docs_namespace)[0] |
216 | + # All pages in this namespace currently in the database |
217 | + db_pages = index_doc.get_descendants().all() |
218 | + delete_pages = [] |
219 | + for db_page in db_pages: |
220 | + still_relevant = False |
221 | + for url in imported_page_urls: |
222 | + if url in db_page.get_absolute_url(): |
223 | + still_relevant = True |
224 | + break |
225 | + # At this point we know that there's no match and the page |
226 | + # can be deleted. |
227 | + if not still_relevant: |
228 | + delete_pages += [db_page.id] |
229 | + # Only remove pages created by a script! |
230 | + Page.objects.filter(id__in=delete_pages, created_by="script").delete() |
231 | + |
232 | + def publish(self): |
233 | + for md_file in self.md_files: |
234 | + md_file.publish() |
235 | + |
236 | + def _create_fake_index_doc(self): |
237 | + '''Creates a fake index page at the top of the branches |
238 | + docs namespace.''' |
239 | + |
240 | + if self.docs_namespace == "current": |
241 | + redirect = "/snappy/guides" |
242 | + else: |
243 | + redirect = None |
244 | + |
245 | + in_navigation = False |
246 | + menu_title = None |
247 | + list_pages = "" |
248 | + for page in self.md_files: |
249 | + list_pages += "<li><a href=\"%s\">%s</a></li>" \ |
250 | + % (os.path.basename(page.full_url), page.title) |
251 | + landing = ( |
252 | + u"<div class=\"row\"><div class=\"eight-col\">\n" |
253 | + "<p>This section contains documentation for the " |
254 | + "<code>%s</code> Snappy branch.</p>" |
255 | + "<p><ul class=\"list-ubuntu\">%s</ul></p>\n" |
256 | + "<p>Auto-imported from <a " |
257 | + "href=\"https://code.launchpad.net/snappy\">%s</a>.</p>\n" |
258 | + "</div></div>") % (self.release_alias, list_pages, |
259 | + self.external_branch.lp_origin) |
260 | + new_release_page = get_or_create_page( |
261 | + self.index_doc_title, full_url=self.docs_namespace, |
262 | + in_navigation=in_navigation, redirect=redirect, html=landing, |
263 | + menu_title=menu_title) |
264 | + new_release_page.publish('en') |
265 | + |
266 | + |
267 | +class SnappyLocalBranch(LocalBranch): |
268 | + def __init__(self, dirname, external_branch): |
269 | + LocalBranch.__init__(self, dirname, external_branch) |
270 | + self.markdown_class = SnappyMarkdownFile |
271 | + self.index_doc_title = 'Snappy documentation' |
272 | + if self.release_alias != 'current': |
273 | + self.index_doc_title += ' (%s)' % self.release_alias |
274 | + |
275 | + def import_markdown(self): |
276 | + LocalBranch.import_markdown(self) |
277 | + for md_file in self.md_files: |
278 | + md_file.replace_links(self.titles) |
279 | + |
280 | + |
281 | +def get_or_create_page(title, full_url, menu_title=None, |
282 | + in_navigation=True, redirect=None, html=None): |
283 | + # First check if pages already exist. |
284 | + pages = Title.objects.select_related('page').filter(path__regex=full_url) |
285 | + if pages: |
286 | + page = pages[0].page |
287 | + page.title = title |
288 | + page.publisher_is_draft = True |
289 | + page.menu_title = menu_title |
290 | + page.in_navigation = in_navigation |
291 | + page.redirect = redirect |
292 | + if html: |
293 | + # We create the page, so we know there's just one placeholder |
294 | + placeholder = page.placeholders.all()[0] |
295 | + if placeholder.get_plugins(): |
296 | + plugin = placeholder.get_plugins()[0].get_plugin_instance()[0] |
297 | + plugin.body = html |
298 | + plugin.save() |
299 | + else: |
300 | + add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) |
301 | + else: |
302 | + parent_pages = Title.objects.select_related('page').filter( |
303 | + path__regex=os.path.dirname(full_url)) |
304 | + if not parent_pages: |
305 | + print('Parent %s not found.' % os.path.dirname(full_url)) |
306 | + sys.exit(1) |
307 | + parent = parent_pages[0].page |
308 | + |
309 | + slug = os.path.basename(full_url) |
310 | + page = create_page( |
311 | + title, "default.html", "en", slug=slug, parent=parent, |
312 | + menu_title=menu_title, in_navigation=in_navigation, |
313 | + position="last-child", redirect=redirect) |
314 | + if html: |
315 | + placeholder = page.placeholders.get() |
316 | + add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) |
317 | + return page |
318 | + |
319 | + |
320 | +def import_branches(selection): |
321 | + if not ExternalDocsBranch.objects.count(): |
322 | + logging.error('No branches registered in the ' |
323 | + 'ExternalDocsBranch table yet.') |
324 | + return |
325 | + tempdir = tempfile.mkdtemp() |
326 | + for branch in ExternalDocsBranch.objects.filter( |
327 | + docs_namespace__regex=selection): |
328 | + checkout_location = os.path.join( |
329 | + tempdir, os.path.basename(branch.docs_namespace)) |
330 | + if get_branch_from_lp(branch.lp_origin, checkout_location) != 0: |
331 | + logging.error( |
332 | + 'Could not check out branch "%s".' % branch.lp_origin) |
333 | + shutil.rmtree(checkout_location) |
334 | + break |
335 | + if branch.lp_origin.startswith('lp:snappy'): |
336 | + local_branch = SnappyLocalBranch(checkout_location, branch) |
337 | + else: |
338 | + local_branch = LocalBranch(checkout_location, branch) |
339 | + local_branch.import_markdown() |
340 | + local_branch.publish() |
341 | + local_branch.remove_old_pages() |
342 | + shutil.rmtree(tempdir) |
343 | + |
344 | + |
345 | +class Command(BaseCommand): |
346 | + help = "Import external branches for documentation." |
347 | + |
348 | + def handle(*args, **options): |
349 | + logging.basicConfig( |
350 | + level=logging.ERROR, |
351 | + format='%(asctime)s %(levelname)-8s %(message)s', |
352 | + datefmt='%F %T') |
353 | + if len(args) < 2 or args[1] == "all": |
354 | + selection = '.*' |
355 | + else: |
356 | + selection = args[1] |
357 | + import_branches(selection) |
358 | |
359 | === added file 'developer_portal/migrations/0003_add_external_docs_branches.py' |
360 | --- developer_portal/migrations/0003_add_external_docs_branches.py 1970-01-01 00:00:00 +0000 |
361 | +++ developer_portal/migrations/0003_add_external_docs_branches.py 2015-08-11 12:06:30 +0000 |
362 | @@ -0,0 +1,62 @@ |
363 | +# -*- coding: utf-8 -*- |
364 | +from south.utils import datetime_utils as datetime |
365 | +from south.db import db |
366 | +from south.v2 import SchemaMigration |
367 | +from django.db import models |
368 | + |
369 | + |
370 | +class Migration(SchemaMigration): |
371 | + |
372 | + def forwards(self, orm): |
373 | + # Adding model 'ExternalDocsBranch' |
374 | + db.create_table(u'developer_portal_externaldocsbranch', ( |
375 | + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
376 | + ('lp_origin', self.gf('django.db.models.fields.CharField')(max_length=200)), |
377 | + ('docs_namespace', self.gf('django.db.models.fields.CharField')(max_length=120)), |
378 | + ('index_doc', self.gf('django.db.models.fields.CharField')(max_length=120, blank=True)), |
379 | + )) |
380 | + db.send_create_signal(u'developer_portal', ['ExternalDocsBranch']) |
381 | + |
382 | + |
383 | + def backwards(self, orm): |
384 | + # Deleting model 'ExternalDocsBranch' |
385 | + db.delete_table(u'developer_portal_externaldocsbranch') |
386 | + |
387 | + |
388 | + models = { |
389 | + 'cms.cmsplugin': { |
390 | + 'Meta': {'object_name': 'CMSPlugin'}, |
391 | + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
392 | + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
393 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
394 | + 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), |
395 | + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), |
396 | + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), |
397 | + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}), |
398 | + 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}), |
399 | + 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), |
400 | + 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), |
401 | + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), |
402 | + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) |
403 | + }, |
404 | + 'cms.placeholder': { |
405 | + 'Meta': {'object_name': 'Placeholder'}, |
406 | + 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), |
407 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
408 | + 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) |
409 | + }, |
410 | + u'developer_portal.externaldocsbranch': { |
411 | + 'Meta': {'object_name': 'ExternalDocsBranch'}, |
412 | + 'docs_namespace': ('django.db.models.fields.CharField', [], {'max_length': '120'}), |
413 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
414 | + 'index_doc': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}), |
415 | + 'lp_origin': ('django.db.models.fields.CharField', [], {'max_length': '200'}) |
416 | + }, |
417 | + u'developer_portal.rawhtml': { |
418 | + 'Meta': {'object_name': 'RawHtml'}, |
419 | + 'body': ('django.db.models.fields.TextField', [], {}), |
420 | + u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}) |
421 | + } |
422 | + } |
423 | + |
424 | + complete_apps = ['developer_portal'] |
425 | \ No newline at end of file |
426 | |
427 | === modified file 'developer_portal/models.py' |
428 | --- developer_portal/models.py 2015-01-19 16:29:47 +0000 |
429 | +++ developer_portal/models.py 2015-08-11 12:06:30 +0000 |
430 | @@ -1,10 +1,13 @@ |
431 | +from django.db import models |
432 | +from django.utils.translation import ugettext_lazy as _ |
433 | |
434 | from cms.models import CMSPlugin |
435 | from djangocms_text_ckeditor.html import extract_images |
436 | from djangocms_text_ckeditor.models import AbstractText |
437 | |
438 | + |
439 | class RawHtml(AbstractText): |
440 | - |
441 | + |
442 | class Meta: |
443 | abstract = False |
444 | |
445 | @@ -13,3 +16,19 @@ |
446 | body = extract_images(body, self) |
447 | self.body = body |
448 | AbstractText.save(self, *args, **kwargs) |
449 | + |
450 | + |
451 | +class ExternalDocsBranch(models.Model): |
452 | + lp_origin = models.CharField( |
453 | + max_length=200, |
454 | + help_text=_('Launchpad branch location, ie: lp:snappy/15.04')) |
455 | + docs_namespace = models.CharField( |
456 | + max_length=120, |
457 | + help_text=_('Path alias we want to use for the docs, ' |
458 | + 'ie "snappy/guides/15.04" or ' |
459 | + '"snappy/guides/latest", etc.')) |
460 | + index_doc = models.CharField( |
461 | + max_length=120, |
462 | + help_text=_('File name of doc to be used as index document, ' |
463 | + 'ie "intro.md"'), |
464 | + blank=True) |
465 | |
466 | === modified file 'locale/developer_portal.pot' |
467 | --- locale/developer_portal.pot 2015-05-11 20:00:59 +0000 |
468 | +++ locale/developer_portal.pot 2015-08-11 12:06:30 +0000 |
469 | @@ -8,7 +8,7 @@ |
470 | msgstr "" |
471 | "Project-Id-Version: PACKAGE VERSION\n" |
472 | "Report-Msgid-Bugs-To: \n" |
473 | -"POT-Creation-Date: 2015-04-24 09:17+0000\n" |
474 | +"POT-Creation-Date: 2015-07-27 14:41+0000\n" |
475 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
476 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
477 | "Language-Team: LANGUAGE <LL@li.org>\n" |
478 | @@ -21,6 +21,16 @@ |
479 | msgid "Raw HTML" |
480 | msgstr "" |
481 | |
482 | +#: developer_portal/models.py:24 |
483 | +msgid "Launchpad branch location, ie: lp:snappy/15.04" |
484 | +msgstr "" |
485 | + |
486 | +#: developer_portal/models.py:27 |
487 | +msgid "" |
488 | +"Path alias we want to use for the docs, ie \"snappy/guides/15.04\" or " |
489 | +"\"snappy/guides/latest\", etc." |
490 | +msgstr "" |
491 | + |
492 | #: developer_portal/settings.py:187 developer_portal/settings.py:195 |
493 | msgid "English" |
494 | msgstr "" |
495 | |
496 | === modified file 'requirements.txt' |
497 | --- requirements.txt 2015-02-26 15:53:30 +0000 |
498 | +++ requirements.txt 2015-08-11 12:06:30 +0000 |
499 | @@ -1,6 +1,8 @@ |
500 | Django==1.6.8 |
501 | +Markdown==2.6.2 |
502 | South==1.0.1 |
503 | Pillow==2.6.1 |
504 | +beautifulsoup4==4.4.0 |
505 | cmsplugin-zinnia==0.6 |
506 | dj-database-url==0.3.0 |
507 | django-admin-enhancer==0.1.3.1 |
This is still WIP.