Merge lp:~dholbach/developer-ubuntu-com/rework-importer into lp:~developer-ubuntu-com-dev/developer-ubuntu-com/django-1.8-cms-2.3
- rework-importer
- Merge into django-1.8-cms-2.3
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 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu App Developer site developers | Pending | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Holbach (dholbach) wrote : | # |
- 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
- 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 TestLocalBranch
Import - 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
1 | === added file 'TODO' | |||
2 | --- TODO 1970-01-01 00:00:00 +0000 | |||
3 | +++ TODO 2016-01-06 12:40:31 +0000 | |||
4 | @@ -0,0 +1,13 @@ | |||
5 | 1 | - fix branch indicator (devel vs. stable, 1.x vs devel, etc.) | ||
6 | 2 | |||
7 | 3 | - fix links | ||
8 | 4 | - add tests for links | ||
9 | 5 | |||
10 | 6 | - check if https://bugs.launchpad.net/developer-ubuntu-com/+bug/1531200 works | ||
11 | 7 | |||
12 | 8 | - import pictures (LP: #1511677) | ||
13 | 9 | - unpublished docs: some articles show up with a blue circle, so | ||
14 | 10 | unpublished although they're accessible(?) | ||
15 | 11 | - add comment at top of RawHTML plugins, so users know not to edit them, | ||
16 | 12 | currently blocked on LP: #1523925 | ||
17 | 13 | - if article already exists, check if HTML actually changed, if not, don't update. | ||
18 | 0 | 14 | ||
19 | === modified file 'developer_portal/admin.py' | |||
20 | --- developer_portal/admin.py 2015-11-02 12:17:27 +0000 | |||
21 | +++ developer_portal/admin.py 2016-01-06 12:40:31 +0000 | |||
22 | @@ -4,22 +4,12 @@ | |||
23 | 4 | from reversion.admin import VersionAdmin | 4 | from reversion.admin import VersionAdmin |
24 | 5 | 5 | ||
25 | 6 | from cms.extensions import TitleExtensionAdmin | 6 | from cms.extensions import TitleExtensionAdmin |
28 | 7 | from .models import ExternalDocsBranch, SEOExtension | 7 | from .models import SEOExtension |
27 | 8 | from django.core.management import call_command | ||
29 | 9 | 8 | ||
30 | 10 | __all__ = ( | 9 | __all__ = ( |
31 | 11 | ) | 10 | ) |
32 | 12 | 11 | ||
33 | 13 | 12 | ||
34 | 14 | def import_selected_external_docs_branches(modeladmin, request, queryset): | ||
35 | 15 | branches = [] | ||
36 | 16 | for branch in queryset: | ||
37 | 17 | branches.append(branch.docs_namespace) | ||
38 | 18 | call_command('import-external-docs-branches', *branches) | ||
39 | 19 | import_selected_external_docs_branches.short_description = \ | ||
40 | 20 | "Import selected branches" | ||
41 | 21 | |||
42 | 22 | |||
43 | 23 | class RevisionAdmin(admin.ModelAdmin): | 13 | class RevisionAdmin(admin.ModelAdmin): |
44 | 24 | list_display = ('date_created', 'user', 'comment') | 14 | list_display = ('date_created', 'user', 'comment') |
45 | 25 | list_display_links = ('date_created', ) | 15 | list_display_links = ('date_created', ) |
46 | @@ -37,13 +27,6 @@ | |||
47 | 37 | admin.site.register(Version, VersionAdmin) | 27 | admin.site.register(Version, VersionAdmin) |
48 | 38 | 28 | ||
49 | 39 | 29 | ||
50 | 40 | class ExternalDocsBranchAdmin(admin.ModelAdmin): | ||
51 | 41 | list_display = ('lp_origin', 'docs_namespace') | ||
52 | 42 | list_filter = ('lp_origin', 'docs_namespace') | ||
53 | 43 | actions = [import_selected_external_docs_branches] | ||
54 | 44 | |||
55 | 45 | admin.site.register(ExternalDocsBranch, ExternalDocsBranchAdmin) | ||
56 | 46 | |||
57 | 47 | class SEOExtensionAdmin(TitleExtensionAdmin): | 30 | class SEOExtensionAdmin(TitleExtensionAdmin): |
58 | 48 | pass | 31 | pass |
59 | 49 | 32 | ||
60 | 50 | 33 | ||
61 | === modified file 'developer_portal/migrations/0001_initial.py' | |||
62 | --- developer_portal/migrations/0001_initial.py 2015-11-17 13:38:11 +0000 | |||
63 | +++ developer_portal/migrations/0001_initial.py 2016-01-06 12:40:31 +0000 | |||
64 | @@ -11,15 +11,6 @@ | |||
65 | 11 | 11 | ||
66 | 12 | operations = [ | 12 | operations = [ |
67 | 13 | migrations.CreateModel( | 13 | migrations.CreateModel( |
68 | 14 | name='ExternalDocsBranch', | ||
69 | 15 | fields=[ | ||
70 | 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | ||
71 | 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)), | ||
72 | 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)), | ||
73 | 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)), | ||
74 | 20 | ], | ||
75 | 21 | ), | ||
76 | 22 | migrations.CreateModel( | ||
77 | 23 | name='RawHtml', | 14 | name='RawHtml', |
78 | 24 | fields=[ | 15 | fields=[ |
79 | 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')), |
80 | 26 | 17 | ||
81 | === modified file 'developer_portal/models.py' | |||
82 | --- developer_portal/models.py 2015-11-02 16:47:26 +0000 | |||
83 | +++ developer_portal/models.py 2016-01-06 12:40:31 +0000 | |||
84 | @@ -1,7 +1,5 @@ | |||
85 | 1 | from django.db import models | 1 | from django.db import models |
86 | 2 | from django.utils.translation import ugettext_lazy as _ | ||
87 | 3 | 2 | ||
88 | 4 | from cms.models import CMSPlugin | ||
89 | 5 | from cms.extensions import TitleExtension | 3 | from cms.extensions import TitleExtension |
90 | 6 | from cms.extensions.extension_pool import extension_pool | 4 | from cms.extensions.extension_pool import extension_pool |
91 | 7 | from djangocms_text_ckeditor.html import extract_images | 5 | from djangocms_text_ckeditor.html import extract_images |
92 | @@ -20,25 +18,6 @@ | |||
93 | 20 | AbstractText.save(self, *args, **kwargs) | 18 | AbstractText.save(self, *args, **kwargs) |
94 | 21 | 19 | ||
95 | 22 | 20 | ||
96 | 23 | class ExternalDocsBranch(models.Model): | ||
97 | 24 | # We originally assumed that branches would also live in LP, | ||
98 | 25 | # well, we were wrong, but let's keep the name around. It's | ||
99 | 26 | # no use having a schema/data migration just for this. | ||
100 | 27 | lp_origin = models.CharField( | ||
101 | 28 | max_length=200, | ||
102 | 29 | help_text=_('External branch location, ie: lp:snappy/15.04 or ' | ||
103 | 30 | 'https://github.com/ubuntu-core/snappy.git')) | ||
104 | 31 | docs_namespace = models.CharField( | ||
105 | 32 | max_length=120, | ||
106 | 33 | help_text=_('Path alias we want to use for the docs, ' | ||
107 | 34 | 'ie "snappy/guides/15.04" or ' | ||
108 | 35 | '"snappy/guides/latest", etc.')) | ||
109 | 36 | index_doc = models.CharField( | ||
110 | 37 | max_length=120, | ||
111 | 38 | help_text=_('File name of doc to be used as index document, ' | ||
112 | 39 | 'ie "intro.md"'), | ||
113 | 40 | blank=True) | ||
114 | 41 | |||
115 | 42 | class SEOExtension(TitleExtension): | 21 | class SEOExtension(TitleExtension): |
116 | 43 | keywords = models.CharField(max_length=256) | 22 | keywords = models.CharField(max_length=256) |
117 | 44 | 23 | ||
118 | 45 | 24 | ||
119 | === modified file 'developer_portal/settings.py' | |||
120 | --- developer_portal/settings.py 2015-12-11 16:10:37 +0000 | |||
121 | +++ developer_portal/settings.py 2016-01-06 12:40:31 +0000 | |||
122 | @@ -76,6 +76,8 @@ | |||
123 | 76 | 'store_data', | 76 | 'store_data', |
124 | 77 | 77 | ||
125 | 78 | 'api_docs', | 78 | 'api_docs', |
126 | 79 | |||
127 | 80 | 'md_importer', | ||
128 | 79 | ] | 81 | ] |
129 | 80 | 82 | ||
130 | 81 | MIDDLEWARE_CLASSES = ( | 83 | MIDDLEWARE_CLASSES = ( |
131 | 82 | 84 | ||
132 | === modified file 'locale/de.po' | |||
133 | --- locale/de.po 2015-11-24 05:38:32 +0000 | |||
134 | +++ locale/de.po 2016-01-06 12:40:31 +0000 | |||
135 | @@ -15,8 +15,8 @@ | |||
136 | 15 | "Content-Type: text/plain; charset=UTF-8\n" | 15 | "Content-Type: text/plain; charset=UTF-8\n" |
137 | 16 | "Content-Transfer-Encoding: 8bit\n" | 16 | "Content-Transfer-Encoding: 8bit\n" |
138 | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
141 | 18 | "X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n" | 18 | "X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n" |
142 | 19 | "X-Generator: Launchpad (build 17850)\n" | 19 | "X-Generator: Launchpad (build 17856)\n" |
143 | 20 | 20 | ||
144 | 21 | #: developer_portal/cms_plugins.py:10 | 21 | #: developer_portal/cms_plugins.py:10 |
145 | 22 | msgid "Raw HTML" | 22 | msgid "Raw HTML" |
146 | 23 | 23 | ||
147 | === modified file 'locale/en_GB.po' | |||
148 | --- locale/en_GB.po 2015-11-24 05:38:32 +0000 | |||
149 | +++ locale/en_GB.po 2016-01-06 12:40:31 +0000 | |||
150 | @@ -15,8 +15,8 @@ | |||
151 | 15 | "Content-Type: text/plain; charset=UTF-8\n" | 15 | "Content-Type: text/plain; charset=UTF-8\n" |
152 | 16 | "Content-Transfer-Encoding: 8bit\n" | 16 | "Content-Transfer-Encoding: 8bit\n" |
153 | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
156 | 18 | "X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n" | 18 | "X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n" |
157 | 19 | "X-Generator: Launchpad (build 17850)\n" | 19 | "X-Generator: Launchpad (build 17856)\n" |
158 | 20 | 20 | ||
159 | 21 | #: developer_portal/cms_plugins.py:10 | 21 | #: developer_portal/cms_plugins.py:10 |
160 | 22 | msgid "Raw HTML" | 22 | msgid "Raw HTML" |
161 | 23 | 23 | ||
162 | === modified file 'locale/es.po' | |||
163 | --- locale/es.po 2015-11-24 05:38:32 +0000 | |||
164 | +++ locale/es.po 2016-01-06 12:40:31 +0000 | |||
165 | @@ -15,8 +15,8 @@ | |||
166 | 15 | "Content-Type: text/plain; charset=UTF-8\n" | 15 | "Content-Type: text/plain; charset=UTF-8\n" |
167 | 16 | "Content-Transfer-Encoding: 8bit\n" | 16 | "Content-Transfer-Encoding: 8bit\n" |
168 | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" | 17 | "Plural-Forms: nplurals=2; plural=n != 1;\n" |
171 | 18 | "X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n" | 18 | "X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n" |
172 | 19 | "X-Generator: Launchpad (build 17850)\n" | 19 | "X-Generator: Launchpad (build 17856)\n" |
173 | 20 | 20 | ||
174 | 21 | #: developer_portal/cms_plugins.py:10 | 21 | #: developer_portal/cms_plugins.py:10 |
175 | 22 | msgid "Raw HTML" | 22 | msgid "Raw HTML" |
176 | 23 | 23 | ||
177 | === modified file 'locale/ug.po' | |||
178 | --- locale/ug.po 2015-11-24 05:38:32 +0000 | |||
179 | +++ locale/ug.po 2016-01-06 12:40:31 +0000 | |||
180 | @@ -15,8 +15,8 @@ | |||
181 | 15 | "Content-Type: text/plain; charset=UTF-8\n" | 15 | "Content-Type: text/plain; charset=UTF-8\n" |
182 | 16 | "Content-Transfer-Encoding: 8bit\n" | 16 | "Content-Transfer-Encoding: 8bit\n" |
183 | 17 | "Plural-Forms: nplurals=1; plural=0;\n" | 17 | "Plural-Forms: nplurals=1; plural=0;\n" |
186 | 18 | "X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n" | 18 | "X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n" |
187 | 19 | "X-Generator: Launchpad (build 17850)\n" | 19 | "X-Generator: Launchpad (build 17856)\n" |
188 | 20 | 20 | ||
189 | 21 | #: developer_portal/cms_plugins.py:10 | 21 | #: developer_portal/cms_plugins.py:10 |
190 | 22 | msgid "Raw HTML" | 22 | msgid "Raw HTML" |
191 | 23 | 23 | ||
192 | === modified file 'locale/zh_CN.po' | |||
193 | --- locale/zh_CN.po 2015-11-24 05:38:32 +0000 | |||
194 | +++ locale/zh_CN.po 2016-01-06 12:40:31 +0000 | |||
195 | @@ -15,8 +15,8 @@ | |||
196 | 15 | "Content-Type: text/plain; charset=UTF-8\n" | 15 | "Content-Type: text/plain; charset=UTF-8\n" |
197 | 16 | "Content-Transfer-Encoding: 8bit\n" | 16 | "Content-Transfer-Encoding: 8bit\n" |
198 | 17 | "Plural-Forms: nplurals=1; plural=0;\n" | 17 | "Plural-Forms: nplurals=1; plural=0;\n" |
201 | 18 | "X-Launchpad-Export-Date: 2015-11-24 05:38+0000\n" | 18 | "X-Launchpad-Export-Date: 2015-12-03 06:16+0000\n" |
202 | 19 | "X-Generator: Launchpad (build 17850)\n" | 19 | "X-Generator: Launchpad (build 17856)\n" |
203 | 20 | 20 | ||
204 | 21 | #: developer_portal/cms_plugins.py:10 | 21 | #: developer_portal/cms_plugins.py:10 |
205 | 22 | msgid "Raw HTML" | 22 | msgid "Raw HTML" |
206 | 23 | 23 | ||
207 | === added directory 'md_importer' | |||
208 | === added file 'md_importer/__init__.py' | |||
209 | === added file 'md_importer/admin.py' | |||
210 | --- md_importer/admin.py 1970-01-01 00:00:00 +0000 | |||
211 | +++ md_importer/admin.py 2016-01-06 12:40:31 +0000 | |||
212 | @@ -0,0 +1,36 @@ | |||
213 | 1 | from django.contrib import admin | ||
214 | 2 | |||
215 | 3 | from .models import ( | ||
216 | 4 | ExternalDocsBranch, ExternalDocsBranchImportDirective, | ||
217 | 5 | ImportedArticle, | ||
218 | 6 | ) | ||
219 | 7 | from django.core.management import call_command | ||
220 | 8 | |||
221 | 9 | __all__ = ( | ||
222 | 10 | ) | ||
223 | 11 | |||
224 | 12 | |||
225 | 13 | def import_selected_external_docs_branches(modeladmin, request, queryset): | ||
226 | 14 | branches = [] | ||
227 | 15 | for branch in queryset: | ||
228 | 16 | branches.append(branch.origin) | ||
229 | 17 | call_command('import-external-docs-branches', *branches) | ||
230 | 18 | import_selected_external_docs_branches.short_description = \ | ||
231 | 19 | "Import selected branches" | ||
232 | 20 | |||
233 | 21 | |||
234 | 22 | @admin.register(ExternalDocsBranch) | ||
235 | 23 | class ExternalDocsBranchAdmin(admin.ModelAdmin): | ||
236 | 24 | list_display = ('origin', 'post_checkout_command', 'branch_name',) | ||
237 | 25 | list_filter = ('origin', 'post_checkout_command', 'branch_name',) | ||
238 | 26 | actions = [import_selected_external_docs_branches] | ||
239 | 27 | |||
240 | 28 | |||
241 | 29 | @admin.register(ExternalDocsBranchImportDirective) | ||
242 | 30 | class ExternalDocsBranchImportDirectiveAdmin(admin.ModelAdmin): | ||
243 | 31 | pass | ||
244 | 32 | |||
245 | 33 | |||
246 | 34 | @admin.register(ImportedArticle) | ||
247 | 35 | class ImportedArticleAdmin(admin.ModelAdmin): | ||
248 | 36 | pass | ||
249 | 0 | 37 | ||
250 | === added directory 'md_importer/management' | |||
251 | === added file 'md_importer/management/__init__.py' | |||
252 | === added directory 'md_importer/management/commands' | |||
253 | === added file 'md_importer/management/commands/__init__.py' | |||
254 | === renamed file 'developer_portal/management/commands/import-external-docs-branches.py' => 'md_importer/management/commands/import-external-docs-branches.py' | |||
255 | --- developer_portal/management/commands/import-external-docs-branches.py 2015-11-06 09:48:07 +0000 | |||
256 | +++ md_importer/management/commands/import-external-docs-branches.py 2016-01-06 12:40:31 +0000 | |||
257 | @@ -1,300 +1,18 @@ | |||
258 | 1 | from django.core.management.base import BaseCommand | 1 | from django.core.management.base import BaseCommand |
259 | 2 | from django.core.management import call_command | 2 | from django.core.management import call_command |
269 | 3 | from django.db import transaction | 3 | |
270 | 4 | 4 | from ..importer.repo import create_repo | |
271 | 5 | from cms.api import create_page, add_plugin | 5 | |
272 | 6 | from cms.models import Page, Title | 6 | import datetime |
264 | 7 | from cms.utils import page_resolver | ||
265 | 8 | |||
266 | 9 | from bs4 import BeautifulSoup | ||
267 | 10 | import codecs | ||
268 | 11 | import glob | ||
273 | 12 | import logging | 7 | import logging |
274 | 13 | import markdown | ||
275 | 14 | import os | ||
276 | 15 | import re | ||
277 | 16 | import shutil | 8 | import shutil |
278 | 17 | import subprocess | ||
279 | 18 | import sys | ||
280 | 19 | import tempfile | 9 | import tempfile |
281 | 20 | 10 | ||
559 | 21 | from developer_portal.models import ExternalDocsBranch | 11 | from developer_portal.models import ( |
560 | 22 | 12 | ExternalDocsBranch, | |
561 | 23 | DOCS_DIRNAME = 'docs' | 13 | ExternalDocsBranchImportDirective, |
562 | 24 | 14 | ImportedArticle, | |
563 | 25 | 15 | ) | |
287 | 26 | class DBActions: | ||
288 | 27 | added_pages = [] | ||
289 | 28 | removed_pages = [] | ||
290 | 29 | |||
291 | 30 | def add_page(self, **kwargs): | ||
292 | 31 | self.added_pages += [kwargs] | ||
293 | 32 | |||
294 | 33 | def remove_page(self, page_id): | ||
295 | 34 | self.removed_pages += [page_id] | ||
296 | 35 | |||
297 | 36 | @transaction.atomic() | ||
298 | 37 | def run(self): | ||
299 | 38 | for added_page in self.added_pages: | ||
300 | 39 | page = get_or_create_page(**added_page) | ||
301 | 40 | page.publish('en') | ||
302 | 41 | |||
303 | 42 | # Only remove pages created by a script! | ||
304 | 43 | Page.objects.filter(id__in=self.removed_pages, | ||
305 | 44 | created_by="script").delete() | ||
306 | 45 | |||
307 | 46 | # https://stackoverflow.com/questions/33284171/ | ||
308 | 47 | call_command('cms', 'fix-tree') | ||
309 | 48 | |||
310 | 49 | |||
311 | 50 | class MarkdownFile: | ||
312 | 51 | html = None | ||
313 | 52 | |||
314 | 53 | def __init__(self, fn, docs_namespace, db_actions, slug_override=None): | ||
315 | 54 | self.fn = fn | ||
316 | 55 | self.docs_namespace = docs_namespace | ||
317 | 56 | self.db_actions = db_actions | ||
318 | 57 | if slug_override: | ||
319 | 58 | self.slug = slug_override | ||
320 | 59 | else: | ||
321 | 60 | self.slug = slugify(self.fn) | ||
322 | 61 | self.full_url = os.path.join(self.docs_namespace, self.slug) | ||
323 | 62 | with codecs.open(self.fn, 'r', encoding='utf-8') as f: | ||
324 | 63 | self.html = markdown.markdown( | ||
325 | 64 | f.read(), | ||
326 | 65 | output_format="html5", | ||
327 | 66 | extensions=['markdown.extensions.tables']) | ||
328 | 67 | self.release_alias = self._get_release_alias() | ||
329 | 68 | self.title = self._read_title() | ||
330 | 69 | self._remove_body_and_html_tags() | ||
331 | 70 | self._use_developer_site_style() | ||
332 | 71 | |||
333 | 72 | def _get_release_alias(self): | ||
334 | 73 | alias = re.findall(r'/tmp/tmp\S+?/(\S+?)/%s/\S+?' % DOCS_DIRNAME, | ||
335 | 74 | self.fn) | ||
336 | 75 | return alias[0] | ||
337 | 76 | |||
338 | 77 | def _read_title(self): | ||
339 | 78 | soup = BeautifulSoup(self.html, 'html5lib') | ||
340 | 79 | if soup.title: | ||
341 | 80 | return soup.title.text | ||
342 | 81 | if soup.h1: | ||
343 | 82 | return soup.h1.text | ||
344 | 83 | return slugify(self.fn).replace('-', ' ').title() | ||
345 | 84 | |||
346 | 85 | def _remove_body_and_html_tags(self): | ||
347 | 86 | self.html = re.sub(r"<html>\n\s<body>\n", "", self.html, | ||
348 | 87 | flags=re.MULTILINE) | ||
349 | 88 | self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html, | ||
350 | 89 | flags=re.MULTILINE) | ||
351 | 90 | |||
352 | 91 | def _use_developer_site_style(self): | ||
353 | 92 | begin = (u"<div class=\"row no-border\">" | ||
354 | 93 | "\n<div class=\"eight-col\">\n") | ||
355 | 94 | end = u"</div>\n</div>" | ||
356 | 95 | self.html = begin + self.html + end | ||
357 | 96 | self.html = self.html.replace( | ||
358 | 97 | "<pre><code>", | ||
359 | 98 | "</div><div class=\"twelve-col\"><pre><code>") | ||
360 | 99 | self.html = self.html.replace( | ||
361 | 100 | "</code></pre>", | ||
362 | 101 | "</code></pre></div><div class=\"eight-col\">") | ||
363 | 102 | |||
364 | 103 | def replace_links(self, titles, url_map): | ||
365 | 104 | for title in titles: | ||
366 | 105 | local_md_fn = os.path.basename(title) | ||
367 | 106 | url = u'/'+url_map[title] | ||
368 | 107 | # Replace links of the form <a href="/path/somefile.md"> first | ||
369 | 108 | href = u"<a href=\"{}\">".format(url) | ||
370 | 109 | md_href = u"<a href=\"{}\">".format(local_md_fn) | ||
371 | 110 | self.html = self.html.replace(md_href, href) | ||
372 | 111 | |||
373 | 112 | # Now we can replace free-standing "somefile.md" references in | ||
374 | 113 | # the HTML | ||
375 | 114 | link = href + u"{}</a>".format(titles[title]) | ||
376 | 115 | self.html = self.html.replace(local_md_fn, link) | ||
377 | 116 | |||
378 | 117 | def publish(self): | ||
379 | 118 | '''Publishes pages in their branch alias namespace.''' | ||
380 | 119 | self.db_actions.add_page( | ||
381 | 120 | title=self.title, full_url=self.full_url, menu_title=self.title, | ||
382 | 121 | html=self.html) | ||
383 | 122 | |||
384 | 123 | |||
385 | 124 | class SnappyMarkdownFile(MarkdownFile): | ||
386 | 125 | def __init__(self, fn, docs_namespace, db_actions): | ||
387 | 126 | MarkdownFile.__init__(self, fn, docs_namespace, db_actions) | ||
388 | 127 | self._make_snappy_mods() | ||
389 | 128 | |||
390 | 129 | def _make_snappy_mods(self): | ||
391 | 130 | # Make sure the reader knows which documentation she is browsing | ||
392 | 131 | if self.release_alias != 'current': | ||
393 | 132 | before = (u"<div class=\"row no-border\">\n" | ||
394 | 133 | "<div class=\"eight-col\">\n") | ||
395 | 134 | after = (u"<div class=\"row no-border\">\n" | ||
396 | 135 | "<div class=\"box pull-three three-col\">" | ||
397 | 136 | "<p>You are browsing the Snappy <code>%s</code> " | ||
398 | 137 | "documentation.</p>" | ||
399 | 138 | "<p><a href=\"/snappy/guides/current/%s\">" | ||
400 | 139 | "Back to the latest stable release ›" | ||
401 | 140 | "</a></p></div>\n" | ||
402 | 141 | "<div class=\"eight-col\">\n") % (self.release_alias, | ||
403 | 142 | self.slug, ) | ||
404 | 143 | self.html = self.html.replace(before, after) | ||
405 | 144 | |||
406 | 145 | def publish(self): | ||
407 | 146 | if self.release_alias == "current": | ||
408 | 147 | # Add a guides/<page> redirect to guides/current/<page> | ||
409 | 148 | self.db_actions.add_page( | ||
410 | 149 | title=self.title, | ||
411 | 150 | full_url=self.full_url.replace('/current', ''), | ||
412 | 151 | redirect="/snappy/guides/current/%s" % (self.slug)) | ||
413 | 152 | else: | ||
414 | 153 | self.title += " (%s)" % (self.release_alias,) | ||
415 | 154 | MarkdownFile.publish(self) | ||
416 | 155 | |||
417 | 156 | |||
418 | 157 | def slugify(filename): | ||
419 | 158 | return os.path.basename(filename).replace('.md', '') | ||
420 | 159 | |||
421 | 160 | |||
422 | 161 | class LocalBranch: | ||
423 | 162 | titles = {} | ||
424 | 163 | url_map = {} | ||
425 | 164 | |||
426 | 165 | def __init__(self, dirname, external_branch, db_actions): | ||
427 | 166 | self.dirname = dirname | ||
428 | 167 | self.docs_path = os.path.join(self.dirname, DOCS_DIRNAME) | ||
429 | 168 | self.doc_fns = glob.glob(self.docs_path+'/*.md') | ||
430 | 169 | self.md_files = [] | ||
431 | 170 | self.external_branch = external_branch | ||
432 | 171 | self.docs_namespace = self.external_branch.docs_namespace | ||
433 | 172 | self.release_alias = os.path.basename(self.docs_namespace) | ||
434 | 173 | self.index_doc_title = self.release_alias.title() | ||
435 | 174 | self.index_doc = self.external_branch.index_doc | ||
436 | 175 | self.db_actions = db_actions | ||
437 | 176 | self.markdown_class = MarkdownFile | ||
438 | 177 | |||
439 | 178 | def import_markdown(self): | ||
440 | 179 | for doc_fn in self.doc_fns: | ||
441 | 180 | if self.index_doc and os.path.basename(doc_fn) == self.index_doc: | ||
442 | 181 | md_file = self.markdown_class( | ||
443 | 182 | doc_fn, | ||
444 | 183 | os.path.dirname(self.docs_namespace), | ||
445 | 184 | self.db_actions, | ||
446 | 185 | slug_override=os.path.basename(self.docs_namespace)) | ||
447 | 186 | self.md_files.insert(0, md_file) | ||
448 | 187 | else: | ||
449 | 188 | md_file = self.markdown_class(doc_fn, self.docs_namespace, | ||
450 | 189 | self.db_actions) | ||
451 | 190 | self.md_files += [md_file] | ||
452 | 191 | self.titles[md_file.fn] = md_file.title | ||
453 | 192 | self.url_map[md_file.fn] = md_file.full_url | ||
454 | 193 | if not self.index_doc: | ||
455 | 194 | self._create_fake_index_doc() | ||
456 | 195 | for md_file in self.md_files: | ||
457 | 196 | md_file.replace_links(self.titles, self.url_map) | ||
458 | 197 | |||
459 | 198 | def remove_old_pages(self): | ||
460 | 199 | imported_page_urls = set([md_file.full_url | ||
461 | 200 | for md_file in self.md_files]) | ||
462 | 201 | index_doc = page_resolver.get_page_queryset_from_path( | ||
463 | 202 | self.docs_namespace) | ||
464 | 203 | db_pages = [] | ||
465 | 204 | if len(index_doc): | ||
466 | 205 | # All pages in this namespace currently in the database | ||
467 | 206 | db_pages = index_doc[0].get_descendants().all() | ||
468 | 207 | for db_page in db_pages: | ||
469 | 208 | still_relevant = False | ||
470 | 209 | for url in imported_page_urls: | ||
471 | 210 | if url in db_page.get_absolute_url(): | ||
472 | 211 | still_relevant = True | ||
473 | 212 | break | ||
474 | 213 | # At this point we know that there's no match and the page | ||
475 | 214 | # can be deleted. | ||
476 | 215 | if not still_relevant: | ||
477 | 216 | self.db_actions.remove_page(db_page.id) | ||
478 | 217 | |||
479 | 218 | def publish(self): | ||
480 | 219 | for md_file in self.md_files: | ||
481 | 220 | md_file.publish() | ||
482 | 221 | |||
483 | 222 | def _create_fake_index_doc(self): | ||
484 | 223 | '''Creates a fake index page at the top of the branches | ||
485 | 224 | docs namespace.''' | ||
486 | 225 | |||
487 | 226 | if self.docs_namespace == "current": | ||
488 | 227 | redirect = "/snappy/guides" | ||
489 | 228 | else: | ||
490 | 229 | redirect = None | ||
491 | 230 | |||
492 | 231 | in_navigation = False | ||
493 | 232 | menu_title = None | ||
494 | 233 | list_pages = "" | ||
495 | 234 | for page in self.md_files: | ||
496 | 235 | list_pages += "<li><a href=\"%s\">%s</a></li>" \ | ||
497 | 236 | % (os.path.basename(page.full_url), page.title) | ||
498 | 237 | landing = ( | ||
499 | 238 | u"<div class=\"row\"><div class=\"eight-col\">\n" | ||
500 | 239 | "<p>This section contains documentation for the " | ||
501 | 240 | "<code>%s</code> Snappy branch.</p>" | ||
502 | 241 | "<p><ul class=\"list-ubuntu\">%s</ul></p>\n" | ||
503 | 242 | "<p>Auto-imported from <a " | ||
504 | 243 | "href=\"https://github.com/ubuntu-core/snappy\">%s</a>.</p>\n" | ||
505 | 244 | "</div></div>") % (self.release_alias, list_pages, | ||
506 | 245 | self.external_branch.lp_origin) | ||
507 | 246 | self.db_actions.add_page( | ||
508 | 247 | title=self.index_doc_title, full_url=self.docs_namespace, | ||
509 | 248 | in_navigation=in_navigation, redirect=redirect, html=landing, | ||
510 | 249 | menu_title=menu_title) | ||
511 | 250 | |||
512 | 251 | |||
513 | 252 | class SnappyLocalBranch(LocalBranch): | ||
514 | 253 | def __init__(self, dirname, external_branch, db_actions): | ||
515 | 254 | LocalBranch.__init__(self, dirname, external_branch, db_actions) | ||
516 | 255 | self.markdown_class = SnappyMarkdownFile | ||
517 | 256 | self.index_doc_title = 'Snappy documentation' | ||
518 | 257 | if self.release_alias != 'current': | ||
519 | 258 | self.index_doc_title += ' (%s)' % self.release_alias | ||
520 | 259 | |||
521 | 260 | |||
522 | 261 | def get_or_create_page(title, full_url, menu_title=None, | ||
523 | 262 | in_navigation=True, redirect=None, html=None): | ||
524 | 263 | # First check if pages already exist. | ||
525 | 264 | pages = Title.objects.select_related('page').filter(path__regex=full_url) | ||
526 | 265 | if pages: | ||
527 | 266 | page = pages[0].page | ||
528 | 267 | page.title = title | ||
529 | 268 | page.publisher_is_draft = True | ||
530 | 269 | page.menu_title = menu_title | ||
531 | 270 | page.in_navigation = in_navigation | ||
532 | 271 | page.redirect = redirect | ||
533 | 272 | if html: | ||
534 | 273 | # We create the page, so we know there's just one placeholder | ||
535 | 274 | placeholder = page.placeholders.all()[0] | ||
536 | 275 | if placeholder.get_plugins(): | ||
537 | 276 | plugin = placeholder.get_plugins()[0].get_plugin_instance()[0] | ||
538 | 277 | plugin.body = html | ||
539 | 278 | plugin.save() | ||
540 | 279 | else: | ||
541 | 280 | add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) | ||
542 | 281 | else: | ||
543 | 282 | parent_pages = Title.objects.select_related('page').filter( | ||
544 | 283 | path__regex=os.path.dirname(full_url)) | ||
545 | 284 | if not parent_pages: | ||
546 | 285 | print('Parent %s not found.' % os.path.dirname(full_url)) | ||
547 | 286 | sys.exit(1) | ||
548 | 287 | parent = parent_pages[0].page | ||
549 | 288 | |||
550 | 289 | slug = os.path.basename(full_url) | ||
551 | 290 | page = create_page( | ||
552 | 291 | title, "default.html", "en", slug=slug, parent=parent, | ||
553 | 292 | menu_title=menu_title, in_navigation=in_navigation, | ||
554 | 293 | position="last-child", redirect=redirect) | ||
555 | 294 | if html: | ||
556 | 295 | placeholder = page.placeholders.get() | ||
557 | 296 | add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) | ||
558 | 297 | return page | ||
564 | 298 | 16 | ||
565 | 299 | 17 | ||
566 | 300 | def import_branches(selection): | 18 | def import_branches(selection): |
567 | @@ -302,52 +20,34 @@ | |||
568 | 302 | logging.error('No branches registered in the ' | 20 | logging.error('No branches registered in the ' |
569 | 303 | 'ExternalDocsBranch table yet.') | 21 | 'ExternalDocsBranch table yet.') |
570 | 304 | return | 22 | return |
571 | 305 | tempdir = tempfile.mkdtemp() | ||
572 | 306 | db_actions = DBActions() | ||
573 | 307 | for branch in ExternalDocsBranch.objects.filter( | 23 | for branch in ExternalDocsBranch.objects.filter( |
583 | 308 | docs_namespace__regex=selection): | 24 | origin__regex=selection, active=True): |
584 | 309 | checkout_location = os.path.join( | 25 | tempdir = tempfile.mkdtemp() |
585 | 310 | tempdir, os.path.basename(branch.docs_namespace)) | 26 | repo = create_repo(tempdir, branch.origin, branch.branch_name, |
586 | 311 | sourcecode = SourceCode(branch.lp_origin, checkout_location) | 27 | branch.post_checkout_command) |
587 | 312 | if sourcecode.get() != 0: | 28 | if repo.get() != 0: |
579 | 313 | logging.error( | ||
580 | 314 | 'Could not check out branch "%s".' % branch.lp_origin) | ||
581 | 315 | if os.path.exists(checkout_location): | ||
582 | 316 | shutil.rmtree(checkout_location) | ||
588 | 317 | break | 29 | break |
622 | 318 | if branch.lp_origin.startswith('lp:snappy') or \ | 30 | for directive in ExternalDocsBranchImportDirective.objects.filter( |
623 | 319 | 'snappy' in branch.lp_origin.split(':')[1].split('.git')[0].split('/'): | 31 | external_docs_branch=branch): |
624 | 320 | local_branch = SnappyLocalBranch(checkout_location, branch, | 32 | repo.add_directive(directive.import_from, |
625 | 321 | db_actions) | 33 | directive.write_to) |
626 | 322 | else: | 34 | repo.execute_import_directives() |
627 | 323 | local_branch = LocalBranch(checkout_location, branch, db_actions) | 35 | repo.publish() |
628 | 324 | local_branch.import_markdown() | 36 | for page in repo.pages: |
629 | 325 | local_branch.publish() | 37 | ImportedArticle.objects.get_or_create( |
630 | 326 | local_branch.remove_old_pages() | 38 | branch=branch, |
631 | 327 | shutil.rmtree(tempdir) | 39 | page=page, |
632 | 328 | db_actions.run() | 40 | last_import=datetime.datetime.now()) |
633 | 329 | 41 | ||
634 | 330 | 42 | # The import is done, now let's clean up. | |
635 | 331 | class SourceCode(): | 43 | for old_article in ImportedArticle.objects.filter(branch=branch): |
636 | 332 | def __init__(self, branch_origin, checkout_location): | 44 | if old_article.page not in repo.pages and \ |
637 | 333 | self.branch_origin = branch_origin | 45 | old_article.page.changed_by in ['python-api', 'script']: |
638 | 334 | self.checkout_location = checkout_location | 46 | old_article.page.delete() |
639 | 335 | 47 | shutil.rmtree(tempdir) | |
640 | 336 | def get(self): | 48 | |
641 | 337 | if self.branch_origin.startswith('lp:') and \ | 49 | # https://stackoverflow.com/questions/33284171/ |
642 | 338 | os.path.exists('/usr/bin/bzr'): | 50 | call_command('cms', 'fix-tree') |
610 | 339 | return subprocess.call([ | ||
611 | 340 | 'bzr', 'checkout', '--lightweight', self.branch_origin, | ||
612 | 341 | self.checkout_location]) | ||
613 | 342 | if self.branch_origin.startswith('https://github.com') and \ | ||
614 | 343 | self.branch_origin.endswith('.git') and \ | ||
615 | 344 | os.path.exists('/usr/bin/git'): | ||
616 | 345 | return subprocess.call([ | ||
617 | 346 | 'git', 'clone', '-q', self.branch_origin, | ||
618 | 347 | self.checkout_location]) | ||
619 | 348 | logging.error( | ||
620 | 349 | 'Branch format "{}" not understood.'.format(self.branch_origin)) | ||
621 | 350 | return 1 | ||
643 | 351 | 51 | ||
644 | 352 | 52 | ||
645 | 353 | class Command(BaseCommand): | 53 | class Command(BaseCommand): |
646 | 354 | 54 | ||
647 | === added directory 'md_importer/management/importer' | |||
648 | === added file 'md_importer/management/importer/__init__.py' | |||
649 | === added file 'md_importer/management/importer/article.py' | |||
650 | --- md_importer/management/importer/article.py 1970-01-01 00:00:00 +0000 | |||
651 | +++ md_importer/management/importer/article.py 2016-01-06 12:40:31 +0000 | |||
652 | @@ -0,0 +1,118 @@ | |||
653 | 1 | from bs4 import BeautifulSoup | ||
654 | 2 | import codecs | ||
655 | 3 | import logging | ||
656 | 4 | import markdown | ||
657 | 5 | import os | ||
658 | 6 | import re | ||
659 | 7 | |||
660 | 8 | from .publish import get_or_create_page, slugify | ||
661 | 9 | |||
662 | 10 | |||
663 | 11 | class Article: | ||
664 | 12 | html = None | ||
665 | 13 | page = None | ||
666 | 14 | title = "" | ||
667 | 15 | |||
668 | 16 | def __init__(self, fn, write_to): | ||
669 | 17 | self.fn = fn | ||
670 | 18 | self.write_to = slugify(self.fn) | ||
671 | 19 | self.full_url = write_to | ||
672 | 20 | self.slug = os.path.basename(self.full_url) | ||
673 | 21 | |||
674 | 22 | def read(self): | ||
675 | 23 | with codecs.open(self.fn, 'r', encoding='utf-8') as f: | ||
676 | 24 | if self.fn.endswith('.md'): | ||
677 | 25 | self.html = markdown.markdown( | ||
678 | 26 | f.read(), | ||
679 | 27 | output_format='html5', | ||
680 | 28 | extensions=['pymdownx.github']) | ||
681 | 29 | elif self.fn.endswith('.html'): | ||
682 | 30 | self.html = f.read() | ||
683 | 31 | else: | ||
684 | 32 | logging.error("Don't know how to interpret '{}'.".format( | ||
685 | 33 | self.fn)) | ||
686 | 34 | return False | ||
687 | 35 | self.title = self._read_title() | ||
688 | 36 | self._remove_body_and_html_tags() | ||
689 | 37 | self._use_developer_site_style() | ||
690 | 38 | return True | ||
691 | 39 | |||
692 | 40 | def _read_title(self): | ||
693 | 41 | soup = BeautifulSoup(self.html, 'html5lib') | ||
694 | 42 | if soup.title: | ||
695 | 43 | return soup.title.text | ||
696 | 44 | if soup.h1: | ||
697 | 45 | return soup.h1.text | ||
698 | 46 | return slugify(self.fn).replace('-', ' ').title() | ||
699 | 47 | |||
700 | 48 | def _remove_body_and_html_tags(self): | ||
701 | 49 | self.html = re.sub(r"<html>\n\s<body>\n", "", self.html, | ||
702 | 50 | flags=re.MULTILINE) | ||
703 | 51 | self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html, | ||
704 | 52 | flags=re.MULTILINE) | ||
705 | 53 | |||
706 | 54 | def _use_developer_site_style(self): | ||
707 | 55 | begin = (u"<div class=\"row no-border\">" | ||
708 | 56 | "\n<div class=\"eight-col\">\n") | ||
709 | 57 | end = u"</div>\n</div>" | ||
710 | 58 | self.html = begin + self.html + end | ||
711 | 59 | self.html = self.html.replace( | ||
712 | 60 | "<pre><code>", | ||
713 | 61 | "</div><div class=\"twelve-col\"><pre><code>") | ||
714 | 62 | self.html = self.html.replace( | ||
715 | 63 | "</code></pre>", | ||
716 | 64 | "</code></pre></div><div class=\"eight-col\">") | ||
717 | 65 | |||
718 | 66 | def replace_links(self, titles, url_map): | ||
719 | 67 | soup = BeautifulSoup(self.html, 'html5lib') | ||
720 | 68 | for link in soup.find_all('a'): | ||
721 | 69 | for title in titles: | ||
722 | 70 | if link.attrs['href'] == os.path.basename(title): | ||
723 | 71 | link.attrs['href'] = url_map[title].full_url | ||
724 | 72 | self.html = soup.prettify() | ||
725 | 73 | |||
726 | 74 | def add_to_db(self): | ||
727 | 75 | '''Publishes pages in their branch alias namespace.''' | ||
728 | 76 | self.page = get_or_create_page( | ||
729 | 77 | title=self.title, full_url=self.full_url, menu_title=self.title, | ||
730 | 78 | html=self.html) | ||
731 | 79 | self.full_url = self.page.get_absolute_url() | ||
732 | 80 | |||
733 | 81 | |||
734 | 82 | class SnappyArticle(Article): | ||
735 | 83 | release_alias = None | ||
736 | 84 | |||
737 | 85 | def read(self): | ||
738 | 86 | if not Article.read(self): | ||
739 | 87 | return False | ||
740 | 88 | self.release_alias = re.findall(r'snappy/guides/(\S+?)/\S+?', | ||
741 | 89 | self.full_url)[0] | ||
742 | 90 | self._make_snappy_mods() | ||
743 | 91 | return True | ||
744 | 92 | |||
745 | 93 | def _make_snappy_mods(self): | ||
746 | 94 | # Make sure the reader knows which documentation she is browsing | ||
747 | 95 | if self.release_alias != 'current': | ||
748 | 96 | before = (u"<div class=\"row no-border\">\n" | ||
749 | 97 | "<div class=\"eight-col\">\n") | ||
750 | 98 | after = (u"<div class=\"row no-border\">\n" | ||
751 | 99 | "<div class=\"box pull-three three-col\">" | ||
752 | 100 | "<p>You are browsing the Snappy <code>%s</code> " | ||
753 | 101 | "documentation.</p>" | ||
754 | 102 | "<p><a href=\"/snappy/guides/current/%s\">" | ||
755 | 103 | "Back to the latest stable release ›" | ||
756 | 104 | "</a></p></div>\n" | ||
757 | 105 | "<div class=\"eight-col\">\n") % (self.release_alias, | ||
758 | 106 | self.slug, ) | ||
759 | 107 | self.html = self.html.replace(before, after) | ||
760 | 108 | |||
761 | 109 | def add_to_db(self): | ||
762 | 110 | if self.release_alias == "current": | ||
763 | 111 | # Add a guides/<page> redirect to guides/current/<page> | ||
764 | 112 | get_or_create_page( | ||
765 | 113 | title=self.title, | ||
766 | 114 | full_url=self.full_url.replace('/current', ''), | ||
767 | 115 | redirect="/snappy/guides/current/{}".format(self.slug)) | ||
768 | 116 | else: | ||
769 | 117 | self.title += " (%s)" % (self.release_alias,) | ||
770 | 118 | Article.add_to_db(self) | ||
771 | 0 | 119 | ||
772 | === added file 'md_importer/management/importer/publish.py' | |||
773 | --- md_importer/management/importer/publish.py 1970-01-01 00:00:00 +0000 | |||
774 | +++ md_importer/management/importer/publish.py 2016-01-06 12:40:31 +0000 | |||
775 | @@ -0,0 +1,64 @@ | |||
776 | 1 | from cms.api import create_page, add_plugin | ||
777 | 2 | from cms.models import Title | ||
778 | 3 | |||
779 | 4 | import logging | ||
780 | 5 | import os | ||
781 | 6 | import sys | ||
782 | 7 | |||
783 | 8 | # XXX: use this once we RawHTML plugins don't strip comments (LP: #1523925) | ||
784 | 9 | START_TEXT = """ | ||
785 | 10 | <!-- | ||
786 | 11 | branch id: {} | ||
787 | 12 | |||
788 | 13 | THIS PAGE IS AUTOMATICALLY UPDATED. | ||
789 | 14 | DON'T EDIT IT - CHANGES WILL BE OVERWRITTEN. | ||
790 | 15 | --> | ||
791 | 16 | """ | ||
792 | 17 | |||
793 | 18 | |||
794 | 19 | def slugify(filename): | ||
795 | 20 | return os.path.basename(filename).replace('.md', '').replace('.html', '') | ||
796 | 21 | |||
797 | 22 | |||
798 | 23 | def get_or_create_page(title, full_url, menu_title=None, | ||
799 | 24 | in_navigation=True, redirect=None, html=None): | ||
800 | 25 | # First check if pages already exist. | ||
801 | 26 | if full_url.startswith('/'): | ||
802 | 27 | full_url = full_url[1:] | ||
803 | 28 | pages = Title.objects.select_related('page').filter( | ||
804 | 29 | path__regex=full_url).filter(publisher_is_draft=True) | ||
805 | 30 | if pages: | ||
806 | 31 | page = pages[0].page | ||
807 | 32 | page.title = title | ||
808 | 33 | page.publisher_is_draft = True | ||
809 | 34 | page.menu_title = menu_title | ||
810 | 35 | page.in_navigation = in_navigation | ||
811 | 36 | page.redirect = redirect | ||
812 | 37 | if html: | ||
813 | 38 | # We create the page, so we know there's just one placeholder | ||
814 | 39 | placeholder = page.placeholders.all()[0] | ||
815 | 40 | if placeholder.get_plugins(): | ||
816 | 41 | plugin = placeholder.get_plugins()[0].get_plugin_instance()[0] | ||
817 | 42 | plugin.body = html | ||
818 | 43 | plugin.save() | ||
819 | 44 | else: | ||
820 | 45 | add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) | ||
821 | 46 | else: | ||
822 | 47 | parent_pages = Title.objects.select_related('page').filter( | ||
823 | 48 | path__regex=os.path.dirname(full_url)).filter( | ||
824 | 49 | publisher_is_draft=True) | ||
825 | 50 | if not parent_pages: | ||
826 | 51 | logging.error('Parent {} not found.'.format( | ||
827 | 52 | os.path.dirname(full_url))) | ||
828 | 53 | sys.exit(1) | ||
829 | 54 | parent = parent_pages[0].page | ||
830 | 55 | |||
831 | 56 | slug = os.path.basename(full_url) | ||
832 | 57 | page = create_page( | ||
833 | 58 | title, "default.html", "en", slug=slug, parent=parent, | ||
834 | 59 | menu_title=menu_title, in_navigation=in_navigation, | ||
835 | 60 | position="last-child", redirect=redirect) | ||
836 | 61 | if html: | ||
837 | 62 | placeholder = page.placeholders.get() | ||
838 | 63 | add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html) | ||
839 | 64 | return page | ||
840 | 0 | 65 | ||
841 | === added file 'md_importer/management/importer/repo.py' | |||
842 | --- md_importer/management/importer/repo.py 1970-01-01 00:00:00 +0000 | |||
843 | +++ md_importer/management/importer/repo.py 2016-01-06 12:40:31 +0000 | |||
844 | @@ -0,0 +1,168 @@ | |||
845 | 1 | from .article import Article, SnappyArticle | ||
846 | 2 | from .publish import get_or_create_page, slugify | ||
847 | 3 | from .source import SourceCode | ||
848 | 4 | |||
849 | 5 | from cms.api import publish_pages | ||
850 | 6 | |||
851 | 7 | import glob | ||
852 | 8 | import logging | ||
853 | 9 | import os | ||
854 | 10 | import shutil | ||
855 | 11 | |||
856 | 12 | |||
857 | 13 | def create_repo(tempdir, origin, branch_name, post_checkout_command): | ||
858 | 14 | if os.path.exists(origin): | ||
859 | 15 | if 'snappy' in origin: | ||
860 | 16 | repo_class = SnappyRepo | ||
861 | 17 | else: | ||
862 | 18 | repo_class = Repo | ||
863 | 19 | else: | ||
864 | 20 | if origin.startswith('lp:snappy') or \ | ||
865 | 21 | 'snappy' in origin.split(':')[1].split('.git')[0].split('/'): | ||
866 | 22 | repo_class = SnappyRepo | ||
867 | 23 | else: | ||
868 | 24 | repo_class = Repo | ||
869 | 25 | return repo_class(tempdir, origin, branch_name, post_checkout_command) | ||
870 | 26 | |||
871 | 27 | |||
872 | 28 | class Repo: | ||
873 | 29 | titles = {} | ||
874 | 30 | url_map = {} | ||
875 | 31 | index_doc_url = None | ||
876 | 32 | index_page = None | ||
877 | 33 | release_alias = None | ||
878 | 34 | directives = [] | ||
879 | 35 | imported_articles = [] | ||
880 | 36 | # On top of the pages in imported_articles this also includes index_page | ||
881 | 37 | pages = [] | ||
882 | 38 | |||
883 | 39 | def __init__(self, tempdir, origin, branch_name, post_checkout_command): | ||
884 | 40 | self.origin = origin | ||
885 | 41 | self.branch_name = branch_name | ||
886 | 42 | self.post_checkout_command = post_checkout_command | ||
887 | 43 | branch_nick = os.path.basename(self.origin.replace('.git', '')) | ||
888 | 44 | self.checkout_location = os.path.join( | ||
889 | 45 | tempdir, branch_nick) | ||
890 | 46 | self.index_doc_title = branch_nick | ||
891 | 47 | self.article_class = Article | ||
892 | 48 | |||
893 | 49 | # Only used to speed up tests - allows reusing same object without | ||
894 | 50 | # having to redownload the source again | ||
895 | 51 | def reset(self): | ||
896 | 52 | self.article_class = Article | ||
897 | 53 | self.directives = [] | ||
898 | 54 | self.imported_articles = [] | ||
899 | 55 | |||
900 | 56 | def get(self): | ||
901 | 57 | sourcecode = SourceCode(self.origin, self.checkout_location, | ||
902 | 58 | self.branch_name, self.post_checkout_command) | ||
903 | 59 | if sourcecode.get() != 0: | ||
904 | 60 | logging.error( | ||
905 | 61 | 'Could not check out branch "{}".'.format(self.origin)) | ||
906 | 62 | shutil.rmtree(self.checkout_location) | ||
907 | 63 | return 1 | ||
908 | 64 | return 0 | ||
909 | 65 | |||
910 | 66 | def add_directive(self, import_from, write_to): | ||
911 | 67 | self.directives += [ | ||
912 | 68 | { | ||
913 | 69 | 'import_from': os.path.join(self.checkout_location, | ||
914 | 70 | import_from), | ||
915 | 71 | 'write_to': write_to | ||
916 | 72 | } | ||
917 | 73 | ] | ||
918 | 74 | |||
919 | 75 | def execute_import_directives(self): | ||
920 | 76 | import_list = [] | ||
921 | 77 | # Import single files first | ||
922 | 78 | for directive in [d for d in self.directives | ||
923 | 79 | if os.path.isfile(d['import_from'])]: | ||
924 | 80 | import_list += [ | ||
925 | 81 | (directive['import_from'], directive['write_to']) | ||
926 | 82 | ] | ||
927 | 83 | # Import directories next | ||
928 | 84 | for directive in [d for d in self.directives | ||
929 | 85 | if os.path.isdir(d['import_from'])]: | ||
930 | 86 | for fn in glob.glob('{}/*'.format(directive['import_from'])): | ||
931 | 87 | if fn not in [a[0] for a in import_list]: | ||
932 | 88 | import_list += [ | ||
933 | 89 | (fn, os.path.join(directive['write_to'], slugify(fn))) | ||
934 | 90 | ] | ||
935 | 91 | # If we import into a namespace and don't have an index doc, | ||
936 | 92 | # we need to write one. | ||
937 | 93 | if directive['write_to'] not in [x[1] for x in import_list]: | ||
938 | 94 | self.index_doc_url = directive['write_to'] | ||
939 | 95 | if self.index_doc_url: | ||
940 | 96 | self._create_fake_index_page() | ||
941 | 97 | # The actual import | ||
942 | 98 | for entry in import_list: | ||
943 | 99 | article = self._read_article(entry[0], entry[1]) | ||
944 | 100 | if article: | ||
945 | 101 | self.imported_articles += [article] | ||
946 | 102 | self.titles[article.fn] = article.title | ||
947 | 103 | self.url_map[article.fn] = article | ||
948 | 104 | if self.index_doc_url: | ||
949 | 105 | self._write_fake_index_doc() | ||
950 | 106 | |||
951 | 107 | def _read_article(self, fn, write_to): | ||
952 | 108 | article = self.article_class(fn, write_to) | ||
953 | 109 | if article.read(): | ||
954 | 110 | return article | ||
955 | 111 | return None | ||
956 | 112 | |||
957 | 113 | def publish(self): | ||
958 | 114 | for article in self.imported_articles: | ||
959 | 115 | article.add_to_db() | ||
960 | 116 | article.replace_links(self.titles, self.url_map) | ||
961 | 117 | self.pages = [article.page for article in self.imported_articles] | ||
962 | 118 | if self.index_page: | ||
963 | 119 | self.pages.extend([self.index_page]) | ||
964 | 120 | publish_pages(self.pages) | ||
965 | 121 | |||
966 | 122 | def _create_fake_index_page(self): | ||
967 | 123 | '''Creates a fake index page at the top of the branches | ||
968 | 124 | docs namespace.''' | ||
969 | 125 | |||
970 | 126 | if self.index_doc_url.endswith('current'): | ||
971 | 127 | redirect = '/snappy/guides' | ||
972 | 128 | else: | ||
973 | 129 | redirect = None | ||
974 | 130 | self.index_page = get_or_create_page( | ||
975 | 131 | title=self.index_doc_title, full_url=self.index_doc_url, | ||
976 | 132 | in_navigation=False, redirect=redirect, html='', | ||
977 | 133 | menu_title=None) | ||
978 | 134 | |||
979 | 135 | def _write_fake_index_doc(self): | ||
980 | 136 | list_pages = '' | ||
981 | 137 | for article in [a for a | ||
982 | 138 | in self.imported_articles | ||
983 | 139 | if a.full_url.startswith(self.index_doc_url)]: | ||
984 | 140 | list_pages += '<li><a href=\"{}\">{}</a></li>'.format( | ||
985 | 141 | os.path.basename(article.full_url), article.title) | ||
986 | 142 | self.index_page.html = ( | ||
987 | 143 | u'<div class=\"row\"><div class=\"eight-col\">\n' | ||
988 | 144 | '<p>This section contains documentation for the ' | ||
989 | 145 | '<code>{}</code> Snappy branch.</p>' | ||
990 | 146 | '<p><ul class=\"list-ubuntu\">{}</ul></p>\n' | ||
991 | 147 | '<p>Auto-imported from <a ' | ||
992 | 148 | 'href=\"{}\">{}</a>.</p>\n' | ||
993 | 149 | '</div></div>'.format(self.release_alias, list_pages, | ||
994 | 150 | self.origin, self.origin)) | ||
995 | 151 | |||
996 | 152 | |||
997 | 153 | class SnappyRepo(Repo): | ||
998 | 154 | def __init__(self, tempdir, origin, branch_name, post_checkout_command): | ||
999 | 155 | Repo.__init__(self, tempdir, origin, branch_name, | ||
1000 | 156 | post_checkout_command) | ||
1001 | 157 | self.article_class = SnappyArticle | ||
1002 | 158 | self.index_doc_title = 'Snappy documentation' | ||
1003 | 159 | |||
1004 | 160 | def _create_fake_index_page(self): | ||
1005 | 161 | self.release_alias = os.path.basename(self.index_doc_url) | ||
1006 | 162 | if not self.index_doc_url.endswith('current'): | ||
1007 | 163 | self.index_doc_title += ' ({})'.format(self.release_alias) | ||
1008 | 164 | Repo._create_fake_index_page(self) | ||
1009 | 165 | |||
1010 | 166 | def reset(self): | ||
1011 | 167 | Repo.reset(self) | ||
1012 | 168 | self.article_class = SnappyArticle | ||
1013 | 0 | 169 | ||
1014 | === added file 'md_importer/management/importer/source.py' | |||
1015 | --- md_importer/management/importer/source.py 1970-01-01 00:00:00 +0000 | |||
1016 | +++ md_importer/management/importer/source.py 2016-01-06 12:40:31 +0000 | |||
1017 | @@ -0,0 +1,60 @@ | |||
1018 | 1 | import logging | ||
1019 | 2 | import os | ||
1020 | 3 | import shutil | ||
1021 | 4 | import subprocess | ||
1022 | 5 | |||
1023 | 6 | |||
1024 | 7 | class SourceCode(): | ||
1025 | 8 | def __init__(self, origin, checkout_location, branch_name, | ||
1026 | 9 | post_checkout_command): | ||
1027 | 10 | self.origin = origin | ||
1028 | 11 | self.checkout_location = checkout_location | ||
1029 | 12 | self.branch_name = branch_name | ||
1030 | 13 | self.post_checkout_command = post_checkout_command | ||
1031 | 14 | |||
1032 | 15 | def get(self): | ||
1033 | 16 | res = self._get_branch() | ||
1034 | 17 | if res == 0 and self.post_checkout_command: | ||
1035 | 18 | res = self._post_checkout() | ||
1036 | 19 | return res | ||
1037 | 20 | return res | ||
1038 | 21 | |||
1039 | 22 | def _get_branch(self): | ||
1040 | 23 | if os.path.exists(self.origin): | ||
1041 | 24 | shutil.copytree( | ||
1042 | 25 | self.origin, | ||
1043 | 26 | self.checkout_location) | ||
1044 | 27 | return 0 | ||
1045 | 28 | if self.origin.startswith('lp:') and \ | ||
1046 | 29 | os.path.exists('/usr/bin/bzr'): | ||
1047 | 30 | return subprocess.call([ | ||
1048 | 31 | 'bzr', 'checkout', '--lightweight', self.origin, | ||
1049 | 32 | self.checkout_location]) | ||
1050 | 33 | if self.origin.startswith('https://github.com') and \ | ||
1051 | 34 | self.origin.endswith('.git') and \ | ||
1052 | 35 | os.path.exists('/usr/bin/git'): | ||
1053 | 36 | retcode = subprocess.call([ | ||
1054 | 37 | 'git', 'clone', '--quiet', self.origin, | ||
1055 | 38 | self.checkout_location]) | ||
1056 | 39 | if retcode == 0 and self.branch_name: | ||
1057 | 40 | pwd = os.getcwd() | ||
1058 | 41 | os.chdir(self.checkout_location) | ||
1059 | 42 | retcode = subprocess.call(['git', 'checkout', '--quiet', | ||
1060 | 43 | self.branch_name]) | ||
1061 | 44 | os.chdir(pwd) | ||
1062 | 45 | return retcode | ||
1063 | 46 | logging.error( | ||
1064 | 47 | 'Branch format "{}" not understood.'.format(self.origin)) | ||
1065 | 48 | return 1 | ||
1066 | 49 | |||
1067 | 50 | def _post_checkout(self): | ||
1068 | 51 | pwd = os.getcwd() | ||
1069 | 52 | os.chdir(self.checkout_location) | ||
1070 | 53 | process = subprocess.Popen(self.post_checkout_command.split(), | ||
1071 | 54 | stdout=subprocess.PIPE) | ||
1072 | 55 | (out, err) = process.communicate() | ||
1073 | 56 | retcode = process.wait() | ||
1074 | 57 | os.chdir(pwd) | ||
1075 | 58 | if retcode != 0: | ||
1076 | 59 | logging.error(out) | ||
1077 | 60 | return retcode | ||
1078 | 0 | 61 | ||
1079 | === added directory 'md_importer/migrations' | |||
1080 | === added file 'md_importer/migrations/0001_initial.py' | |||
1081 | --- md_importer/migrations/0001_initial.py 1970-01-01 00:00:00 +0000 | |||
1082 | +++ md_importer/migrations/0001_initial.py 2016-01-06 12:40:31 +0000 | |||
1083 | @@ -0,0 +1,46 @@ | |||
1084 | 1 | # -*- coding: utf-8 -*- | ||
1085 | 2 | from __future__ import unicode_literals | ||
1086 | 3 | |||
1087 | 4 | from django.db import migrations, models | ||
1088 | 5 | |||
1089 | 6 | |||
1090 | 7 | class Migration(migrations.Migration): | ||
1091 | 8 | |||
1092 | 9 | dependencies = [ | ||
1093 | 10 | ('cms', '0013_urlconfrevision'), | ||
1094 | 11 | ] | ||
1095 | 12 | |||
1096 | 13 | operations = [ | ||
1097 | 14 | migrations.CreateModel( | ||
1098 | 15 | name='ExternalDocsBranch', | ||
1099 | 16 | fields=[ | ||
1100 | 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | ||
1101 | 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)), | ||
1102 | 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)), | ||
1103 | 20 | ('post_checkout_command', models.CharField(help_text='Command to run after checkout of the branch.', max_length=100, blank=True)), | ||
1104 | 21 | ('active', models.BooleanField(default=True)), | ||
1105 | 22 | ], | ||
1106 | 23 | options={ | ||
1107 | 24 | 'verbose_name': 'external docs branch', | ||
1108 | 25 | 'verbose_name_plural': 'external docs branches', | ||
1109 | 26 | }, | ||
1110 | 27 | ), | ||
1111 | 28 | migrations.CreateModel( | ||
1112 | 29 | name='ExternalDocsBranchImportDirective', | ||
1113 | 30 | fields=[ | ||
1114 | 31 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | ||
1115 | 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)), | ||
1116 | 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)), | ||
1117 | 34 | ('external_docs_branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')), | ||
1118 | 35 | ], | ||
1119 | 36 | ), | ||
1120 | 37 | migrations.CreateModel( | ||
1121 | 38 | name='ImportedArticle', | ||
1122 | 39 | fields=[ | ||
1123 | 40 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | ||
1124 | 41 | ('last_import', models.DateTimeField(help_text='Datetime of last import.', verbose_name='Datetime')), | ||
1125 | 42 | ('branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')), | ||
1126 | 43 | ('page', models.ForeignKey(to='cms.Page')), | ||
1127 | 44 | ], | ||
1128 | 45 | ), | ||
1129 | 46 | ] | ||
1130 | 0 | 47 | ||
1131 | === added file 'md_importer/migrations/__init__.py' | |||
1132 | === added file 'md_importer/models.py' | |||
1133 | --- md_importer/models.py 1970-01-01 00:00:00 +0000 | |||
1134 | +++ md_importer/models.py 2016-01-06 12:40:31 +0000 | |||
1135 | @@ -0,0 +1,54 @@ | |||
1136 | 1 | from django.db import models | ||
1137 | 2 | from django.utils.translation import ugettext_lazy as _ | ||
1138 | 3 | |||
1139 | 4 | from cms.models import Page | ||
1140 | 5 | |||
1141 | 6 | |||
1142 | 7 | class ExternalDocsBranch(models.Model): | ||
1143 | 8 | origin = models.CharField( | ||
1144 | 9 | max_length=200, | ||
1145 | 10 | help_text=_('External branch location, ie: lp:snappy/15.04 or ' | ||
1146 | 11 | 'https://github.com/ubuntu-core/snappy.git')) | ||
1147 | 12 | branch_name = models.CharField( | ||
1148 | 13 | max_length=200, | ||
1149 | 14 | help_text=_('For use with git branches, ie: "master" or "15.04" ' | ||
1150 | 15 | 'or "1.x".'), | ||
1151 | 16 | blank=True) | ||
1152 | 17 | post_checkout_command = models.CharField( | ||
1153 | 18 | max_length=100, | ||
1154 | 19 | help_text=_('Command to run after checkout of the branch.'), | ||
1155 | 20 | blank=True) | ||
1156 | 21 | active = models.BooleanField(default=True) | ||
1157 | 22 | |||
1158 | 23 | def __str__(self): | ||
1159 | 24 | if self.branch_name: | ||
1160 | 25 | return "{} - {}".format(self.origin, self.branch_name) | ||
1161 | 26 | return "{}".format(self.origin) | ||
1162 | 27 | |||
1163 | 28 | class Meta: | ||
1164 | 29 | verbose_name = "external docs branch" | ||
1165 | 30 | verbose_name_plural = "external docs branches" | ||
1166 | 31 | |||
1167 | 32 | |||
1168 | 33 | class ExternalDocsBranchImportDirective(models.Model): | ||
1169 | 34 | external_docs_branch = models.ForeignKey(ExternalDocsBranch) | ||
1170 | 35 | import_from = models.CharField( | ||
1171 | 36 | max_length=150, | ||
1172 | 37 | help_text=_('File or directory to import from the branch. ' | ||
1173 | 38 | 'Ie: "docs/intro.md" (file) or ' | ||
1174 | 39 | '"docs" (complete directory), etc.')) | ||
1175 | 40 | write_to = models.CharField( | ||
1176 | 41 | max_length=150, | ||
1177 | 42 | help_text=_('Article URL (for a specific file) or article namespace ' | ||
1178 | 43 | 'for a directory or a set of files.')) | ||
1179 | 44 | |||
1180 | 45 | def __str__(self): | ||
1181 | 46 | return "{} -- {}".format(self.external_docs_branch, | ||
1182 | 47 | self.import_from) | ||
1183 | 48 | |||
1184 | 49 | |||
1185 | 50 | class ImportedArticle(models.Model): | ||
1186 | 51 | page = models.ForeignKey(Page) | ||
1187 | 52 | branch = models.ForeignKey(ExternalDocsBranch) | ||
1188 | 53 | last_import = models.DateTimeField( | ||
1189 | 54 | _('Datetime'), help_text=_('Datetime of last import.')) | ||
1190 | 0 | 55 | ||
1191 | === added directory 'md_importer/tests' | |||
1192 | === added file 'md_importer/tests/__init__.py' | |||
1193 | --- md_importer/tests/__init__.py 1970-01-01 00:00:00 +0000 | |||
1194 | +++ md_importer/tests/__init__.py 2016-01-06 12:40:31 +0000 | |||
1195 | @@ -0,0 +1,8 @@ | |||
1196 | 1 | import shutil | ||
1197 | 2 | |||
1198 | 3 | from utils import SnapcraftTestRepo, SnappyTestRepo | ||
1199 | 4 | |||
1200 | 5 | |||
1201 | 6 | def tearDownModule(): | ||
1202 | 7 | shutil.rmtree(SnapcraftTestRepo().tempdir) | ||
1203 | 8 | shutil.rmtree(SnappyTestRepo().tempdir) | ||
1204 | 0 | 9 | ||
1205 | === added directory 'md_importer/tests/data' | |||
1206 | === added directory 'md_importer/tests/data/link-test' | |||
1207 | === added file 'md_importer/tests/data/link-test/file1.md' | |||
1208 | --- md_importer/tests/data/link-test/file1.md 1970-01-01 00:00:00 +0000 | |||
1209 | +++ md_importer/tests/data/link-test/file1.md 2016-01-06 12:40:31 +0000 | |||
1210 | @@ -0,0 +1,5 @@ | |||
1211 | 1 | # Test | ||
1212 | 2 | |||
1213 | 3 | This is a link to [the file2 file](file2.md). | ||
1214 | 4 | |||
1215 | 5 | That's it. | ||
1216 | 0 | 6 | ||
1217 | === added file 'md_importer/tests/data/link-test/file2.md' | |||
1218 | --- md_importer/tests/data/link-test/file2.md 1970-01-01 00:00:00 +0000 | |||
1219 | +++ md_importer/tests/data/link-test/file2.md 2016-01-06 12:40:31 +0000 | |||
1220 | @@ -0,0 +1,3 @@ | |||
1221 | 1 | # File 2 | ||
1222 | 2 | |||
1223 | 3 | Here's just some text. | ||
1224 | 0 | 4 | ||
1225 | === added file 'md_importer/tests/test_branch_fetch.py' | |||
1226 | --- md_importer/tests/test_branch_fetch.py 1970-01-01 00:00:00 +0000 | |||
1227 | +++ md_importer/tests/test_branch_fetch.py 2016-01-06 12:40:31 +0000 | |||
1228 | @@ -0,0 +1,42 @@ | |||
1229 | 1 | import os | ||
1230 | 2 | import shutil | ||
1231 | 3 | import tempfile | ||
1232 | 4 | |||
1233 | 5 | from django.test import TestCase | ||
1234 | 6 | |||
1235 | 7 | from ..management.importer.repo import create_repo, Repo | ||
1236 | 8 | from .utils import SnapcraftTestRepo | ||
1237 | 9 | |||
1238 | 10 | |||
1239 | 11 | class TestBranchFetch(TestCase): | ||
1240 | 12 | def test_git_fetch(self): | ||
1241 | 13 | snapcraft = SnapcraftTestRepo() | ||
1242 | 14 | snapcraft.repo.reset() | ||
1243 | 15 | self.assertEqual(snapcraft.fetch_retcode, 0) | ||
1244 | 16 | self.assertTrue(isinstance(snapcraft.repo, Repo)) | ||
1245 | 17 | |||
1246 | 18 | def test_bzr_fetch(self): | ||
1247 | 19 | tempdir = tempfile.mkdtemp() | ||
1248 | 20 | repo = create_repo( | ||
1249 | 21 | tempdir, | ||
1250 | 22 | 'lp:snapcraft', # outdated, but should work for testing | ||
1251 | 23 | '', | ||
1252 | 24 | '') | ||
1253 | 25 | ret = repo.get() | ||
1254 | 26 | shutil.rmtree(tempdir) | ||
1255 | 27 | self.assertEqual(ret, 0) | ||
1256 | 28 | self.assertTrue(isinstance(repo, Repo)) | ||
1257 | 29 | |||
1258 | 30 | def test_post_checkout_command(self): | ||
1259 | 31 | tempdir = tempfile.mkdtemp() | ||
1260 | 32 | repo = create_repo( | ||
1261 | 33 | tempdir, | ||
1262 | 34 | 'lp:snapcraft', | ||
1263 | 35 | '', | ||
1264 | 36 | 'touch something.html' | ||
1265 | 37 | ) | ||
1266 | 38 | ret = repo.get() | ||
1267 | 39 | self.assertEqual(ret, 0) | ||
1268 | 40 | self.assertTrue(os.path.exists( | ||
1269 | 41 | os.path.join(repo.checkout_location, 'something.html'))) | ||
1270 | 42 | shutil.rmtree(tempdir) | ||
1271 | 0 | 43 | ||
1272 | === added file 'md_importer/tests/test_branch_import.py' | |||
1273 | --- md_importer/tests/test_branch_import.py 1970-01-01 00:00:00 +0000 | |||
1274 | +++ md_importer/tests/test_branch_import.py 2016-01-06 12:40:31 +0000 | |||
1275 | @@ -0,0 +1,67 @@ | |||
1276 | 1 | from django.test import TestCase | ||
1277 | 2 | |||
1278 | 3 | from cms.api import publish_pages | ||
1279 | 4 | from cms.models import Page | ||
1280 | 5 | |||
1281 | 6 | from ..management.importer.article import Article | ||
1282 | 7 | from .utils import ( | ||
1283 | 8 | db_create_home_page, | ||
1284 | 9 | db_empty_page_list, | ||
1285 | 10 | SnapcraftTestRepo, | ||
1286 | 11 | ) | ||
1287 | 12 | |||
1288 | 13 | |||
1289 | 14 | class TestBranchImport(TestCase): | ||
1290 | 15 | def test_1dir_import(self): | ||
1291 | 16 | db_empty_page_list() | ||
1292 | 17 | db_create_home_page() | ||
1293 | 18 | snapcraft = SnapcraftTestRepo() | ||
1294 | 19 | snapcraft.repo.reset() | ||
1295 | 20 | snapcraft.repo.add_directive('docs', '/') | ||
1296 | 21 | snapcraft.repo.execute_import_directives() | ||
1297 | 22 | snapcraft.repo.publish() | ||
1298 | 23 | pages = Page.objects.all() | ||
1299 | 24 | self.assertGreater(len(pages), 3) | ||
1300 | 25 | for article in snapcraft.repo.imported_articles: | ||
1301 | 26 | self.assertTrue(isinstance(article, Article)) | ||
1302 | 27 | |||
1303 | 28 | def test_1dir_and_2files_import(self): | ||
1304 | 29 | db_empty_page_list() | ||
1305 | 30 | db_create_home_page() | ||
1306 | 31 | snapcraft = SnapcraftTestRepo() | ||
1307 | 32 | snapcraft.repo.reset() | ||
1308 | 33 | snapcraft.repo.add_directive('docs', '/') | ||
1309 | 34 | snapcraft.repo.add_directive('README.md', '/') | ||
1310 | 35 | snapcraft.repo.add_directive('HACKING.md', '/hacking') | ||
1311 | 36 | snapcraft.repo.execute_import_directives() | ||
1312 | 37 | snapcraft.repo.publish() | ||
1313 | 38 | pages = Page.objects.all() | ||
1314 | 39 | self.assertGreater(len(pages), 5) | ||
1315 | 40 | self.assertIn(u'/en/', [p.get_absolute_url() for p in pages]) | ||
1316 | 41 | self.assertIn(u'/en/hacking/', [p.get_absolute_url() for p in pages]) | ||
1317 | 42 | |||
1318 | 43 | # Check if all importe article has 'home' as parent | ||
1319 | 44 | def test_articletree_1file_import(self): | ||
1320 | 45 | db_empty_page_list() | ||
1321 | 46 | home = db_create_home_page() | ||
1322 | 47 | publish_pages([[home]]) | ||
1323 | 48 | snapcraft = SnapcraftTestRepo() | ||
1324 | 49 | snapcraft.repo.reset() | ||
1325 | 50 | snapcraft.repo.add_directive('README.md', '/readme') | ||
1326 | 51 | snapcraft.repo.execute_import_directives() | ||
1327 | 52 | snapcraft.repo.publish() | ||
1328 | 53 | self.assertEqual(Page.objects.count(), 1+1) # readme + home | ||
1329 | 54 | self.assertTrue(snapcraft.repo.pages[0].parent == home) | ||
1330 | 55 | |||
1331 | 56 | # Check if all imported articles have 'home' as parent | ||
1332 | 57 | def test_articletree_1dir_import(self): | ||
1333 | 58 | db_empty_page_list() | ||
1334 | 59 | home = db_create_home_page() | ||
1335 | 60 | snapcraft = SnapcraftTestRepo() | ||
1336 | 61 | snapcraft.repo.reset() | ||
1337 | 62 | snapcraft.repo.add_directive('docs', '/') | ||
1338 | 63 | snapcraft.repo.execute_import_directives() | ||
1339 | 64 | snapcraft.repo.publish() | ||
1340 | 65 | for page in Page.objects.filter(publisher_is_draft=False): | ||
1341 | 66 | if page.parent is not None: | ||
1342 | 67 | self.assertEqual(page.parent_id, home.id) | ||
1343 | 0 | 68 | ||
1344 | === added file 'md_importer/tests/test_link_rewrite.py' | |||
1345 | --- md_importer/tests/test_link_rewrite.py 1970-01-01 00:00:00 +0000 | |||
1346 | +++ md_importer/tests/test_link_rewrite.py 2016-01-06 12:40:31 +0000 | |||
1347 | @@ -0,0 +1,44 @@ | |||
1348 | 1 | from bs4 import BeautifulSoup | ||
1349 | 2 | import os | ||
1350 | 3 | import shutil | ||
1351 | 4 | import tempfile | ||
1352 | 5 | |||
1353 | 6 | from django.http.response import HttpResponseNotFound | ||
1354 | 7 | from django.test import TestCase, Client | ||
1355 | 8 | |||
1356 | 9 | from cms.models import Page | ||
1357 | 10 | |||
1358 | 11 | from ..management.importer.article import Article | ||
1359 | 12 | from ..management.importer.repo import create_repo | ||
1360 | 13 | from .utils import ( | ||
1361 | 14 | db_create_home_page, | ||
1362 | 15 | db_empty_page_list, | ||
1363 | 16 | ) | ||
1364 | 17 | |||
1365 | 18 | |||
1366 | 19 | class TestLinkRewrite(TestCase): | ||
1367 | 20 | def test_simple_case(self): | ||
1368 | 21 | db_empty_page_list() | ||
1369 | 22 | db_create_home_page() | ||
1370 | 23 | tempdir = tempfile.mkdtemp() | ||
1371 | 24 | repo = create_repo( | ||
1372 | 25 | tempdir, | ||
1373 | 26 | os.path.join(os.path.dirname(__file__), 'data/link-test'), | ||
1374 | 27 | '', | ||
1375 | 28 | '') | ||
1376 | 29 | self.assertEqual(repo.get(), 0) | ||
1377 | 30 | repo.add_directive('', '/') | ||
1378 | 31 | repo.execute_import_directives() | ||
1379 | 32 | repo.publish() | ||
1380 | 33 | pages = Page.objects.all() | ||
1381 | 34 | self.assertEqual(pages.count(), 1+2) # Home + 2 articles | ||
1382 | 35 | c = Client() | ||
1383 | 36 | for article in repo.imported_articles: | ||
1384 | 37 | self.assertTrue(isinstance(article, Article)) | ||
1385 | 38 | soup = BeautifulSoup(article.html, 'html5lib') | ||
1386 | 39 | for link in soup.find_all('a'): | ||
1387 | 40 | if not link.has_attr('class') or \ | ||
1388 | 41 | 'headeranchor-link' not in link.attrs['class']: | ||
1389 | 42 | res = c.get(link.attrs['href']) | ||
1390 | 43 | self.assertNotIsInstance(res, HttpResponseNotFound) | ||
1391 | 44 | shutil.rmtree(tempdir) | ||
1392 | 0 | 45 | ||
1393 | === added file 'md_importer/tests/test_snappy_import.py' | |||
1394 | --- md_importer/tests/test_snappy_import.py 1970-01-01 00:00:00 +0000 | |||
1395 | +++ md_importer/tests/test_snappy_import.py 2016-01-06 12:40:31 +0000 | |||
1396 | @@ -0,0 +1,70 @@ | |||
1397 | 1 | from django.test import TestCase | ||
1398 | 2 | |||
1399 | 3 | from cms.api import publish_pages | ||
1400 | 4 | from cms.models import Page | ||
1401 | 5 | |||
1402 | 6 | from ..management.importer.repo import SnappyRepo | ||
1403 | 7 | from ..management.importer.article import SnappyArticle | ||
1404 | 8 | from .utils import ( | ||
1405 | 9 | db_add_empty_page, | ||
1406 | 10 | db_create_home_page, | ||
1407 | 11 | db_empty_page_list, | ||
1408 | 12 | SnappyTestRepo, | ||
1409 | 13 | ) | ||
1410 | 14 | |||
1411 | 15 | |||
1412 | 16 | class TestSnappyImport(TestCase): | ||
1413 | 17 | def test_snappy_devel_import(self): | ||
1414 | 18 | db_empty_page_list() | ||
1415 | 19 | home = db_create_home_page() | ||
1416 | 20 | snappy_page = db_add_empty_page('Snappy', home) | ||
1417 | 21 | guides = db_add_empty_page('Guides', snappy_page) | ||
1418 | 22 | publish_pages([home, snappy_page, guides]) | ||
1419 | 23 | snappy = SnappyTestRepo() | ||
1420 | 24 | snappy.repo.reset() | ||
1421 | 25 | self.assertEqual(snappy.fetch_retcode, 0) | ||
1422 | 26 | self.assertTrue(isinstance(snappy.repo, SnappyRepo)) | ||
1423 | 27 | snappy.repo.add_directive('docs', '/snappy/guides/devel') | ||
1424 | 28 | snappy.repo.execute_import_directives() | ||
1425 | 29 | snappy.repo.publish() | ||
1426 | 30 | for article in snappy.repo.imported_articles: | ||
1427 | 31 | self.assertTrue(isinstance(article, SnappyArticle)) | ||
1428 | 32 | self.assertGreater(len(snappy.repo.pages), 0) | ||
1429 | 33 | devel = Page.objects.filter(parent=guides.get_public_object()) | ||
1430 | 34 | self.assertEqual(devel.count(), 1) | ||
1431 | 35 | for page in Page.objects.filter(publisher_is_draft=False): | ||
1432 | 36 | if page not in [home, snappy_page, guides, devel[0]]: | ||
1433 | 37 | self.assertEqual(page.parent, devel[0]) | ||
1434 | 38 | |||
1435 | 39 | def test_snappy_current_import(self): | ||
1436 | 40 | db_empty_page_list() | ||
1437 | 41 | home = db_create_home_page() | ||
1438 | 42 | snappy_page = db_add_empty_page('Snappy', home) | ||
1439 | 43 | guides = db_add_empty_page('Guides', snappy_page) | ||
1440 | 44 | publish_pages([home, snappy_page, guides]) | ||
1441 | 45 | snappy = SnappyTestRepo() | ||
1442 | 46 | snappy.repo.reset() | ||
1443 | 47 | self.assertTrue(isinstance(snappy.repo, SnappyRepo)) | ||
1444 | 48 | snappy.repo.add_directive('docs', '/snappy/guides/current') | ||
1445 | 49 | snappy.repo.execute_import_directives() | ||
1446 | 50 | snappy.repo.publish() | ||
1447 | 51 | number_of_articles = len(snappy.repo.imported_articles) | ||
1448 | 52 | for article in snappy.repo.imported_articles: | ||
1449 | 53 | self.assertTrue(isinstance(article, SnappyArticle)) | ||
1450 | 54 | self.assertGreater(number_of_articles, 0) | ||
1451 | 55 | pages = Page.objects.all() | ||
1452 | 56 | current_search = [ | ||
1453 | 57 | a for a in pages | ||
1454 | 58 | if a.get_slug('current') and | ||
1455 | 59 | a.get_absolute_url().endswith('snappy/guides/current/')] | ||
1456 | 60 | self.assertEqual(len(current_search), 1) | ||
1457 | 61 | current = current_search[0] | ||
1458 | 62 | nav_pages = [home, snappy_page, guides, current] | ||
1459 | 63 | # 1 imported article, 1 redirect | ||
1460 | 64 | self.assertEqual( | ||
1461 | 65 | number_of_articles*2, pages.count()-len(nav_pages)) | ||
1462 | 66 | for page in [a for a in pages if a not in nav_pages]: | ||
1463 | 67 | if page.get_redirect('en'): | ||
1464 | 68 | self.assertEqual(page.parent, guides) | ||
1465 | 69 | else: | ||
1466 | 70 | self.assertEqual(page.parent, current) | ||
1467 | 0 | 71 | ||
1468 | === added file 'md_importer/tests/test_utils.py' | |||
1469 | --- md_importer/tests/test_utils.py 1970-01-01 00:00:00 +0000 | |||
1470 | +++ md_importer/tests/test_utils.py 2016-01-06 12:40:31 +0000 | |||
1471 | @@ -0,0 +1,33 @@ | |||
1472 | 1 | from django.test import TestCase | ||
1473 | 2 | |||
1474 | 3 | from cms.api import publish_pages | ||
1475 | 4 | from cms.models import Page | ||
1476 | 5 | |||
1477 | 6 | from .utils import ( | ||
1478 | 7 | db_add_empty_page, | ||
1479 | 8 | db_create_home_page, | ||
1480 | 9 | db_empty_page_list, | ||
1481 | 10 | ) | ||
1482 | 11 | |||
1483 | 12 | |||
1484 | 13 | class PageDBActivities(TestCase): | ||
1485 | 14 | def test_empty_page_list(self): | ||
1486 | 15 | db_empty_page_list() | ||
1487 | 16 | self.assertEqual(Page.objects.count(), 0) | ||
1488 | 17 | |||
1489 | 18 | def test_create_home_page(self): | ||
1490 | 19 | db_empty_page_list() | ||
1491 | 20 | home = db_create_home_page() | ||
1492 | 21 | publish_pages([home]) | ||
1493 | 22 | self.assertNotEqual(home, None) | ||
1494 | 23 | self.assertEqual(Page.objects.count(), 1) | ||
1495 | 24 | |||
1496 | 25 | def test_simple_articletree(self): | ||
1497 | 26 | db_empty_page_list() | ||
1498 | 27 | home = db_create_home_page() | ||
1499 | 28 | snappy = db_add_empty_page('Snappy', home) | ||
1500 | 29 | guides = db_add_empty_page('Guides', snappy) | ||
1501 | 30 | publish_pages([home, snappy, guides]) | ||
1502 | 31 | self.assertEqual(Page.objects.count(), 3) | ||
1503 | 32 | self.assertEqual(guides.parent, snappy) | ||
1504 | 33 | self.assertEqual(snappy.parent, home) | ||
1505 | 0 | 34 | ||
1506 | === added file 'md_importer/tests/utils.py' | |||
1507 | --- md_importer/tests/utils.py 1970-01-01 00:00:00 +0000 | |||
1508 | +++ md_importer/tests/utils.py 2016-01-06 12:40:31 +0000 | |||
1509 | @@ -0,0 +1,67 @@ | |||
1510 | 1 | import shutil | ||
1511 | 2 | import tempfile | ||
1512 | 3 | |||
1513 | 4 | from django.utils.text import slugify | ||
1514 | 5 | |||
1515 | 6 | from cms.api import create_page | ||
1516 | 7 | from cms.models import Page | ||
1517 | 8 | |||
1518 | 9 | from ..management.importer.repo import create_repo | ||
1519 | 10 | |||
1520 | 11 | |||
1521 | 12 | class Singleton(type): | ||
1522 | 13 | _instances = {} | ||
1523 | 14 | |||
1524 | 15 | def __call__(cls, *args, **kwargs): | ||
1525 | 16 | if cls not in cls._instances: | ||
1526 | 17 | cls._instances[cls] = super(Singleton, cls).__call__(*args, | ||
1527 | 18 | **kwargs) | ||
1528 | 19 | return cls._instances[cls] | ||
1529 | 20 | |||
1530 | 21 | |||
1531 | 22 | # We are going to re-use this one, so we don't have to checkout the git | ||
1532 | 23 | # repo all the time. | ||
1533 | 24 | class SnapcraftTestRepo(): | ||
1534 | 25 | __metaclass__ = Singleton | ||
1535 | 26 | |||
1536 | 27 | def __init__(self): | ||
1537 | 28 | self.tempdir = tempfile.mkdtemp() | ||
1538 | 29 | self.repo = create_repo( | ||
1539 | 30 | self.tempdir, | ||
1540 | 31 | 'https://github.com/ubuntu-core/snapcraft.git', | ||
1541 | 32 | 'master', | ||
1542 | 33 | '') | ||
1543 | 34 | self.fetch_retcode = self.repo.get() | ||
1544 | 35 | |||
1545 | 36 | |||
1546 | 37 | class SnappyTestRepo(): | ||
1547 | 38 | __metaclass__ = Singleton | ||
1548 | 39 | |||
1549 | 40 | def __init__(self): | ||
1550 | 41 | self.tempdir = tempfile.mkdtemp() | ||
1551 | 42 | self.repo = create_repo( | ||
1552 | 43 | self.tempdir, | ||
1553 | 44 | 'https://github.com/ubuntu-core/snappy.git', | ||
1554 | 45 | 'master', | ||
1555 | 46 | '') | ||
1556 | 47 | self.fetch_retcode = self.repo.get() | ||
1557 | 48 | |||
1558 | 49 | |||
1559 | 50 | def tearDownModule(): | ||
1560 | 51 | shutil.rmtree(SnapcraftTestRepo().tempdir) | ||
1561 | 52 | shutil.rmtree(SnappyTestRepo().tempdir) | ||
1562 | 53 | |||
1563 | 54 | |||
1564 | 55 | def db_empty_page_list(): | ||
1565 | 56 | Page.objects.all().delete() | ||
1566 | 57 | |||
1567 | 58 | |||
1568 | 59 | def db_create_home_page(): | ||
1569 | 60 | home = create_page('Test import', 'default.html', 'en', slug='home') | ||
1570 | 61 | return home | ||
1571 | 62 | |||
1572 | 63 | |||
1573 | 64 | def db_add_empty_page(title, parent): | ||
1574 | 65 | page = create_page(title, 'default.html', 'en', slug=slugify(title), | ||
1575 | 66 | parent=parent) | ||
1576 | 67 | return page | ||
1577 | 0 | 68 | ||
1578 | === modified file 'requirements.txt' | |||
1579 | --- requirements.txt 2015-12-11 16:10:30 +0000 | |||
1580 | +++ requirements.txt 2016-01-06 12:40:31 +0000 | |||
1581 | @@ -1,4 +1,4 @@ | |||
1583 | 1 | Django==1.8.7 | 1 | Django==1.8.8 |
1584 | 2 | django-template-debug==0.3.5 | 2 | django-template-debug==0.3.5 |
1585 | 3 | oslo.config==3.1.0 | 3 | oslo.config==3.1.0 |
1586 | 4 | oslo.i18n==3.1.0 | 4 | oslo.i18n==3.1.0 |
1587 | @@ -7,6 +7,7 @@ | |||
1588 | 7 | Pillow==2.9.0 | 7 | Pillow==2.9.0 |
1589 | 8 | cmsplugin-zinnia==0.8 | 8 | cmsplugin-zinnia==0.8 |
1590 | 9 | Markdown==2.6.5 | 9 | Markdown==2.6.5 |
1591 | 10 | pymdown-extensions==1.0.1 | ||
1592 | 10 | beautifulsoup4==4.4.1 | 11 | beautifulsoup4==4.4.1 |
1593 | 11 | dj-database-url==0.3.0 | 12 | dj-database-url==0.3.0 |
1594 | 12 | django-admin-enhancer==1.0.0 | 13 | django-admin-enhancer==1.0.0 |
1595 | @@ -20,8 +21,8 @@ | |||
1596 | 20 | django-meta==0.3.1 | 21 | django-meta==0.3.1 |
1597 | 21 | django-meta-mixin==0.2.1 | 22 | django-meta-mixin==0.2.1 |
1598 | 22 | django-missing==0.1.15 | 23 | django-missing==0.1.15 |
1601 | 23 | django-parler==1.5.1 | 24 | django-parler==1.6 |
1602 | 24 | django-polymorphic==0.7.2 | 25 | django-polymorphic==0.8.1 |
1603 | 25 | django-reversion==1.9.3 | 26 | django-reversion==1.9.3 |
1604 | 26 | django-sekizai==0.9.0 | 27 | django-sekizai==0.9.0 |
1605 | 27 | django-swiftstorage==1.1.0 | 28 | django-swiftstorage==1.1.0 |
1606 | @@ -31,11 +32,11 @@ | |||
1607 | 31 | django-taggit-templatetags==0.2.5 | 32 | django-taggit-templatetags==0.2.5 |
1608 | 32 | django-templatetag-sugar==1.0 | 33 | django-templatetag-sugar==1.0 |
1609 | 33 | django-xmlrpc==0.1.5 | 34 | django-xmlrpc==0.1.5 |
1611 | 34 | djangocms-admin-style==1.0.7 | 35 | djangocms-admin-style==1.0.8 |
1612 | 35 | djangocms-link==1.7.1 | 36 | djangocms-link==1.7.1 |
1613 | 36 | djangocms-picture==0.2.0 | 37 | djangocms-picture==0.2.0 |
1614 | 37 | djangocms-snippet==1.7.1 | 38 | djangocms-snippet==1.7.1 |
1616 | 38 | djangocms-text-ckeditor==2.8.0 | 39 | djangocms-text-ckeditor==2.8.1 |
1617 | 39 | djangocms-utils==0.9.5 | 40 | djangocms-utils==0.9.5 |
1618 | 40 | djangocms-video==0.2.0 | 41 | djangocms-video==0.2.0 |
1619 | 41 | python-keystoneclient==1.3.3 | 42 | python-keystoneclient==1.3.3 |
1620 | @@ -49,4 +50,4 @@ | |||
1621 | 49 | django-pygments==0.1 | 50 | django-pygments==0.1 |
1622 | 50 | django-openid-auth==0.7 | 51 | django-openid-auth==0.7 |
1623 | 51 | python-openid==2.2.5 | 52 | python-openid==2.2.5 |
1625 | 52 | djangorestframework==3.3.1 | 53 | djangorestframework==3.3.2 |
sqlite> select * from developer_ portal_ externaldocsbra nch; /github. com/ubuntu- core/snapcraft. git|1|master| /github. com/ubuntu- core/snapcraft. git|1|1.x| /github. com/ubuntu- core/snappy. git|1|master| /github. com/ubuntu- core/snappy. git|1|15.04| portal_ externaldocsbra nchimportdirect ive; md|snappy/ build-apps/ devel|1 snappy/ build-apps/ devel|1 md|snappy/ build-apps/ |2 snappy/ build-apps/ |2 snappy/ guides/ devel|3 snappy/ guides/ current| 4 md|snappy/ build-apps/ hacking| 1
1|https:/
2|https:/
3|https:/
4|https:/
sqlite> select * from developer_
1|README.
2|docs|
3|README.
4|docs|
5|docs|
6|docs|
7|HACKING.
sqlite>