Merge lp:~dholbach/developer-ubuntu-com/rework-importer into lp:~developer-ubuntu-com-dev/developer-ubuntu-com/django-1.8-cms-2.3

Proposed by Daniel Holbach
Status: Superseded
Proposed branch: lp:~dholbach/developer-ubuntu-com/rework-importer
Merge into: lp:~developer-ubuntu-com-dev/developer-ubuntu-com/django-1.8-cms-2.3
Diff against target: 1625 lines (+953/-399)
28 files modified
TODO (+13/-0)
developer_portal/admin.py (+1/-18)
developer_portal/migrations/0001_initial.py (+0/-9)
developer_portal/models.py (+0/-21)
developer_portal/settings.py (+2/-0)
locale/de.po (+2/-2)
locale/en_GB.po (+2/-2)
locale/es.po (+2/-2)
locale/ug.po (+2/-2)
locale/zh_CN.po (+2/-2)
md_importer/admin.py (+36/-0)
md_importer/management/commands/import-external-docs-branches.py (+35/-335)
md_importer/management/importer/article.py (+118/-0)
md_importer/management/importer/publish.py (+64/-0)
md_importer/management/importer/repo.py (+168/-0)
md_importer/management/importer/source.py (+60/-0)
md_importer/migrations/0001_initial.py (+46/-0)
md_importer/models.py (+54/-0)
md_importer/tests/__init__.py (+8/-0)
md_importer/tests/data/link-test/file1.md (+5/-0)
md_importer/tests/data/link-test/file2.md (+3/-0)
md_importer/tests/test_branch_fetch.py (+42/-0)
md_importer/tests/test_branch_import.py (+67/-0)
md_importer/tests/test_link_rewrite.py (+44/-0)
md_importer/tests/test_snappy_import.py (+70/-0)
md_importer/tests/test_utils.py (+33/-0)
md_importer/tests/utils.py (+67/-0)
requirements.txt (+7/-6)
To merge this branch: bzr merge lp:~dholbach/developer-ubuntu-com/rework-importer
Reviewer Review Type Date Requested Status
Ubuntu App Developer site developers Pending
Review via email: mp+281183@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Holbach (dholbach) wrote :

sqlite> select * from developer_portal_externaldocsbranch;
1|https://github.com/ubuntu-core/snapcraft.git|1|master|
2|https://github.com/ubuntu-core/snapcraft.git|1|1.x|
3|https://github.com/ubuntu-core/snappy.git|1|master|
4|https://github.com/ubuntu-core/snappy.git|1|15.04|
sqlite> select * from developer_portal_externaldocsbranchimportdirective;
1|README.md|snappy/build-apps/devel|1
2|docs|snappy/build-apps/devel|1
3|README.md|snappy/build-apps/|2
4|docs|snappy/build-apps/|2
5|docs|snappy/guides/devel|3
6|docs|snappy/guides/current|4
7|HACKING.md|snappy/build-apps/hacking|1
sqlite>

252. By Daniel Holbach

break up tests into individual files

253. By Daniel Holbach

update TODO

254. By Daniel Holbach

improve create_repo helper function

255. By Daniel Holbach

allow SourceCode._get_branch to use local docs (ie for testing)

256. By Daniel Holbach

first cut at testing local links

257. By Daniel Holbach

fix simple link rewrite functionality and test

258. By Daniel Holbach

move md_importer into its own app

259. By Daniel Holbach

add migration for md_importer

260. By Daniel Holbach

merge lp:~dholbach/developer-ubuntu-com/django-cms-update

261. By Daniel Holbach

make directory structure clearer

262. By Daniel Holbach

adapt to new name of management command

263. By Daniel Holbach

add more readable error message

264. By Daniel Holbach

add more local test data (imported just the docs from snappy's and snapcraft's git master branches)

265. By Daniel Holbach

WIP commit:
- move and update TODO file
- define some global values for the importer centrally
- don't sys.exit() when adding pages to the db
- add debug messages
- break _find_parent() into its own function
- mostly make use of local test data
- drop singleton, use separate test classes instead
- add more assertions in tests

266. By Daniel Holbach

remove debug prints, make Repo class variables actual members

267. By Daniel Holbach

- make pep8 happy, remove unnecessary imports
- use TestLocalBranchImport as base class for almost everything,
  reduces a lot of duplication

268. By Daniel Holbach

remove unnecessary code, fix publishing of pages in tests

269. By Daniel Holbach

fix test_link_rewrite by fixing the URL

270. By Daniel Holbach

make Article class variables actual members

271. By Daniel Holbach

add new test to check links in snapcraft import, move link checking function into TestLocalBranchImport

272. By Daniel Holbach

merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/django-1.8-cms-2.3

273. By Daniel Holbach

allow empty strings in import directives

274. By Daniel Holbach

update migration

275. By Daniel Holbach

check if articles were added below home

276. By Daniel Holbach

remove unnecessary call to set a page's publisher_is_draft bit

277. By Daniel Holbach

give article a .publish() method which gives back the page's public_object

278. By Daniel Holbach

rename home_page to root_page, use cms.test_utils.testcases.CMSTestCase, make sure we use public_object wherever possible

279. By Daniel Holbach

update tests accordingly

280. By Daniel Holbach

disregard anchors, make link checking more flexible (ie not only check for intro.md, but also things like docs/intro.md

281. By Daniel Holbach

make URL work, even if LANG is already part of it

282. By Daniel Holbach

links look like they're working now

283. By Daniel Holbach

add test with a broken link, add convenience function is_local_link, modify tests

284. By Daniel Holbach

import doc fix from https://bugs.launchpad.net/developer-ubuntu-com/+bug/1531200

285. By Daniel Holbach

bug fixed

286. By Daniel Holbach

stop import if local images are found, add tests

287. By Daniel Holbach

update TODO

288. By Daniel Holbach

add test to see if importing the same content twice results in the same number of pages

289. By Daniel Holbach

when replacing links, only update HTML if things actually change, only publish if page is dirty

290. By Daniel Holbach

only update page attributes if they actually change, only update the text plugin if the html actually changes, veryify with djangocms_text_ckeditor.html.clean_html, add a text plugin to a page, even if html is empty

291. By Daniel Holbach

add test to check if running an import twice will update the articles in question

292. By Daniel Holbach

update TODO

293. By Daniel Holbach

add misc tests - forgot to 'bzr add'

294. By Daniel Holbach

make update-mtemplate work again, simplify it

295. By Daniel Holbach

update .pot file

296. By Daniel Holbach

add actual page object to repo.pages

297. By Daniel Holbach

use UTC for ImportedArticle.last_import, simplify ImportedArticle cleanup

298. By Daniel Holbach

use repo instead of branch consistently

299. By Daniel Holbach

break out the process of importing a branch into its own module, add a first simple test for it

300. By Daniel Holbach

fix clean up of imported articles, add a test

301. By Daniel Holbach

update TODO

Unmerged revisions

301. By Daniel Holbach

update TODO

300. By Daniel Holbach

fix clean up of imported articles, add a test

299. By Daniel Holbach

break out the process of importing a branch into its own module, add a first simple test for it

298. By Daniel Holbach

use repo instead of branch consistently

297. By Daniel Holbach

use UTC for ImportedArticle.last_import, simplify ImportedArticle cleanup

296. By Daniel Holbach

add actual page object to repo.pages

295. By Daniel Holbach

update .pot file

294. By Daniel Holbach

make update-mtemplate work again, simplify it

293. By Daniel Holbach

add misc tests - forgot to 'bzr add'

292. By Daniel Holbach

update TODO

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'TODO'
--- TODO 1970-01-01 00:00:00 +0000
+++ TODO 2016-01-06 12:40:31 +0000
@@ -0,0 +1,13 @@
1- fix branch indicator (devel vs. stable, 1.x vs devel, etc.)
2
3- fix links
4- add tests for links
5
6- check if https://bugs.launchpad.net/developer-ubuntu-com/+bug/1531200 works
7
8- import pictures (LP: #1511677)
9- unpublished docs: some articles show up with a blue circle, so
10 unpublished although they're accessible(?)
11- add comment at top of RawHTML plugins, so users know not to edit them,
12 currently blocked on LP: #1523925
13- if article already exists, check if HTML actually changed, if not, don't update.
014
=== modified file 'developer_portal/admin.py'
--- developer_portal/admin.py 2015-11-02 12:17:27 +0000
+++ developer_portal/admin.py 2016-01-06 12:40:31 +0000
@@ -4,22 +4,12 @@
4from reversion.admin import VersionAdmin4from reversion.admin import VersionAdmin
55
6from cms.extensions import TitleExtensionAdmin6from cms.extensions import TitleExtensionAdmin
7from .models import ExternalDocsBranch, SEOExtension7from .models import SEOExtension
8from django.core.management import call_command
98
10__all__ = (9__all__ = (
11)10)
1211
1312
14def import_selected_external_docs_branches(modeladmin, request, queryset):
15 branches = []
16 for branch in queryset:
17 branches.append(branch.docs_namespace)
18 call_command('import-external-docs-branches', *branches)
19 import_selected_external_docs_branches.short_description = \
20 "Import selected branches"
21
22
23class RevisionAdmin(admin.ModelAdmin):13class RevisionAdmin(admin.ModelAdmin):
24 list_display = ('date_created', 'user', 'comment')14 list_display = ('date_created', 'user', 'comment')
25 list_display_links = ('date_created', )15 list_display_links = ('date_created', )
@@ -37,13 +27,6 @@
37admin.site.register(Version, VersionAdmin)27admin.site.register(Version, VersionAdmin)
3828
3929
40class ExternalDocsBranchAdmin(admin.ModelAdmin):
41 list_display = ('lp_origin', 'docs_namespace')
42 list_filter = ('lp_origin', 'docs_namespace')
43 actions = [import_selected_external_docs_branches]
44
45admin.site.register(ExternalDocsBranch, ExternalDocsBranchAdmin)
46
47class SEOExtensionAdmin(TitleExtensionAdmin):30class SEOExtensionAdmin(TitleExtensionAdmin):
48 pass31 pass
4932
5033
=== modified file 'developer_portal/migrations/0001_initial.py'
--- developer_portal/migrations/0001_initial.py 2015-11-17 13:38:11 +0000
+++ developer_portal/migrations/0001_initial.py 2016-01-06 12:40:31 +0000
@@ -11,15 +11,6 @@
1111
12 operations = [12 operations = [
13 migrations.CreateModel(13 migrations.CreateModel(
14 name='ExternalDocsBranch',
15 fields=[
16 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17 ('lp_origin', models.CharField(help_text='External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-core/snappy.git', max_length=200)),
18 ('docs_namespace', models.CharField(help_text='Path alias we want to use for the docs, ie "snappy/guides/15.04" or "snappy/guides/latest", etc.', max_length=120)),
19 ('index_doc', models.CharField(help_text='File name of doc to be used as index document, ie "intro.md"', max_length=120, blank=True)),
20 ],
21 ),
22 migrations.CreateModel(
23 name='RawHtml',14 name='RawHtml',
24 fields=[15 fields=[
25 ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),16 ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
2617
=== modified file 'developer_portal/models.py'
--- developer_portal/models.py 2015-11-02 16:47:26 +0000
+++ developer_portal/models.py 2016-01-06 12:40:31 +0000
@@ -1,7 +1,5 @@
1from django.db import models1from django.db import models
2from django.utils.translation import ugettext_lazy as _
32
4from cms.models import CMSPlugin
5from cms.extensions import TitleExtension3from cms.extensions import TitleExtension
6from cms.extensions.extension_pool import extension_pool4from cms.extensions.extension_pool import extension_pool
7from djangocms_text_ckeditor.html import extract_images5from djangocms_text_ckeditor.html import extract_images
@@ -20,25 +18,6 @@
20 AbstractText.save(self, *args, **kwargs)18 AbstractText.save(self, *args, **kwargs)
2119
2220
23class ExternalDocsBranch(models.Model):
24 # We originally assumed that branches would also live in LP,
25 # well, we were wrong, but let's keep the name around. It's
26 # no use having a schema/data migration just for this.
27 lp_origin = models.CharField(
28 max_length=200,
29 help_text=_('External branch location, ie: lp:snappy/15.04 or '
30 'https://github.com/ubuntu-core/snappy.git'))
31 docs_namespace = models.CharField(
32 max_length=120,
33 help_text=_('Path alias we want to use for the docs, '
34 'ie "snappy/guides/15.04" or '
35 '"snappy/guides/latest", etc.'))
36 index_doc = models.CharField(
37 max_length=120,
38 help_text=_('File name of doc to be used as index document, '
39 'ie "intro.md"'),
40 blank=True)
41
42class SEOExtension(TitleExtension):21class SEOExtension(TitleExtension):
43 keywords = models.CharField(max_length=256)22 keywords = models.CharField(max_length=256)
4423
4524
=== modified file 'developer_portal/settings.py'
--- developer_portal/settings.py 2015-12-11 16:10:37 +0000
+++ developer_portal/settings.py 2016-01-06 12:40:31 +0000
@@ -76,6 +76,8 @@
76 'store_data',76 'store_data',
7777
78 'api_docs',78 'api_docs',
79
80 'md_importer',
79]81]
8082
81MIDDLEWARE_CLASSES = (83MIDDLEWARE_CLASSES = (
8284
=== modified file 'locale/de.po'
--- locale/de.po 2015-11-24 05:38:32 +0000
+++ locale/de.po 2016-01-06 12:40:31 +0000
@@ -15,8 +15,8 @@
15"Content-Type: text/plain; charset=UTF-8\n"15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18"X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n"18"X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n"
19"X-Generator: Launchpad (build 17850)\n"19"X-Generator: Launchpad (build 17856)\n"
2020
21#: developer_portal/cms_plugins.py:1021#: developer_portal/cms_plugins.py:10
22msgid "Raw HTML"22msgid "Raw HTML"
2323
=== modified file 'locale/en_GB.po'
--- locale/en_GB.po 2015-11-24 05:38:32 +0000
+++ locale/en_GB.po 2016-01-06 12:40:31 +0000
@@ -15,8 +15,8 @@
15"Content-Type: text/plain; charset=UTF-8\n"15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18"X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n"18"X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n"
19"X-Generator: Launchpad (build 17850)\n"19"X-Generator: Launchpad (build 17856)\n"
2020
21#: developer_portal/cms_plugins.py:1021#: developer_portal/cms_plugins.py:10
22msgid "Raw HTML"22msgid "Raw HTML"
2323
=== modified file 'locale/es.po'
--- locale/es.po 2015-11-24 05:38:32 +0000
+++ locale/es.po 2016-01-06 12:40:31 +0000
@@ -15,8 +15,8 @@
15"Content-Type: text/plain; charset=UTF-8\n"15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=2; plural=n != 1;\n"17"Plural-Forms: nplurals=2; plural=n != 1;\n"
18"X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n"18"X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n"
19"X-Generator: Launchpad (build 17850)\n"19"X-Generator: Launchpad (build 17856)\n"
2020
21#: developer_portal/cms_plugins.py:1021#: developer_portal/cms_plugins.py:10
22msgid "Raw HTML"22msgid "Raw HTML"
2323
=== modified file 'locale/ug.po'
--- locale/ug.po 2015-11-24 05:38:32 +0000
+++ locale/ug.po 2016-01-06 12:40:31 +0000
@@ -15,8 +15,8 @@
15"Content-Type: text/plain; charset=UTF-8\n"15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=1; plural=0;\n"17"Plural-Forms: nplurals=1; plural=0;\n"
18"X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n"18"X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n"
19"X-Generator: Launchpad (build 17850)\n"19"X-Generator: Launchpad (build 17856)\n"
2020
21#: developer_portal/cms_plugins.py:1021#: developer_portal/cms_plugins.py:10
22msgid "Raw HTML"22msgid "Raw HTML"
2323
=== modified file 'locale/zh_CN.po'
--- locale/zh_CN.po 2015-11-24 05:38:32 +0000
+++ locale/zh_CN.po 2016-01-06 12:40:31 +0000
@@ -15,8 +15,8 @@
15"Content-Type: text/plain; charset=UTF-8\n"15"Content-Type: text/plain; charset=UTF-8\n"
16"Content-Transfer-Encoding: 8bit\n"16"Content-Transfer-Encoding: 8bit\n"
17"Plural-Forms: nplurals=1; plural=0;\n"17"Plural-Forms: nplurals=1; plural=0;\n"
18"X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n"18"X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n"
19"X-Generator: Launchpad (build 17850)\n"19"X-Generator: Launchpad (build 17856)\n"
2020
21#: developer_portal/cms_plugins.py:1021#: developer_portal/cms_plugins.py:10
22msgid "Raw HTML"22msgid "Raw HTML"
2323
=== added directory 'md_importer'
=== added file 'md_importer/__init__.py'
=== added file 'md_importer/admin.py'
--- md_importer/admin.py 1970-01-01 00:00:00 +0000
+++ md_importer/admin.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,36 @@
1from django.contrib import admin
2
3from .models import (
4 ExternalDocsBranch, ExternalDocsBranchImportDirective,
5 ImportedArticle,
6)
7from django.core.management import call_command
8
9__all__ = (
10)
11
12
13def import_selected_external_docs_branches(modeladmin, request, queryset):
14 branches = []
15 for branch in queryset:
16 branches.append(branch.origin)
17 call_command('import-external-docs-branches', *branches)
18 import_selected_external_docs_branches.short_description = \
19 "Import selected branches"
20
21
22@admin.register(ExternalDocsBranch)
23class ExternalDocsBranchAdmin(admin.ModelAdmin):
24 list_display = ('origin', 'post_checkout_command', 'branch_name',)
25 list_filter = ('origin', 'post_checkout_command', 'branch_name',)
26 actions = [import_selected_external_docs_branches]
27
28
29@admin.register(ExternalDocsBranchImportDirective)
30class ExternalDocsBranchImportDirectiveAdmin(admin.ModelAdmin):
31 pass
32
33
34@admin.register(ImportedArticle)
35class ImportedArticleAdmin(admin.ModelAdmin):
36 pass
037
=== added directory 'md_importer/management'
=== added file 'md_importer/management/__init__.py'
=== added directory 'md_importer/management/commands'
=== added file 'md_importer/management/commands/__init__.py'
=== renamed file 'developer_portal/management/commands/import-external-docs-branches.py' => 'md_importer/management/commands/import-external-docs-branches.py'
--- developer_portal/management/commands/import-external-docs-branches.py 2015-11-06 09:48:07 +0000
+++ md_importer/management/commands/import-external-docs-branches.py 2016-01-06 12:40:31 +0000
@@ -1,300 +1,18 @@
1from django.core.management.base import BaseCommand1from django.core.management.base import BaseCommand
2from django.core.management import call_command2from django.core.management import call_command
3from django.db import transaction3
44from ..importer.repo import create_repo
5from cms.api import create_page, add_plugin5
6from cms.models import Page, Title6import datetime
7from cms.utils import page_resolver
8
9from bs4 import BeautifulSoup
10import codecs
11import glob
12import logging7import logging
13import markdown
14import os
15import re
16import shutil8import shutil
17import subprocess
18import sys
19import tempfile9import tempfile
2010
21from developer_portal.models import ExternalDocsBranch11from developer_portal.models import (
2212 ExternalDocsBranch,
23DOCS_DIRNAME = 'docs'13 ExternalDocsBranchImportDirective,
2414 ImportedArticle,
2515)
26class DBActions:
27 added_pages = []
28 removed_pages = []
29
30 def add_page(self, **kwargs):
31 self.added_pages += [kwargs]
32
33 def remove_page(self, page_id):
34 self.removed_pages += [page_id]
35
36 @transaction.atomic()
37 def run(self):
38 for added_page in self.added_pages:
39 page = get_or_create_page(**added_page)
40 page.publish('en')
41
42 # Only remove pages created by a script!
43 Page.objects.filter(id__in=self.removed_pages,
44 created_by="script").delete()
45
46 # https://stackoverflow.com/questions/33284171/
47 call_command('cms', 'fix-tree')
48
49
50class MarkdownFile:
51 html = None
52
53 def __init__(self, fn, docs_namespace, db_actions, slug_override=None):
54 self.fn = fn
55 self.docs_namespace = docs_namespace
56 self.db_actions = db_actions
57 if slug_override:
58 self.slug = slug_override
59 else:
60 self.slug = slugify(self.fn)
61 self.full_url = os.path.join(self.docs_namespace, self.slug)
62 with codecs.open(self.fn, 'r', encoding='utf-8') as f:
63 self.html = markdown.markdown(
64 f.read(),
65 output_format="html5",
66 extensions=['markdown.extensions.tables'])
67 self.release_alias = self._get_release_alias()
68 self.title = self._read_title()
69 self._remove_body_and_html_tags()
70 self._use_developer_site_style()
71
72 def _get_release_alias(self):
73 alias = re.findall(r'/tmp/tmp\S+?/(\S+?)/%s/\S+?' % DOCS_DIRNAME,
74 self.fn)
75 return alias[0]
76
77 def _read_title(self):
78 soup = BeautifulSoup(self.html, 'html5lib')
79 if soup.title:
80 return soup.title.text
81 if soup.h1:
82 return soup.h1.text
83 return slugify(self.fn).replace('-', ' ').title()
84
85 def _remove_body_and_html_tags(self):
86 self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
87 flags=re.MULTILINE)
88 self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
89 flags=re.MULTILINE)
90
91 def _use_developer_site_style(self):
92 begin = (u"<div class=\"row no-border\">"
93 "\n<div class=\"eight-col\">\n")
94 end = u"</div>\n</div>"
95 self.html = begin + self.html + end
96 self.html = self.html.replace(
97 "<pre><code>",
98 "</div><div class=\"twelve-col\"><pre><code>")
99 self.html = self.html.replace(
100 "</code></pre>",
101 "</code></pre></div><div class=\"eight-col\">")
102
103 def replace_links(self, titles, url_map):
104 for title in titles:
105 local_md_fn = os.path.basename(title)
106 url = u'/'+url_map[title]
107 # Replace links of the form <a href="/path/somefile.md"> first
108 href = u"<a href=\"{}\">".format(url)
109 md_href = u"<a href=\"{}\">".format(local_md_fn)
110 self.html = self.html.replace(md_href, href)
111
112 # Now we can replace free-standing "somefile.md" references in
113 # the HTML
114 link = href + u"{}</a>".format(titles[title])
115 self.html = self.html.replace(local_md_fn, link)
116
117 def publish(self):
118 '''Publishes pages in their branch alias namespace.'''
119 self.db_actions.add_page(
120 title=self.title, full_url=self.full_url, menu_title=self.title,
121 html=self.html)
122
123
124class SnappyMarkdownFile(MarkdownFile):
125 def __init__(self, fn, docs_namespace, db_actions):
126 MarkdownFile.__init__(self, fn, docs_namespace, db_actions)
127 self._make_snappy_mods()
128
129 def _make_snappy_mods(self):
130 # Make sure the reader knows which documentation she is browsing
131 if self.release_alias != 'current':
132 before = (u"<div class=\"row no-border\">\n"
133 "<div class=\"eight-col\">\n")
134 after = (u"<div class=\"row no-border\">\n"
135 "<div class=\"box pull-three three-col\">"
136 "<p>You are browsing the Snappy <code>%s</code> "
137 "documentation.</p>"
138 "<p><a href=\"/snappy/guides/current/%s\">"
139 "Back to the latest stable release &rsaquo;"
140 "</a></p></div>\n"
141 "<div class=\"eight-col\">\n") % (self.release_alias,
142 self.slug, )
143 self.html = self.html.replace(before, after)
144
145 def publish(self):
146 if self.release_alias == "current":
147 # Add a guides/<page> redirect to guides/current/<page>
148 self.db_actions.add_page(
149 title=self.title,
150 full_url=self.full_url.replace('/current', ''),
151 redirect="/snappy/guides/current/%s" % (self.slug))
152 else:
153 self.title += " (%s)" % (self.release_alias,)
154 MarkdownFile.publish(self)
155
156
157def slugify(filename):
158 return os.path.basename(filename).replace('.md', '')
159
160
161class LocalBranch:
162 titles = {}
163 url_map = {}
164
165 def __init__(self, dirname, external_branch, db_actions):
166 self.dirname = dirname
167 self.docs_path = os.path.join(self.dirname, DOCS_DIRNAME)
168 self.doc_fns = glob.glob(self.docs_path+'/*.md')
169 self.md_files = []
170 self.external_branch = external_branch
171 self.docs_namespace = self.external_branch.docs_namespace
172 self.release_alias = os.path.basename(self.docs_namespace)
173 self.index_doc_title = self.release_alias.title()
174 self.index_doc = self.external_branch.index_doc
175 self.db_actions = db_actions
176 self.markdown_class = MarkdownFile
177
178 def import_markdown(self):
179 for doc_fn in self.doc_fns:
180 if self.index_doc and os.path.basename(doc_fn) == self.index_doc:
181 md_file = self.markdown_class(
182 doc_fn,
183 os.path.dirname(self.docs_namespace),
184 self.db_actions,
185 slug_override=os.path.basename(self.docs_namespace))
186 self.md_files.insert(0, md_file)
187 else:
188 md_file = self.markdown_class(doc_fn, self.docs_namespace,
189 self.db_actions)
190 self.md_files += [md_file]
191 self.titles[md_file.fn] = md_file.title
192 self.url_map[md_file.fn] = md_file.full_url
193 if not self.index_doc:
194 self._create_fake_index_doc()
195 for md_file in self.md_files:
196 md_file.replace_links(self.titles, self.url_map)
197
198 def remove_old_pages(self):
199 imported_page_urls = set([md_file.full_url
200 for md_file in self.md_files])
201 index_doc = page_resolver.get_page_queryset_from_path(
202 self.docs_namespace)
203 db_pages = []
204 if len(index_doc):
205 # All pages in this namespace currently in the database
206 db_pages = index_doc[0].get_descendants().all()
207 for db_page in db_pages:
208 still_relevant = False
209 for url in imported_page_urls:
210 if url in db_page.get_absolute_url():
211 still_relevant = True
212 break
213 # At this point we know that there's no match and the page
214 # can be deleted.
215 if not still_relevant:
216 self.db_actions.remove_page(db_page.id)
217
218 def publish(self):
219 for md_file in self.md_files:
220 md_file.publish()
221
222 def _create_fake_index_doc(self):
223 '''Creates a fake index page at the top of the branches
224 docs namespace.'''
225
226 if self.docs_namespace == "current":
227 redirect = "/snappy/guides"
228 else:
229 redirect = None
230
231 in_navigation = False
232 menu_title = None
233 list_pages = ""
234 for page in self.md_files:
235 list_pages += "<li><a href=\"%s\">%s</a></li>" \
236 % (os.path.basename(page.full_url), page.title)
237 landing = (
238 u"<div class=\"row\"><div class=\"eight-col\">\n"
239 "<p>This section contains documentation for the "
240 "<code>%s</code> Snappy branch.</p>"
241 "<p><ul class=\"list-ubuntu\">%s</ul></p>\n"
242 "<p>Auto-imported from <a "
243 "href=\"https://github.com/ubuntu-core/snappy\">%s</a>.</p>\n"
244 "</div></div>") % (self.release_alias, list_pages,
245 self.external_branch.lp_origin)
246 self.db_actions.add_page(
247 title=self.index_doc_title, full_url=self.docs_namespace,
248 in_navigation=in_navigation, redirect=redirect, html=landing,
249 menu_title=menu_title)
250
251
252class SnappyLocalBranch(LocalBranch):
253 def __init__(self, dirname, external_branch, db_actions):
254 LocalBranch.__init__(self, dirname, external_branch, db_actions)
255 self.markdown_class = SnappyMarkdownFile
256 self.index_doc_title = 'Snappy documentation'
257 if self.release_alias != 'current':
258 self.index_doc_title += ' (%s)' % self.release_alias
259
260
261def get_or_create_page(title, full_url, menu_title=None,
262 in_navigation=True, redirect=None, html=None):
263 # First check if pages already exist.
264 pages = Title.objects.select_related('page').filter(path__regex=full_url)
265 if pages:
266 page = pages[0].page
267 page.title = title
268 page.publisher_is_draft = True
269 page.menu_title = menu_title
270 page.in_navigation = in_navigation
271 page.redirect = redirect
272 if html:
273 # We create the page, so we know there's just one placeholder
274 placeholder = page.placeholders.all()[0]
275 if placeholder.get_plugins():
276 plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
277 plugin.body = html
278 plugin.save()
279 else:
280 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
281 else:
282 parent_pages = Title.objects.select_related('page').filter(
283 path__regex=os.path.dirname(full_url))
284 if not parent_pages:
285 print('Parent %s not found.' % os.path.dirname(full_url))
286 sys.exit(1)
287 parent = parent_pages[0].page
288
289 slug = os.path.basename(full_url)
290 page = create_page(
291 title, "default.html", "en", slug=slug, parent=parent,
292 menu_title=menu_title, in_navigation=in_navigation,
293 position="last-child", redirect=redirect)
294 if html:
295 placeholder = page.placeholders.get()
296 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
297 return page
29816
29917
300def import_branches(selection):18def import_branches(selection):
@@ -302,52 +20,34 @@
302 logging.error('No branches registered in the '20 logging.error('No branches registered in the '
303 'ExternalDocsBranch table yet.')21 'ExternalDocsBranch table yet.')
304 return22 return
305 tempdir = tempfile.mkdtemp()
306 db_actions = DBActions()
307 for branch in ExternalDocsBranch.objects.filter(23 for branch in ExternalDocsBranch.objects.filter(
308 docs_namespace__regex=selection):24 origin__regex=selection, active=True):
309 checkout_location = os.path.join(25 tempdir = tempfile.mkdtemp()
310 tempdir, os.path.basename(branch.docs_namespace))26 repo = create_repo(tempdir, branch.origin, branch.branch_name,
311 sourcecode = SourceCode(branch.lp_origin, checkout_location)27 branch.post_checkout_command)
312 if sourcecode.get() != 0:28 if repo.get() != 0:
313 logging.error(
314 'Could not check out branch "%s".' % branch.lp_origin)
315 if os.path.exists(checkout_location):
316 shutil.rmtree(checkout_location)
317 break29 break
318 if branch.lp_origin.startswith('lp:snappy') or \30 for directive in ExternalDocsBranchImportDirective.objects.filter(
319 'snappy' in branch.lp_origin.split(':')[1].split('.git')[0].split('/'):31 external_docs_branch=branch):
320 local_branch = SnappyLocalBranch(checkout_location, branch,32 repo.add_directive(directive.import_from,
321 db_actions)33 directive.write_to)
322 else:34 repo.execute_import_directives()
323 local_branch = LocalBranch(checkout_location, branch, db_actions)35 repo.publish()
324 local_branch.import_markdown()36 for page in repo.pages:
325 local_branch.publish()37 ImportedArticle.objects.get_or_create(
326 local_branch.remove_old_pages()38 branch=branch,
327 shutil.rmtree(tempdir)39 page=page,
328 db_actions.run()40 last_import=datetime.datetime.now())
32941
33042 # The import is done, now let's clean up.
331class SourceCode():43 for old_article in ImportedArticle.objects.filter(branch=branch):
332 def __init__(self, branch_origin, checkout_location):44 if old_article.page not in repo.pages and \
333 self.branch_origin = branch_origin45 old_article.page.changed_by in ['python-api', 'script']:
334 self.checkout_location = checkout_location46 old_article.page.delete()
33547 shutil.rmtree(tempdir)
336 def get(self):48
337 if self.branch_origin.startswith('lp:') and \49 # https://stackoverflow.com/questions/33284171/
338 os.path.exists('/usr/bin/bzr'):50 call_command('cms', 'fix-tree')
339 return subprocess.call([
340 'bzr', 'checkout', '--lightweight', self.branch_origin,
341 self.checkout_location])
342 if self.branch_origin.startswith('https://github.com') and \
343 self.branch_origin.endswith('.git') and \
344 os.path.exists('/usr/bin/git'):
345 return subprocess.call([
346 'git', 'clone', '-q', self.branch_origin,
347 self.checkout_location])
348 logging.error(
349 'Branch format "{}" not understood.'.format(self.branch_origin))
350 return 1
35151
35252
353class Command(BaseCommand):53class Command(BaseCommand):
35454
=== added directory 'md_importer/management/importer'
=== added file 'md_importer/management/importer/__init__.py'
=== added file 'md_importer/management/importer/article.py'
--- md_importer/management/importer/article.py 1970-01-01 00:00:00 +0000
+++ md_importer/management/importer/article.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,118 @@
1from bs4 import BeautifulSoup
2import codecs
3import logging
4import markdown
5import os
6import re
7
8from .publish import get_or_create_page, slugify
9
10
11class Article:
12 html = None
13 page = None
14 title = ""
15
16 def __init__(self, fn, write_to):
17 self.fn = fn
18 self.write_to = slugify(self.fn)
19 self.full_url = write_to
20 self.slug = os.path.basename(self.full_url)
21
22 def read(self):
23 with codecs.open(self.fn, 'r', encoding='utf-8') as f:
24 if self.fn.endswith('.md'):
25 self.html = markdown.markdown(
26 f.read(),
27 output_format='html5',
28 extensions=['pymdownx.github'])
29 elif self.fn.endswith('.html'):
30 self.html = f.read()
31 else:
32 logging.error("Don't know how to interpret '{}'.".format(
33 self.fn))
34 return False
35 self.title = self._read_title()
36 self._remove_body_and_html_tags()
37 self._use_developer_site_style()
38 return True
39
40 def _read_title(self):
41 soup = BeautifulSoup(self.html, 'html5lib')
42 if soup.title:
43 return soup.title.text
44 if soup.h1:
45 return soup.h1.text
46 return slugify(self.fn).replace('-', ' ').title()
47
48 def _remove_body_and_html_tags(self):
49 self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
50 flags=re.MULTILINE)
51 self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
52 flags=re.MULTILINE)
53
54 def _use_developer_site_style(self):
55 begin = (u"<div class=\"row no-border\">"
56 "\n<div class=\"eight-col\">\n")
57 end = u"</div>\n</div>"
58 self.html = begin + self.html + end
59 self.html = self.html.replace(
60 "<pre><code>",
61 "</div><div class=\"twelve-col\"><pre><code>")
62 self.html = self.html.replace(
63 "</code></pre>",
64 "</code></pre></div><div class=\"eight-col\">")
65
66 def replace_links(self, titles, url_map):
67 soup = BeautifulSoup(self.html, 'html5lib')
68 for link in soup.find_all('a'):
69 for title in titles:
70 if link.attrs['href'] == os.path.basename(title):
71 link.attrs['href'] = url_map[title].full_url
72 self.html = soup.prettify()
73
74 def add_to_db(self):
75 '''Publishes pages in their branch alias namespace.'''
76 self.page = get_or_create_page(
77 title=self.title, full_url=self.full_url, menu_title=self.title,
78 html=self.html)
79 self.full_url = self.page.get_absolute_url()
80
81
82class SnappyArticle(Article):
83 release_alias = None
84
85 def read(self):
86 if not Article.read(self):
87 return False
88 self.release_alias = re.findall(r'snappy/guides/(\S+?)/\S+?',
89 self.full_url)[0]
90 self._make_snappy_mods()
91 return True
92
93 def _make_snappy_mods(self):
94 # Make sure the reader knows which documentation she is browsing
95 if self.release_alias != 'current':
96 before = (u"<div class=\"row no-border\">\n"
97 "<div class=\"eight-col\">\n")
98 after = (u"<div class=\"row no-border\">\n"
99 "<div class=\"box pull-three three-col\">"
100 "<p>You are browsing the Snappy <code>%s</code> "
101 "documentation.</p>"
102 "<p><a href=\"/snappy/guides/current/%s\">"
103 "Back to the latest stable release &rsaquo;"
104 "</a></p></div>\n"
105 "<div class=\"eight-col\">\n") % (self.release_alias,
106 self.slug, )
107 self.html = self.html.replace(before, after)
108
109 def add_to_db(self):
110 if self.release_alias == "current":
111 # Add a guides/<page> redirect to guides/current/<page>
112 get_or_create_page(
113 title=self.title,
114 full_url=self.full_url.replace('/current', ''),
115 redirect="/snappy/guides/current/{}".format(self.slug))
116 else:
117 self.title += " (%s)" % (self.release_alias,)
118 Article.add_to_db(self)
0119
=== added file 'md_importer/management/importer/publish.py'
--- md_importer/management/importer/publish.py 1970-01-01 00:00:00 +0000
+++ md_importer/management/importer/publish.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,64 @@
1from cms.api import create_page, add_plugin
2from cms.models import Title
3
4import logging
5import os
6import sys
7
8# XXX: use this once we RawHTML plugins don't strip comments (LP: #1523925)
9START_TEXT = """
10<!--
11branch id: {}
12
13THIS PAGE IS AUTOMATICALLY UPDATED.
14DON'T EDIT IT - CHANGES WILL BE OVERWRITTEN.
15-->
16"""
17
18
19def slugify(filename):
20 return os.path.basename(filename).replace('.md', '').replace('.html', '')
21
22
23def get_or_create_page(title, full_url, menu_title=None,
24 in_navigation=True, redirect=None, html=None):
25 # First check if pages already exist.
26 if full_url.startswith('/'):
27 full_url = full_url[1:]
28 pages = Title.objects.select_related('page').filter(
29 path__regex=full_url).filter(publisher_is_draft=True)
30 if pages:
31 page = pages[0].page
32 page.title = title
33 page.publisher_is_draft = True
34 page.menu_title = menu_title
35 page.in_navigation = in_navigation
36 page.redirect = redirect
37 if html:
38 # We create the page, so we know there's just one placeholder
39 placeholder = page.placeholders.all()[0]
40 if placeholder.get_plugins():
41 plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
42 plugin.body = html
43 plugin.save()
44 else:
45 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
46 else:
47 parent_pages = Title.objects.select_related('page').filter(
48 path__regex=os.path.dirname(full_url)).filter(
49 publisher_is_draft=True)
50 if not parent_pages:
51 logging.error('Parent {} not found.'.format(
52 os.path.dirname(full_url)))
53 sys.exit(1)
54 parent = parent_pages[0].page
55
56 slug = os.path.basename(full_url)
57 page = create_page(
58 title, "default.html", "en", slug=slug, parent=parent,
59 menu_title=menu_title, in_navigation=in_navigation,
60 position="last-child", redirect=redirect)
61 if html:
62 placeholder = page.placeholders.get()
63 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
64 return page
065
=== added file 'md_importer/management/importer/repo.py'
--- md_importer/management/importer/repo.py 1970-01-01 00:00:00 +0000
+++ md_importer/management/importer/repo.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,168 @@
1from .article import Article, SnappyArticle
2from .publish import get_or_create_page, slugify
3from .source import SourceCode
4
5from cms.api import publish_pages
6
7import glob
8import logging
9import os
10import shutil
11
12
13def create_repo(tempdir, origin, branch_name, post_checkout_command):
14 if os.path.exists(origin):
15 if 'snappy' in origin:
16 repo_class = SnappyRepo
17 else:
18 repo_class = Repo
19 else:
20 if origin.startswith('lp:snappy') or \
21 'snappy' in origin.split(':')[1].split('.git')[0].split('/'):
22 repo_class = SnappyRepo
23 else:
24 repo_class = Repo
25 return repo_class(tempdir, origin, branch_name, post_checkout_command)
26
27
28class Repo:
29 titles = {}
30 url_map = {}
31 index_doc_url = None
32 index_page = None
33 release_alias = None
34 directives = []
35 imported_articles = []
36 # On top of the pages in imported_articles this also includes index_page
37 pages = []
38
39 def __init__(self, tempdir, origin, branch_name, post_checkout_command):
40 self.origin = origin
41 self.branch_name = branch_name
42 self.post_checkout_command = post_checkout_command
43 branch_nick = os.path.basename(self.origin.replace('.git', ''))
44 self.checkout_location = os.path.join(
45 tempdir, branch_nick)
46 self.index_doc_title = branch_nick
47 self.article_class = Article
48
49 # Only used to speed up tests - allows reusing same object without
50 # having to redownload the source again
51 def reset(self):
52 self.article_class = Article
53 self.directives = []
54 self.imported_articles = []
55
56 def get(self):
57 sourcecode = SourceCode(self.origin, self.checkout_location,
58 self.branch_name, self.post_checkout_command)
59 if sourcecode.get() != 0:
60 logging.error(
61 'Could not check out branch "{}".'.format(self.origin))
62 shutil.rmtree(self.checkout_location)
63 return 1
64 return 0
65
66 def add_directive(self, import_from, write_to):
67 self.directives += [
68 {
69 'import_from': os.path.join(self.checkout_location,
70 import_from),
71 'write_to': write_to
72 }
73 ]
74
75 def execute_import_directives(self):
76 import_list = []
77 # Import single files first
78 for directive in [d for d in self.directives
79 if os.path.isfile(d['import_from'])]:
80 import_list += [
81 (directive['import_from'], directive['write_to'])
82 ]
83 # Import directories next
84 for directive in [d for d in self.directives
85 if os.path.isdir(d['import_from'])]:
86 for fn in glob.glob('{}/*'.format(directive['import_from'])):
87 if fn not in [a[0] for a in import_list]:
88 import_list += [
89 (fn, os.path.join(directive['write_to'], slugify(fn)))
90 ]
91 # If we import into a namespace and don't have an index doc,
92 # we need to write one.
93 if directive['write_to'] not in [x[1] for x in import_list]:
94 self.index_doc_url = directive['write_to']
95 if self.index_doc_url:
96 self._create_fake_index_page()
97 # The actual import
98 for entry in import_list:
99 article = self._read_article(entry[0], entry[1])
100 if article:
101 self.imported_articles += [article]
102 self.titles[article.fn] = article.title
103 self.url_map[article.fn] = article
104 if self.index_doc_url:
105 self._write_fake_index_doc()
106
107 def _read_article(self, fn, write_to):
108 article = self.article_class(fn, write_to)
109 if article.read():
110 return article
111 return None
112
113 def publish(self):
114 for article in self.imported_articles:
115 article.add_to_db()
116 article.replace_links(self.titles, self.url_map)
117 self.pages = [article.page for article in self.imported_articles]
118 if self.index_page:
119 self.pages.extend([self.index_page])
120 publish_pages(self.pages)
121
122 def _create_fake_index_page(self):
123 '''Creates a fake index page at the top of the branches
124 docs namespace.'''
125
126 if self.index_doc_url.endswith('current'):
127 redirect = '/snappy/guides'
128 else:
129 redirect = None
130 self.index_page = get_or_create_page(
131 title=self.index_doc_title, full_url=self.index_doc_url,
132 in_navigation=False, redirect=redirect, html='',
133 menu_title=None)
134
135 def _write_fake_index_doc(self):
136 list_pages = ''
137 for article in [a for a
138 in self.imported_articles
139 if a.full_url.startswith(self.index_doc_url)]:
140 list_pages += '<li><a href=\"{}\">{}</a></li>'.format(
141 os.path.basename(article.full_url), article.title)
142 self.index_page.html = (
143 u'<div class=\"row\"><div class=\"eight-col\">\n'
144 '<p>This section contains documentation for the '
145 '<code>{}</code> Snappy branch.</p>'
146 '<p><ul class=\"list-ubuntu\">{}</ul></p>\n'
147 '<p>Auto-imported from <a '
148 'href=\"{}\">{}</a>.</p>\n'
149 '</div></div>'.format(self.release_alias, list_pages,
150 self.origin, self.origin))
151
152
153class SnappyRepo(Repo):
154 def __init__(self, tempdir, origin, branch_name, post_checkout_command):
155 Repo.__init__(self, tempdir, origin, branch_name,
156 post_checkout_command)
157 self.article_class = SnappyArticle
158 self.index_doc_title = 'Snappy documentation'
159
160 def _create_fake_index_page(self):
161 self.release_alias = os.path.basename(self.index_doc_url)
162 if not self.index_doc_url.endswith('current'):
163 self.index_doc_title += ' ({})'.format(self.release_alias)
164 Repo._create_fake_index_page(self)
165
166 def reset(self):
167 Repo.reset(self)
168 self.article_class = SnappyArticle
0169
=== added file 'md_importer/management/importer/source.py'
--- md_importer/management/importer/source.py 1970-01-01 00:00:00 +0000
+++ md_importer/management/importer/source.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,60 @@
1import logging
2import os
3import shutil
4import subprocess
5
6
7class SourceCode():
8 def __init__(self, origin, checkout_location, branch_name,
9 post_checkout_command):
10 self.origin = origin
11 self.checkout_location = checkout_location
12 self.branch_name = branch_name
13 self.post_checkout_command = post_checkout_command
14
15 def get(self):
16 res = self._get_branch()
17 if res == 0 and self.post_checkout_command:
18 res = self._post_checkout()
19 return res
20 return res
21
22 def _get_branch(self):
23 if os.path.exists(self.origin):
24 shutil.copytree(
25 self.origin,
26 self.checkout_location)
27 return 0
28 if self.origin.startswith('lp:') and \
29 os.path.exists('/usr/bin/bzr'):
30 return subprocess.call([
31 'bzr', 'checkout', '--lightweight', self.origin,
32 self.checkout_location])
33 if self.origin.startswith('https://github.com') and \
34 self.origin.endswith('.git') and \
35 os.path.exists('/usr/bin/git'):
36 retcode = subprocess.call([
37 'git', 'clone', '--quiet', self.origin,
38 self.checkout_location])
39 if retcode == 0 and self.branch_name:
40 pwd = os.getcwd()
41 os.chdir(self.checkout_location)
42 retcode = subprocess.call(['git', 'checkout', '--quiet',
43 self.branch_name])
44 os.chdir(pwd)
45 return retcode
46 logging.error(
47 'Branch format "{}" not understood.'.format(self.origin))
48 return 1
49
50 def _post_checkout(self):
51 pwd = os.getcwd()
52 os.chdir(self.checkout_location)
53 process = subprocess.Popen(self.post_checkout_command.split(),
54 stdout=subprocess.PIPE)
55 (out, err) = process.communicate()
56 retcode = process.wait()
57 os.chdir(pwd)
58 if retcode != 0:
59 logging.error(out)
60 return retcode
061
=== added directory 'md_importer/migrations'
=== added file 'md_importer/migrations/0001_initial.py'
--- md_importer/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
+++ md_importer/migrations/0001_initial.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,46 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import migrations, models
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('cms', '0013_urlconfrevision'),
11 ]
12
13 operations = [
14 migrations.CreateModel(
15 name='ExternalDocsBranch',
16 fields=[
17 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 ('origin', models.CharField(help_text='External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-core/snappy.git', max_length=200)),
19 ('branch_name', models.CharField(help_text='For use with git branches, ie: "master" or "15.04" or "1.x".', max_length=200, blank=True)),
20 ('post_checkout_command', models.CharField(help_text='Command to run after checkout of the branch.', max_length=100, blank=True)),
21 ('active', models.BooleanField(default=True)),
22 ],
23 options={
24 'verbose_name': 'external docs branch',
25 'verbose_name_plural': 'external docs branches',
26 },
27 ),
28 migrations.CreateModel(
29 name='ExternalDocsBranchImportDirective',
30 fields=[
31 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
32 ('import_from', models.CharField(help_text='File or directory to import from the branch. Ie: "docs/intro.md" (file) or "docs" (complete directory), etc.', max_length=150)),
33 ('write_to', models.CharField(help_text='Article URL (for a specific file) or article namespace for a directory or a set of files.', max_length=150)),
34 ('external_docs_branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
35 ],
36 ),
37 migrations.CreateModel(
38 name='ImportedArticle',
39 fields=[
40 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
41 ('last_import', models.DateTimeField(help_text='Datetime of last import.', verbose_name='Datetime')),
42 ('branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
43 ('page', models.ForeignKey(to='cms.Page')),
44 ],
45 ),
46 ]
047
=== added file 'md_importer/migrations/__init__.py'
=== added file 'md_importer/models.py'
--- md_importer/models.py 1970-01-01 00:00:00 +0000
+++ md_importer/models.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,54 @@
1from django.db import models
2from django.utils.translation import ugettext_lazy as _
3
4from cms.models import Page
5
6
7class ExternalDocsBranch(models.Model):
8 origin = models.CharField(
9 max_length=200,
10 help_text=_('External branch location, ie: lp:snappy/15.04 or '
11 'https://github.com/ubuntu-core/snappy.git'))
12 branch_name = models.CharField(
13 max_length=200,
14 help_text=_('For use with git branches, ie: "master" or "15.04" '
15 'or "1.x".'),
16 blank=True)
17 post_checkout_command = models.CharField(
18 max_length=100,
19 help_text=_('Command to run after checkout of the branch.'),
20 blank=True)
21 active = models.BooleanField(default=True)
22
23 def __str__(self):
24 if self.branch_name:
25 return "{} - {}".format(self.origin, self.branch_name)
26 return "{}".format(self.origin)
27
28 class Meta:
29 verbose_name = "external docs branch"
30 verbose_name_plural = "external docs branches"
31
32
33class ExternalDocsBranchImportDirective(models.Model):
34 external_docs_branch = models.ForeignKey(ExternalDocsBranch)
35 import_from = models.CharField(
36 max_length=150,
37 help_text=_('File or directory to import from the branch. '
38 'Ie: "docs/intro.md" (file) or '
39 '"docs" (complete directory), etc.'))
40 write_to = models.CharField(
41 max_length=150,
42 help_text=_('Article URL (for a specific file) or article namespace '
43 'for a directory or a set of files.'))
44
45 def __str__(self):
46 return "{} -- {}".format(self.external_docs_branch,
47 self.import_from)
48
49
50class ImportedArticle(models.Model):
51 page = models.ForeignKey(Page)
52 branch = models.ForeignKey(ExternalDocsBranch)
53 last_import = models.DateTimeField(
54 _('Datetime'), help_text=_('Datetime of last import.'))
055
=== added directory 'md_importer/tests'
=== added file 'md_importer/tests/__init__.py'
--- md_importer/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/__init__.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,8 @@
1import shutil
2
3from utils import SnapcraftTestRepo, SnappyTestRepo
4
5
6def tearDownModule():
7 shutil.rmtree(SnapcraftTestRepo().tempdir)
8 shutil.rmtree(SnappyTestRepo().tempdir)
09
=== added directory 'md_importer/tests/data'
=== added directory 'md_importer/tests/data/link-test'
=== added file 'md_importer/tests/data/link-test/file1.md'
--- md_importer/tests/data/link-test/file1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-test/file1.md 2016-01-06 12:40:31 +0000
@@ -0,0 +1,5 @@
1# Test
2
3This is a link to [the file2 file](file2.md).
4
5That's it.
06
=== added file 'md_importer/tests/data/link-test/file2.md'
--- md_importer/tests/data/link-test/file2.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-test/file2.md 2016-01-06 12:40:31 +0000
@@ -0,0 +1,3 @@
1# File 2
2
3Here's just some text.
04
=== added file 'md_importer/tests/test_branch_fetch.py'
--- md_importer/tests/test_branch_fetch.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/test_branch_fetch.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,42 @@
1import os
2import shutil
3import tempfile
4
5from django.test import TestCase
6
7from ..management.importer.repo import create_repo, Repo
8from .utils import SnapcraftTestRepo
9
10
11class TestBranchFetch(TestCase):
12 def test_git_fetch(self):
13 snapcraft = SnapcraftTestRepo()
14 snapcraft.repo.reset()
15 self.assertEqual(snapcraft.fetch_retcode, 0)
16 self.assertTrue(isinstance(snapcraft.repo, Repo))
17
18 def test_bzr_fetch(self):
19 tempdir = tempfile.mkdtemp()
20 repo = create_repo(
21 tempdir,
22 'lp:snapcraft', # outdated, but should work for testing
23 '',
24 '')
25 ret = repo.get()
26 shutil.rmtree(tempdir)
27 self.assertEqual(ret, 0)
28 self.assertTrue(isinstance(repo, Repo))
29
30 def test_post_checkout_command(self):
31 tempdir = tempfile.mkdtemp()
32 repo = create_repo(
33 tempdir,
34 'lp:snapcraft',
35 '',
36 'touch something.html'
37 )
38 ret = repo.get()
39 self.assertEqual(ret, 0)
40 self.assertTrue(os.path.exists(
41 os.path.join(repo.checkout_location, 'something.html')))
42 shutil.rmtree(tempdir)
043
=== added file 'md_importer/tests/test_branch_import.py'
--- md_importer/tests/test_branch_import.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/test_branch_import.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,67 @@
1from django.test import TestCase
2
3from cms.api import publish_pages
4from cms.models import Page
5
6from ..management.importer.article import Article
7from .utils import (
8 db_create_home_page,
9 db_empty_page_list,
10 SnapcraftTestRepo,
11)
12
13
14class TestBranchImport(TestCase):
15 def test_1dir_import(self):
16 db_empty_page_list()
17 db_create_home_page()
18 snapcraft = SnapcraftTestRepo()
19 snapcraft.repo.reset()
20 snapcraft.repo.add_directive('docs', '/')
21 snapcraft.repo.execute_import_directives()
22 snapcraft.repo.publish()
23 pages = Page.objects.all()
24 self.assertGreater(len(pages), 3)
25 for article in snapcraft.repo.imported_articles:
26 self.assertTrue(isinstance(article, Article))
27
28 def test_1dir_and_2files_import(self):
29 db_empty_page_list()
30 db_create_home_page()
31 snapcraft = SnapcraftTestRepo()
32 snapcraft.repo.reset()
33 snapcraft.repo.add_directive('docs', '/')
34 snapcraft.repo.add_directive('README.md', '/')
35 snapcraft.repo.add_directive('HACKING.md', '/hacking')
36 snapcraft.repo.execute_import_directives()
37 snapcraft.repo.publish()
38 pages = Page.objects.all()
39 self.assertGreater(len(pages), 5)
40 self.assertIn(u'/en/', [p.get_absolute_url() for p in pages])
41 self.assertIn(u'/en/hacking/', [p.get_absolute_url() for p in pages])
42
43 # Check if all importe article has 'home' as parent
44 def test_articletree_1file_import(self):
45 db_empty_page_list()
46 home = db_create_home_page()
47 publish_pages([[home]])
48 snapcraft = SnapcraftTestRepo()
49 snapcraft.repo.reset()
50 snapcraft.repo.add_directive('README.md', '/readme')
51 snapcraft.repo.execute_import_directives()
52 snapcraft.repo.publish()
53 self.assertEqual(Page.objects.count(), 1+1) # readme + home
54 self.assertTrue(snapcraft.repo.pages[0].parent == home)
55
56 # Check if all imported articles have 'home' as parent
57 def test_articletree_1dir_import(self):
58 db_empty_page_list()
59 home = db_create_home_page()
60 snapcraft = SnapcraftTestRepo()
61 snapcraft.repo.reset()
62 snapcraft.repo.add_directive('docs', '/')
63 snapcraft.repo.execute_import_directives()
64 snapcraft.repo.publish()
65 for page in Page.objects.filter(publisher_is_draft=False):
66 if page.parent is not None:
67 self.assertEqual(page.parent_id, home.id)
068
=== added file 'md_importer/tests/test_link_rewrite.py'
--- md_importer/tests/test_link_rewrite.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/test_link_rewrite.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,44 @@
1from bs4 import BeautifulSoup
2import os
3import shutil
4import tempfile
5
6from django.http.response import HttpResponseNotFound
7from django.test import TestCase, Client
8
9from cms.models import Page
10
11from ..management.importer.article import Article
12from ..management.importer.repo import create_repo
13from .utils import (
14 db_create_home_page,
15 db_empty_page_list,
16)
17
18
19class TestLinkRewrite(TestCase):
20 def test_simple_case(self):
21 db_empty_page_list()
22 db_create_home_page()
23 tempdir = tempfile.mkdtemp()
24 repo = create_repo(
25 tempdir,
26 os.path.join(os.path.dirname(__file__), 'data/link-test'),
27 '',
28 '')
29 self.assertEqual(repo.get(), 0)
30 repo.add_directive('', '/')
31 repo.execute_import_directives()
32 repo.publish()
33 pages = Page.objects.all()
34 self.assertEqual(pages.count(), 1+2) # Home + 2 articles
35 c = Client()
36 for article in repo.imported_articles:
37 self.assertTrue(isinstance(article, Article))
38 soup = BeautifulSoup(article.html, 'html5lib')
39 for link in soup.find_all('a'):
40 if not link.has_attr('class') or \
41 'headeranchor-link' not in link.attrs['class']:
42 res = c.get(link.attrs['href'])
43 self.assertNotIsInstance(res, HttpResponseNotFound)
44 shutil.rmtree(tempdir)
045
=== added file 'md_importer/tests/test_snappy_import.py'
--- md_importer/tests/test_snappy_import.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/test_snappy_import.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,70 @@
1from django.test import TestCase
2
3from cms.api import publish_pages
4from cms.models import Page
5
6from ..management.importer.repo import SnappyRepo
7from ..management.importer.article import SnappyArticle
8from .utils import (
9 db_add_empty_page,
10 db_create_home_page,
11 db_empty_page_list,
12 SnappyTestRepo,
13)
14
15
16class TestSnappyImport(TestCase):
17 def test_snappy_devel_import(self):
18 db_empty_page_list()
19 home = db_create_home_page()
20 snappy_page = db_add_empty_page('Snappy', home)
21 guides = db_add_empty_page('Guides', snappy_page)
22 publish_pages([home, snappy_page, guides])
23 snappy = SnappyTestRepo()
24 snappy.repo.reset()
25 self.assertEqual(snappy.fetch_retcode, 0)
26 self.assertTrue(isinstance(snappy.repo, SnappyRepo))
27 snappy.repo.add_directive('docs', '/snappy/guides/devel')
28 snappy.repo.execute_import_directives()
29 snappy.repo.publish()
30 for article in snappy.repo.imported_articles:
31 self.assertTrue(isinstance(article, SnappyArticle))
32 self.assertGreater(len(snappy.repo.pages), 0)
33 devel = Page.objects.filter(parent=guides.get_public_object())
34 self.assertEqual(devel.count(), 1)
35 for page in Page.objects.filter(publisher_is_draft=False):
36 if page not in [home, snappy_page, guides, devel[0]]:
37 self.assertEqual(page.parent, devel[0])
38
39 def test_snappy_current_import(self):
40 db_empty_page_list()
41 home = db_create_home_page()
42 snappy_page = db_add_empty_page('Snappy', home)
43 guides = db_add_empty_page('Guides', snappy_page)
44 publish_pages([home, snappy_page, guides])
45 snappy = SnappyTestRepo()
46 snappy.repo.reset()
47 self.assertTrue(isinstance(snappy.repo, SnappyRepo))
48 snappy.repo.add_directive('docs', '/snappy/guides/current')
49 snappy.repo.execute_import_directives()
50 snappy.repo.publish()
51 number_of_articles = len(snappy.repo.imported_articles)
52 for article in snappy.repo.imported_articles:
53 self.assertTrue(isinstance(article, SnappyArticle))
54 self.assertGreater(number_of_articles, 0)
55 pages = Page.objects.all()
56 current_search = [
57 a for a in pages
58 if a.get_slug('current') and
59 a.get_absolute_url().endswith('snappy/guides/current/')]
60 self.assertEqual(len(current_search), 1)
61 current = current_search[0]
62 nav_pages = [home, snappy_page, guides, current]
63 # 1 imported article, 1 redirect
64 self.assertEqual(
65 number_of_articles*2, pages.count()-len(nav_pages))
66 for page in [a for a in pages if a not in nav_pages]:
67 if page.get_redirect('en'):
68 self.assertEqual(page.parent, guides)
69 else:
70 self.assertEqual(page.parent, current)
071
=== added file 'md_importer/tests/test_utils.py'
--- md_importer/tests/test_utils.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/test_utils.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,33 @@
1from django.test import TestCase
2
3from cms.api import publish_pages
4from cms.models import Page
5
6from .utils import (
7 db_add_empty_page,
8 db_create_home_page,
9 db_empty_page_list,
10)
11
12
13class PageDBActivities(TestCase):
14 def test_empty_page_list(self):
15 db_empty_page_list()
16 self.assertEqual(Page.objects.count(), 0)
17
18 def test_create_home_page(self):
19 db_empty_page_list()
20 home = db_create_home_page()
21 publish_pages([home])
22 self.assertNotEqual(home, None)
23 self.assertEqual(Page.objects.count(), 1)
24
25 def test_simple_articletree(self):
26 db_empty_page_list()
27 home = db_create_home_page()
28 snappy = db_add_empty_page('Snappy', home)
29 guides = db_add_empty_page('Guides', snappy)
30 publish_pages([home, snappy, guides])
31 self.assertEqual(Page.objects.count(), 3)
32 self.assertEqual(guides.parent, snappy)
33 self.assertEqual(snappy.parent, home)
034
=== added file 'md_importer/tests/utils.py'
--- md_importer/tests/utils.py 1970-01-01 00:00:00 +0000
+++ md_importer/tests/utils.py 2016-01-06 12:40:31 +0000
@@ -0,0 +1,67 @@
1import shutil
2import tempfile
3
4from django.utils.text import slugify
5
6from cms.api import create_page
7from cms.models import Page
8
9from ..management.importer.repo import create_repo
10
11
12class Singleton(type):
13 _instances = {}
14
15 def __call__(cls, *args, **kwargs):
16 if cls not in cls._instances:
17 cls._instances[cls] = super(Singleton, cls).__call__(*args,
18 **kwargs)
19 return cls._instances[cls]
20
21
22# We are going to re-use this one, so we don't have to checkout the git
23# repo all the time.
24class SnapcraftTestRepo():
25 __metaclass__ = Singleton
26
27 def __init__(self):
28 self.tempdir = tempfile.mkdtemp()
29 self.repo = create_repo(
30 self.tempdir,
31 'https://github.com/ubuntu-core/snapcraft.git',
32 'master',
33 '')
34 self.fetch_retcode = self.repo.get()
35
36
37class SnappyTestRepo():
38 __metaclass__ = Singleton
39
40 def __init__(self):
41 self.tempdir = tempfile.mkdtemp()
42 self.repo = create_repo(
43 self.tempdir,
44 'https://github.com/ubuntu-core/snappy.git',
45 'master',
46 '')
47 self.fetch_retcode = self.repo.get()
48
49
50def tearDownModule():
51 shutil.rmtree(SnapcraftTestRepo().tempdir)
52 shutil.rmtree(SnappyTestRepo().tempdir)
53
54
55def db_empty_page_list():
56 Page.objects.all().delete()
57
58
59def db_create_home_page():
60 home = create_page('Test import', 'default.html', 'en', slug='home')
61 return home
62
63
64def db_add_empty_page(title, parent):
65 page = create_page(title, 'default.html', 'en', slug=slugify(title),
66 parent=parent)
67 return page
068
=== modified file 'requirements.txt'
--- requirements.txt 2015-12-11 16:10:30 +0000
+++ requirements.txt 2016-01-06 12:40:31 +0000
@@ -1,4 +1,4 @@
1Django==1.8.71Django==1.8.8
2django-template-debug==0.3.52django-template-debug==0.3.5
3oslo.config==3.1.03oslo.config==3.1.0
4oslo.i18n==3.1.04oslo.i18n==3.1.0
@@ -7,6 +7,7 @@
7Pillow==2.9.07Pillow==2.9.0
8cmsplugin-zinnia==0.88cmsplugin-zinnia==0.8
9Markdown==2.6.59Markdown==2.6.5
10pymdown-extensions==1.0.1
10beautifulsoup4==4.4.111beautifulsoup4==4.4.1
11dj-database-url==0.3.012dj-database-url==0.3.0
12django-admin-enhancer==1.0.013django-admin-enhancer==1.0.0
@@ -20,8 +21,8 @@
20django-meta==0.3.121django-meta==0.3.1
21django-meta-mixin==0.2.122django-meta-mixin==0.2.1
22django-missing==0.1.1523django-missing==0.1.15
23django-parler==1.5.124django-parler==1.6
24django-polymorphic==0.7.225django-polymorphic==0.8.1
25django-reversion==1.9.326django-reversion==1.9.3
26django-sekizai==0.9.027django-sekizai==0.9.0
27django-swiftstorage==1.1.028django-swiftstorage==1.1.0
@@ -31,11 +32,11 @@
31django-taggit-templatetags==0.2.532django-taggit-templatetags==0.2.5
32django-templatetag-sugar==1.033django-templatetag-sugar==1.0
33django-xmlrpc==0.1.534django-xmlrpc==0.1.5
34djangocms-admin-style==1.0.735djangocms-admin-style==1.0.8
35djangocms-link==1.7.136djangocms-link==1.7.1
36djangocms-picture==0.2.037djangocms-picture==0.2.0
37djangocms-snippet==1.7.138djangocms-snippet==1.7.1
38djangocms-text-ckeditor==2.8.039djangocms-text-ckeditor==2.8.1
39djangocms-utils==0.9.540djangocms-utils==0.9.5
40djangocms-video==0.2.041djangocms-video==0.2.0
41python-keystoneclient==1.3.342python-keystoneclient==1.3.3
@@ -49,4 +50,4 @@
49django-pygments==0.150django-pygments==0.1
50django-openid-auth==0.751django-openid-auth==0.7
51python-openid==2.2.552python-openid==2.2.5
52djangorestframework==3.3.153djangorestframework==3.3.2

Subscribers

People subscribed via source and target branches