Merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/importer-versioning into lp:developer-ubuntu-com

Proposed by Daniel Holbach on 2016-05-12
Status: Needs review
Proposed branch: lp:~developer-ubuntu-com-dev/developer-ubuntu-com/importer-versioning
Merge into: lp:developer-ubuntu-com
Diff against target: 552 lines (+300/-30)
10 files modified
md_importer/importer/article.py (+4/-7)
md_importer/importer/process.py (+49/-5)
md_importer/importer/publish.py (+19/-5)
md_importer/importer/repo.py (+19/-10)
md_importer/migrations/0005_add_versioning.py (+41/-0)
md_importer/models.py (+37/-0)
md_importer/tests/test_process.py (+95/-0)
md_importer/tests/test_publish.py (+20/-1)
md_importer/tests/utils.py (+2/-2)
templates/cms_page_base.html (+14/-0)
To merge this branch: bzr merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/importer-versioning
Reviewer Review Type Date Requested Status
Ubuntu App Developer site developers 2016-05-12 Pending
Review via email: mp+294517@code.launchpad.net
To post a comment you must log in.
Daniel Holbach (dholbach) wrote :

What's still needed: make the version picker look nice.

237. By Daniel Holbach on 2016-05-14

fix dissociation of version information

238. By Daniel Holbach on 2016-05-14

fix urls to not include 'None'

Unmerged revisions

238. By Daniel Holbach on 2016-05-14

fix urls to not include 'None'

237. By Daniel Holbach on 2016-05-14

fix dissociation of version information

236. By Daniel Holbach on 2016-05-12

merge trunk

235. By Daniel Holbach on 2016-05-12

exclude current version

234. By Daniel Holbach on 2016-05-12

make sure we add the alternative url info

233. By Daniel Holbach on 2016-05-12

fix template

232. By Daniel Holbach on 2016-05-12

first cut at adding version/url info to page template

231. By Daniel Holbach on 2016-05-12

fix logic

230. By Daniel Holbach on 2016-05-11

clarify tests, add a new test to check the extension data of the page - currently fails

229. By Daniel Holbach on 2016-05-11

simplify code and make version extension handling actually work

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'md_importer/importer/article.py'
2--- md_importer/importer/article.py 2016-04-21 12:26:42 +0000
3+++ md_importer/importer/article.py 2016-05-14 20:13:14 +0000
4@@ -6,7 +6,6 @@
5 import sys
6
7 from . import (
8- DEFAULT_LANG,
9 DEFAULT_TEMPLATE,
10 logger,
11 MARKDOWN_EXTENSIONS,
12@@ -14,6 +13,7 @@
13 )
14 from .publish import (
15 ArticlePage,
16+ clean_url,
17 ParentNotFoundException,
18 slugify,
19 )
20@@ -25,14 +25,13 @@
21
22
23 class Article:
24- def __init__(self, fn, write_to, advertise=True,
25+ def __init__(self, fn, full_url, advertise=True,
26 template=DEFAULT_TEMPLATE):
27 self.html = None
28 self.article_page = None
29 self.title = ""
30 self.fn = fn
31- self.write_to = slugify(self.fn)
32- self.full_url = write_to
33+ self.full_url = full_url
34 self.slug = os.path.basename(self.full_url)
35 self.links_rewritten = False
36 self.local_images = []
37@@ -129,9 +128,7 @@
38 in_navigation=self.advertise, template=self.template)
39 except ParentNotFoundException:
40 return False
41- self.full_url = re.sub(
42- r'^\/None\/', '/{}/'.format(DEFAULT_LANG),
43- self.article_page.draft.get_absolute_url())
44+ self.full_url = clean_url(self.article_page.draft.get_absolute_url())
45 return True
46
47 def publish(self):
48
49=== modified file 'md_importer/importer/process.py'
50--- md_importer/importer/process.py 2016-04-27 15:14:30 +0000
51+++ md_importer/importer/process.py 2016-05-14 20:13:14 +0000
52@@ -3,21 +3,27 @@
53 import shutil
54 import tempfile
55
56-from django.core.management import call_command
57-
58-from md_importer.importer import logger
59-from md_importer.importer.publish import find_page
60+from md_importer.importer import (
61+ DEFAULT_LANG,
62+ logger,
63+)
64+from md_importer.importer.publish import (
65+ find_page,
66+ remove_none_from_url,
67+)
68 from md_importer.importer.repo import Repo
69 from md_importer.models import (
70+ ExternalDocsBranch,
71 ExternalDocsBranchImportDirective,
72 ImportedArticle,
73+ VersionExtension,
74 )
75
76
77 def process_branch(branch):
78 tempdir = tempfile.mkdtemp()
79 repo = Repo(tempdir, branch.origin, branch.branch_name,
80- branch.post_checkout_command)
81+ branch.post_checkout_command, branch.version)
82 if repo.get() != 0:
83 return None
84 for directive in ExternalDocsBranchImportDirective.objects.filter(
85@@ -46,6 +52,43 @@
86 url=url,
87 last_import=timestamp)
88
89+ if repo.version:
90+ # Updates pages with information about other versions of the article
91+ ebs = ExternalDocsBranch.objects.filter(origin=repo.origin)
92+ available_versions = [a.version for a in ebs]
93+ urls = [remove_none_from_url(i.url)
94+ for i in ImportedArticle.objects.all()]
95+
96+ for page in repo.pages:
97+ url = page.get_absolute_url()
98+ alternative_urls = [
99+ {
100+ 'version': v,
101+ 'url': remove_none_from_url(url.replace(repo.version, v)),
102+ }
103+ for v in available_versions if v != repo.version
104+ ]
105+ # Check which of the alternative versions of the article
106+ # actually exist
107+ for useless_entry in [x for x in alternative_urls
108+ if x['url'] not in urls]:
109+ alternative_urls.remove(useless_entry)
110+ draft = page.get_draft_object()
111+ if not hasattr(draft, 'versionextension'):
112+ version_ext = VersionExtension(extended_object=draft)
113+ version_ext.save()
114+ draft.versionextension = version_ext
115+ if draft.versionextension.version != repo.version:
116+ draft.versionextension.version = repo.version
117+ draft.versionextension.update_alternatives(alternative_urls)
118+ draft.versionextension.save()
119+ draft.publish(DEFAULT_LANG)
120+ else:
121+ for page in repo.pages:
122+ if hasattr(page, 'versionextension'):
123+ page.versionextension.delete()
124+ page.publish(DEFAULT_LANG)
125+
126 # Remove old entries
127 for imported_article in ImportedArticle.objects.filter(
128 branch=branch, last_import__lt=timestamp):
129@@ -61,6 +104,7 @@
130 if p.changed_by in ['python-api', 'script']]
131 ImportedArticle.objects.filter(
132 branch=branch).filter(id__in=imported_page_ids).delete()
133+
134 shutil.rmtree(tempdir)
135
136 return repo
137
138=== modified file 'md_importer/importer/publish.py'
139--- md_importer/importer/publish.py 2016-04-21 12:26:42 +0000
140+++ md_importer/importer/publish.py 2016-05-14 20:13:14 +0000
141@@ -134,12 +134,26 @@
142 return os.path.basename(filename).replace('.md', '').replace('.html', '')
143
144
145+def remove_none_from_url(url):
146+ return re.sub(
147+ r'^\/None|{}\/'.format(DEFAULT_LANG),
148+ '',
149+ url)
150+
151+
152 def clean_url(url):
153- return remove_leading_and_trailing_slash(
154- re.sub(
155- r'^\/None|{}\/'.format(DEFAULT_LANG),
156- '',
157- url))
158+ return remove_leading_and_trailing_slash(remove_none_from_url(url))
159+
160+
161+def generate_url(import_to, article_fn, version=None):
162+ if not version:
163+ if not article_fn:
164+ # Unversioned index page
165+ return clean_url(import_to)
166+ # Unversioned 'regular' article
167+ return clean_url(os.path.join(import_to, slugify(article_fn)))
168+ # Versioned article
169+ return clean_url(os.path.join(import_to, version, slugify(article_fn)))
170
171
172 def find_page(url, draft=False):
173
174=== modified file 'md_importer/importer/repo.py'
175--- md_importer/importer/repo.py 2016-04-21 12:26:42 +0000
176+++ md_importer/importer/repo.py 2016-05-14 20:13:14 +0000
177@@ -5,9 +5,9 @@
178 )
179 from .article import Article
180 from .publish import (
181+ generate_url,
182 IndexPage,
183 ParentNotFoundException,
184- slugify,
185 )
186 from .source import SourceCode
187
188@@ -18,7 +18,8 @@
189
190
191 class Repo:
192- def __init__(self, tempdir, origin, branch_name, post_checkout_command):
193+ def __init__(self, tempdir, origin, branch_name,
194+ post_checkout_command=None, version=None):
195 self.directives = []
196 self.imported_articles = []
197 self.url_map = {}
198@@ -31,9 +32,13 @@
199 self.origin = origin
200 self.branch_name = branch_name
201 self.post_checkout_command = post_checkout_command
202+ self.version = version
203 self.branch_nick = os.path.basename(self.origin.replace('.git', ''))
204 self.checkout_location = os.path.join(tempdir, self.branch_nick)
205
206+ def __str__(self):
207+ return '{} -- {}'.format(self.origin, self.branch_name)
208+
209 def get(self):
210 sourcecode = SourceCode(self.origin, self.checkout_location,
211 self.branch_name, self.post_checkout_command)
212@@ -79,29 +84,33 @@
213 for fn in glob.glob('{}/*'.format(directive['import_from'])):
214 if fn not in [a[0] for a in import_list]:
215 import_list += [
216- (fn, os.path.join(directive['write_to'], slugify(fn)),
217+ (fn,
218+ generate_url(directive['write_to'], fn, self.version),
219 directive['advertise'], directive['template'])
220 ]
221 # If we import into a namespace and don't have an index doc,
222 # we need to write one.
223 # XXX: Right now we just create one index doc, this will change in
224 # the near future.
225- if directive['write_to'] not in [x[1] for x in import_list] and \
226+ index_url = generate_url(directive['write_to'], '', self.version)
227+ if index_url not in [x[1] for x in import_list] and \
228 not self.index_page:
229 try:
230 self.index_page = IndexPage(
231- title=self.branch_nick, full_url=directive['write_to'],
232+ title=self.branch_nick, full_url=index_url,
233 in_navigation=directive['advertise'], html='',
234 menu_title=None, template=DEFAULT_TEMPLATE)
235- self.pages.extend([self.index_page.page])
236 except ParentNotFoundException:
237 return self._abort_import(
238 'Could not create fake index page at {}'.format(
239- directive['write_to']))
240+ index_url))
241+ self.pages.extend([self.index_page.page])
242+
243 # The actual import
244 for entry in import_list:
245 article = self._read_article(
246- entry[0], entry[1], entry[2], entry[3])
247+ fn=entry[0], full_url=entry[1], advertise=entry[2],
248+ template=entry[3])
249 if article:
250 self.imported_articles += [article]
251 self.titles[article.fn] = article.title
252@@ -121,8 +130,8 @@
253 logger.error('Importing of {} aborted: {}.'.format(self.origin, msg))
254 return False
255
256- def _read_article(self, fn, write_to, advertise, template):
257- article = Article(fn, write_to, advertise, template)
258+ def _read_article(self, fn, full_url, advertise, template):
259+ article = Article(fn, full_url, advertise, template)
260 if article.read():
261 return article
262 return None
263
264=== added file 'md_importer/migrations/0005_add_versioning.py'
265--- md_importer/migrations/0005_add_versioning.py 1970-01-01 00:00:00 +0000
266+++ md_importer/migrations/0005_add_versioning.py 2016-05-14 20:13:14 +0000
267@@ -0,0 +1,41 @@
268+# -*- coding: utf-8 -*-
269+from __future__ import unicode_literals
270+
271+from django.db import migrations, models
272+
273+
274+class Migration(migrations.Migration):
275+
276+ dependencies = [
277+ ('cms', '0014_auto_20160404_1908'),
278+ ('md_importer', '0004_move_from_page_object_to_url_string'),
279+ ]
280+
281+ operations = [
282+ migrations.CreateModel(
283+ name='VersionedURL',
284+ fields=[
285+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
286+ ('version', models.CharField(max_length=10)),
287+ ('url', models.URLField(max_length=1000)),
288+ ],
289+ ),
290+ migrations.CreateModel(
291+ name='VersionExtension',
292+ fields=[
293+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
294+ ('version', models.CharField(max_length=10, blank=True)),
295+ ('alternative_urls', models.ManyToManyField(to='md_importer.VersionedURL', blank=True)),
296+ ('extended_object', models.OneToOneField(editable=False, to='cms.Page')),
297+ ('public_extension', models.OneToOneField(related_name='draft_extension', null=True, editable=False, to='md_importer.VersionExtension')),
298+ ],
299+ options={
300+ 'abstract': False,
301+ },
302+ ),
303+ migrations.AddField(
304+ model_name='externaldocsbranch',
305+ name='version',
306+ field=models.CharField(help_text='If this is a versioned import, set the version, e.g. "15.04" here.', max_length=10, blank=True),
307+ ),
308+ ]
309
310=== modified file 'md_importer/models.py'
311--- md_importer/models.py 2016-04-18 08:56:51 +0000
312+++ md_importer/models.py 2016-05-14 20:13:14 +0000
313@@ -2,6 +2,9 @@
314 from django.db import models
315 from django.utils.translation import ugettext_lazy as _
316
317+from cms.extensions import PageExtension
318+from cms.extensions.extension_pool import extension_pool
319+
320
321 if settings.CMS_TEMPLATES:
322 cms_templates = settings.CMS_TEMPLATES
323@@ -26,6 +29,11 @@
324 help_text=_('Command to run after checkout of the branch.'),
325 blank=True)
326 active = models.BooleanField(default=True)
327+ version = models.CharField(
328+ max_length=10,
329+ help_text=_('If this is a versioned import, set the version, e.g. '
330+ '"15.04" here.'),
331+ blank=True)
332
333 def __str__(self):
334 if self.branch_name:
335@@ -80,3 +88,32 @@
336 def __str__(self):
337 return '{} -- {} -- {}'.format(
338 self.url, self.branch, self.last_import)
339+
340+
341+class VersionedURL(models.Model):
342+ version = models.CharField(max_length=10)
343+ url = models.URLField(max_length=1000)
344+
345+
346+class VersionExtension(PageExtension):
347+ version = models.CharField(
348+ max_length=10,
349+ blank=True)
350+ alternative_urls = models.ManyToManyField(VersionedURL, blank=True)
351+
352+ def update_alternatives(self, alternative_urls):
353+ for alternative in alternative_urls:
354+ versioned_url, created = VersionedURL.objects.get_or_create(
355+ version=alternative['version'],
356+ url=alternative['url'])
357+ if versioned_url not in self.alternative_urls.all():
358+ self.alternative_urls.add(versioned_url)
359+ for listed_url in self.alternative_urls.all():
360+ if not [a for a in alternative_urls
361+ if a['url'] == listed_url.url]:
362+ self.alternative_urls.remove(listed_url)
363+
364+ def copy_relations(self, oldinstance, language):
365+ self.alternative_urls = oldinstance.alternative_urls.all()
366+
367+extension_pool.register(VersionExtension)
368
369=== renamed file 'md_importer/tests/test_import_process.py' => 'md_importer/tests/test_process.py'
370--- md_importer/tests/test_import_process.py 2016-04-18 10:30:14 +0000
371+++ md_importer/tests/test_process.py 2016-05-14 20:13:14 +0000
372@@ -1,4 +1,5 @@
373 import os
374+import re
375
376 from django.test import TestCase
377 from cms.api import publish_pages
378@@ -328,3 +329,97 @@
379 self.assertTrue(published_pages.has_at_least_size(20))
380 for imported_article in ImportedArticle.objects.all():
381 self.assertTrue(check_imported_article(imported_article))
382+
383+
384+class TestVersionedImportProcessPasses(TestCase):
385+ def runTest(self):
386+ db_empty_page_list()
387+ root = db_create_root_page()
388+ snappy_page = db_add_empty_page('Snappy', root)
389+ guides = db_add_empty_page('Guides', snappy_page)
390+ publish_pages([snappy_page, guides])
391+ ExternalDocsBranch.objects.all().delete()
392+ ExternalDocsBranchImportDirective.objects.all().delete()
393+ ImportedArticle.objects.all().delete()
394+ branch, created = ExternalDocsBranch.objects.get_or_create(
395+ origin=os.path.join(
396+ os.path.dirname(__file__), 'data/snappy-test'),
397+ branch_name='',
398+ version='16.04')
399+ a, created = ExternalDocsBranchImportDirective.objects.get_or_create(
400+ import_from='docs', write_to='snappy/guides',
401+ external_docs_branch=branch)
402+ self.assertIsNotNone(process_branch(branch))
403+
404+
405+class TestTwoVersionedImportProcessPass(TestCase):
406+ def runTest(self):
407+ db_empty_page_list()
408+ root = db_create_root_page()
409+ snappy_page = db_add_empty_page('Snappy', root)
410+ guides = db_add_empty_page('Guides', snappy_page)
411+ publish_pages([snappy_page, guides])
412+ ExternalDocsBranch.objects.all().delete()
413+ ExternalDocsBranchImportDirective.objects.all().delete()
414+ ImportedArticle.objects.all().delete()
415+ branch, created = ExternalDocsBranch.objects.get_or_create(
416+ origin=os.path.join(
417+ os.path.dirname(__file__), 'data/snappy-test'),
418+ branch_name='',
419+ version='16.04')
420+ a, created = ExternalDocsBranchImportDirective.objects.get_or_create(
421+ import_from='docs', write_to='snappy/guides',
422+ external_docs_branch=branch)
423+ self.assertIsNotNone(process_branch(branch))
424+ branch2, created = ExternalDocsBranch.objects.get_or_create(
425+ origin=os.path.join(
426+ os.path.dirname(__file__), 'data/snappy-test'),
427+ branch_name='',
428+ version='15.04')
429+ b, created = ExternalDocsBranchImportDirective.objects.get_or_create(
430+ import_from='docs', write_to='snappy/guides',
431+ external_docs_branch=branch2)
432+ self.assertIsNotNone(process_branch(branch2))
433+
434+
435+class TestTwoVersionedImportsValidExtensionData(TestCase):
436+ def runTest(self):
437+ db_empty_page_list()
438+ root = db_create_root_page()
439+ snappy_page = db_add_empty_page('Snappy', root)
440+ guides = db_add_empty_page('Guides', snappy_page)
441+ publish_pages([snappy_page, guides])
442+ ExternalDocsBranch.objects.all().delete()
443+ ExternalDocsBranchImportDirective.objects.all().delete()
444+ ImportedArticle.objects.all().delete()
445+ branch, created = ExternalDocsBranch.objects.get_or_create(
446+ origin=os.path.join(
447+ os.path.dirname(__file__), 'data/snappy-test'),
448+ branch_name='',
449+ version='16.04')
450+ a, created = ExternalDocsBranchImportDirective.objects.get_or_create(
451+ import_from='docs', write_to='snappy/guides',
452+ external_docs_branch=branch)
453+ self.assertIsNotNone(process_branch(branch))
454+ branch2, created = ExternalDocsBranch.objects.get_or_create(
455+ origin=os.path.join(
456+ os.path.dirname(__file__), 'data/snappy-test'),
457+ branch_name='',
458+ version='15.04')
459+ b, created = ExternalDocsBranchImportDirective.objects.get_or_create(
460+ import_from='docs', write_to='snappy/guides',
461+ external_docs_branch=branch2)
462+ self.assertIsNotNone(process_branch(branch2))
463+
464+ # Check if the REST article was imported into 15.04 and 16.04 namespace
465+ published_pages = PublishedPages()
466+ self.assertTrue(published_pages.contains_url(
467+ '/{}/snappy/guides/15.04/rest/'.format(DEFAULT_LANG)))
468+ self.assertTrue(published_pages.contains_url(
469+ '/{}/snappy/guides/16.04/rest/'.format(DEFAULT_LANG)))
470+
471+ for page in published_pages.pages:
472+ if re.findall(
473+ '\/{}\/snappy\/guides\/1\d\.04\/(\S+?)\/'.format(DEFAULT_LANG),
474+ page.get_absolute_url()):
475+ self.assertTrue(hasattr(page, 'versionextension'))
476
477=== modified file 'md_importer/tests/test_publish.py'
478--- md_importer/tests/test_publish.py 2016-04-21 10:39:58 +0000
479+++ md_importer/tests/test_publish.py 2016-05-14 20:13:14 +0000
480@@ -5,7 +5,10 @@
481 from developer_portal.models import RawHtml
482
483 from md_importer.importer import DEFAULT_LANG
484-from md_importer.importer.publish import ArticlePage
485+from md_importer.importer.publish import (
486+ ArticlePage,
487+ generate_url,
488+)
489
490 from .utils import (
491 db_empty_page_list,
492@@ -75,3 +78,19 @@
493 plugins = placeholder.get_plugins()
494 self.assertEqual(plugins.count(), 1)
495 self.assertIsInstance(plugins[0].get_plugin_instance()[0], RawHtml)
496+
497+
498+class TestGenerateURL(TestCase):
499+ def runTest(self):
500+ self.assertEqual(
501+ generate_url('snappy/guides', ''),
502+ 'snappy/guides')
503+ self.assertEqual(
504+ generate_url('snappy/guides', 'interfaces.md'),
505+ 'snappy/guides/interfaces')
506+ self.assertEqual(
507+ generate_url('snappy/guides', '', '16.04'),
508+ 'snappy/guides/16.04')
509+ self.assertEqual(
510+ generate_url('snappy/guides', 'interfaces.md', '16.04'),
511+ 'snappy/guides/16.04/interfaces')
512
513=== modified file 'md_importer/tests/utils.py'
514--- md_importer/tests/utils.py 2016-04-18 10:30:14 +0000
515+++ md_importer/tests/utils.py 2016-05-14 20:13:14 +0000
516@@ -52,10 +52,10 @@
517 self.root.get_absolute_url(),
518 '/{}/'.format(DEFAULT_LANG))
519
520- def create_repo(self, docs_path):
521+ def create_repo(self, docs_path, version=None):
522 origin = os.path.join(os.path.dirname(__file__), docs_path)
523 self.assertTrue(os.path.exists(origin))
524- self.repo = Repo(self.tempdir, origin, '', '')
525+ self.repo = Repo(self.tempdir, origin, '', '', version=version)
526 self.fetch_retcode = self.repo.get()
527 self.assertEqual(self.fetch_retcode, 0)
528
529
530=== modified file 'templates/cms_page_base.html'
531--- templates/cms_page_base.html 2015-10-21 14:41:23 +0000
532+++ templates/cms_page_base.html 2016-05-14 20:13:14 +0000
533@@ -9,5 +9,19 @@
534 {% endblock extrameta %}
535
536 {% block content %}
537+{% if request.current_page.versionextension %}
538+ <div>
539+ <p>Current version {{ request.current_page.versionextension.version }}</p>
540+ {% if request.current_page.versionextension.alternative_urls %}
541+ <p>Other versions:
542+ <ul>
543+ {% for alternative in request.current_page.versionextension.alternative_urls.all %}
544+ <li><a href="{{ alternative.url }}">{{ alternative.version }}</a></li>
545+ {% endfor %}
546+ </ul>
547+ </p>
548+ {% endif %}
549+ </div>
550+{% endif %}
551 {% placeholder page_content %}
552 {% endblock %}

Subscribers

People subscribed via source and target branches