Merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/soon-trunk into lp:developer-ubuntu-com

Proposed by Daniel Holbach
Status: Merged
Approved by: David Callé
Approved revision: 194
Merged at revision: 190
Proposed branch: lp:~developer-ubuntu-com-dev/developer-ubuntu-com/soon-trunk
Merge into: lp:developer-ubuntu-com
Diff against target: 8893 lines (+6790/-1259)
78 files modified
Makefile (+5/-5)
README.md (+1/-1)
api_docs/migrations/0001_initial.py (+138/-185)
developer_portal/admin.py (+1/-16)
developer_portal/blog/views.py (+18/-6)
developer_portal/management/commands/initdb.py (+26/-20)
developer_portal/management/commands/update-template.py (+13/-16)
developer_portal/migrations/0001_initial.py (+0/-20)
developer_portal/migrations/0002_add_rawhtml_plugin.py (+0/-53)
developer_portal/migrations/0003_add_external_docs_branches.py (+0/-62)
developer_portal/migrations/0004_auto__add_seoextension.py (+0/-126)
developer_portal/models.py (+0/-21)
developer_portal/settings.py (+44/-22)
developer_portal/urls.py (+1/-1)
locale/developer_portal.pot (+69/-54)
md_importer/TODO (+7/-0)
md_importer/admin.py (+36/-0)
md_importer/importer/__init__.py (+5/-0)
md_importer/importer/article.py (+163/-0)
md_importer/importer/process.py (+60/-0)
md_importer/importer/publish.py (+75/-0)
md_importer/importer/repo.py (+174/-0)
md_importer/importer/source.py (+60/-0)
md_importer/management/commands/import_md.py (+15/-344)
md_importer/migrations/0001_initial.py (+46/-0)
md_importer/models.py (+56/-0)
md_importer/tests/data/link-broken-test/file1.md (+5/-0)
md_importer/tests/data/link-broken-test/file2.md (+3/-0)
md_importer/tests/data/link-test/file1.md (+5/-0)
md_importer/tests/data/link-test/file2.md (+3/-0)
md_importer/tests/data/link2-test/file1.md (+5/-0)
md_importer/tests/data/link2-test/file3.md (+3/-0)
md_importer/tests/data/local-image-test/test1.md (+6/-0)
md_importer/tests/data/remote-image-test/test1.md (+6/-0)
md_importer/tests/data/snapcraft-test/CONTRIBUTING.md (+32/-0)
md_importer/tests/data/snapcraft-test/HACKING.md (+48/-0)
md_importer/tests/data/snapcraft-test/README.md (+33/-0)
md_importer/tests/data/snapcraft-test/docs/get-started.md (+47/-0)
md_importer/tests/data/snapcraft-test/docs/intro.md (+104/-0)
md_importer/tests/data/snapcraft-test/docs/ros-snap.md (+366/-0)
md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg (+1199/-0)
md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md (+233/-0)
md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md (+130/-0)
md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md (+83/-0)
md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md (+43/-0)
md_importer/tests/data/snapcraft-test/docs/your-first-snap.md (+318/-0)
md_importer/tests/data/snappy-test/README.md (+119/-0)
md_importer/tests/data/snappy-test/docs/autoupdate.md (+48/-0)
md_importer/tests/data/snappy-test/docs/config.md (+128/-0)
md_importer/tests/data/snappy-test/docs/cross-build.md (+18/-0)
md_importer/tests/data/snappy-test/docs/frameworks.md (+256/-0)
md_importer/tests/data/snappy-test/docs/gadget.md (+244/-0)
md_importer/tests/data/snappy-test/docs/garbage.md (+93/-0)
md_importer/tests/data/snappy-test/docs/hashes.md (+30/-0)
md_importer/tests/data/snappy-test/docs/meta.md (+113/-0)
md_importer/tests/data/snappy-test/docs/package-names.md (+138/-0)
md_importer/tests/data/snappy-test/docs/rest.md (+603/-0)
md_importer/tests/data/snappy-test/docs/security.md (+233/-0)
md_importer/tests/data/snappy-test/docs/system-updates.rst (+421/-0)
md_importer/tests/test_branch_fetch.py (+47/-0)
md_importer/tests/test_branch_import.py (+118/-0)
md_importer/tests/test_import_process.py (+68/-0)
md_importer/tests/test_link_rewrite.py (+86/-0)
md_importer/tests/test_misc.py (+35/-0)
md_importer/tests/test_snappy_import.py (+61/-0)
md_importer/tests/test_utils.py (+50/-0)
md_importer/tests/utils.py (+76/-0)
pip-cache-revno.txt (+1/-1)
requirements.txt (+43/-43)
service/urls.py (+7/-7)
store_data/migrations/0001_initial.py (+62/-133)
store_data/migrations/0002_add_title_field.py (+0/-58)
store_data/migrations/0003_add_website_field.py (+0/-59)
templates/zinnia/_entry_detail_base.html (+2/-2)
templates/zinnia/base.html (+1/-1)
templates/zinnia/category_list.html (+2/-1)
templates/zinnia/entry_detail_base.html (+1/-1)
templates/zinnia/entry_list.html (+1/-1)
To merge this branch: bzr merge lp:~developer-ubuntu-com-dev/developer-ubuntu-com/soon-trunk
Reviewer Review Type Date Requested Status
David Callé Pending
Michael Hall Pending
Daniel Holbach Pending
Ubuntu App Developer site developers Pending
Review via email: mp+283019@code.launchpad.net

Description of the change

This is an MP which bundles the changes in

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

and fixes issues because of changes which were backed out previously.

This should be reviewed and tested carefully by all of us.

To post a comment you must log in.
Revision history for this message
David Callé (davidc3) wrote :

Thanks Daniel, I'll start testing today

Revision history for this message
Daniel Holbach (dholbach) wrote :

pip-cache needs an update to add pymdown-extensions (for markdown tables, fenced code blocks, etc.)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2016-01-12 14:24:40 +0000
3+++ Makefile 2016-01-19 00:21:38 +0000
4@@ -46,7 +46,7 @@
5
6 syncdb:
7 @echo "Syncing database"
8- @python manage.py syncdb --noinput --migrate --settings charm_settings
9+ @python manage.py migrate --noinput --settings charm_settings
10
11 collectstatic: collectstatic.done
12 collectstatic.done:
13@@ -61,18 +61,18 @@
14 update-pip-cache:
15 @echo "Updating pip-cache"
16 rm -rf pip-cache
17- bzr branch lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies pip-cache
18+ bzr branch lp:developer-ubuntu-com/dependencies pip-cache
19 pip install --exists-action=w --download pip-cache/ -r requirements.txt
20 bzr add pip-cache/*
21 bzr commit pip-cache/ -m 'automatically updated devportal requirements'
22- bzr push --directory pip-cache lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies
23+ bzr push --directory pip-cache lp:developer-ubuntu-com/dependencies
24 bzr revno pip-cache > pip-cache-revno.txt
25 rm -rf pip-cache
26 @echo "** Remember to commit pip-cache-revno.txt"
27
28 pip-cache:
29 @echo "Downloading pip-cache"
30- @bzr branch -r `cat pip-cache-revno.txt` lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies pip-cache
31+ @bzr branch -r `cat pip-cache-revno.txt` lp:developer-ubuntu-com/dependencies pip-cache
32
33 env: pip-cache
34 @echo "Creating virtualenv"
35@@ -82,7 +82,7 @@
36
37 db.sqlite3: env
38 @echo "Initializing database"
39- @./env/bin/python manage.py syncdb --noinput --migrate
40+ @./env/bin/python manage.py migrate --noinput
41 @./env/bin/python manage.py initdb
42 @./env/bin/python manage.py init_apidocs
43
44
45=== modified file 'README.md'
46--- README.md 2015-12-08 10:25:29 +0000
47+++ README.md 2016-01-19 00:21:38 +0000
48@@ -27,7 +27,7 @@
49 ./env/bin/python manage.py flush --noinput
50 echo "delete from auth_permission;" | ./env/bin/python manage.py dbshell
51 ./env/bin/python manage.py loaddata ../dbbackup/dbdump.json
52- ./env/bin/python manage.py syncdb --noinput --all
53+ ./env/bin/python manage.py migrate --noinput
54 ./env/bin/python manage.py initdb
55
56 # Managing translations
57
58=== modified file 'api_docs/migrations/0001_initial.py'
59--- api_docs/migrations/0001_initial.py 2015-12-08 10:25:29 +0000
60+++ api_docs/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
61@@ -1,186 +1,139 @@
62 # -*- coding: utf-8 -*-
63-from south.utils import datetime_utils as datetime
64-from south.db import db
65-from south.v2 import SchemaMigration
66-from django.db import models
67-
68-
69-class Migration(SchemaMigration):
70-
71- def forwards(self, orm):
72- # Adding model 'Topic'
73- db.create_table(u'api_docs_topic', (
74- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
75- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
76- ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
77- ))
78- db.send_create_signal(u'api_docs', ['Topic'])
79-
80- # Adding model 'Language'
81- db.create_table(u'api_docs_language', (
82- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
83- ('topic', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Topic'])),
84- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
85- ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
86- ('current_version', self.gf('django.db.models.fields.related.ForeignKey')(related_name='current_for_lang', null=True, to=orm['api_docs.Version'])),
87- ('development_version', self.gf('django.db.models.fields.related.ForeignKey')(related_name='development_for_lang', null=True, to=orm['api_docs.Version'])),
88- ))
89- db.send_create_signal(u'api_docs', ['Language'])
90-
91- # Adding model 'Version'
92- db.create_table(u'api_docs_version', (
93- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
94- ('language', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Language'], null=True)),
95- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
96- ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
97- ))
98- db.send_create_signal(u'api_docs', ['Version'])
99-
100- # Adding model 'Section'
101- db.create_table(u'api_docs_section', (
102- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
103- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
104- ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
105- ('topic_version', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Version'])),
106- ))
107- db.send_create_signal(u'api_docs', ['Section'])
108-
109- # Adding model 'Namespace'
110- db.create_table(u'api_docs_namespace', (
111- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
112- ('platform_section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
113- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
114- ('display_name', self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True)),
115- ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
116- ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
117- ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
118- ))
119- db.send_create_signal(u'api_docs', ['Namespace'])
120-
121- # Adding model 'Element'
122- db.create_table(u'api_docs_element', (
123- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
124- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
125- ('description', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
126- ('namespace', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Namespace'], null=True, blank=True)),
127- ('section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
128- ('fullname', self.gf('django.db.models.fields.CharField')(max_length=128)),
129- ('keywords', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
130- ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
131- ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
132- ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
133- ))
134- db.send_create_signal(u'api_docs', ['Element'])
135-
136- # Adding model 'Page'
137- db.create_table(u'api_docs_page', (
138- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
139- ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
140- ('title', self.gf('django.db.models.fields.CharField')(max_length=64)),
141- ('description', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
142- ('namespace', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Namespace'], null=True, blank=True)),
143- ('section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
144- ('fullname', self.gf('django.db.models.fields.CharField')(max_length=128)),
145- ('keywords', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
146- ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
147- ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
148- ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
149- ('order_index', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, blank=True)),
150- ))
151- db.send_create_signal(u'api_docs', ['Page'])
152-
153-
154- def backwards(self, orm):
155- # Deleting model 'Topic'
156- db.delete_table(u'api_docs_topic')
157-
158- # Deleting model 'Language'
159- db.delete_table(u'api_docs_language')
160-
161- # Deleting model 'Version'
162- db.delete_table(u'api_docs_version')
163-
164- # Deleting model 'Section'
165- db.delete_table(u'api_docs_section')
166-
167- # Deleting model 'Namespace'
168- db.delete_table(u'api_docs_namespace')
169-
170- # Deleting model 'Element'
171- db.delete_table(u'api_docs_element')
172-
173- # Deleting model 'Page'
174- db.delete_table(u'api_docs_page')
175-
176-
177- models = {
178- u'api_docs.element': {
179- 'Meta': {'ordering': "('name',)", 'object_name': 'Element'},
180- 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
181- 'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
182- 'fullname': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
183- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
184- 'keywords': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
185- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
186- 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Namespace']", 'null': 'True', 'blank': 'True'}),
187- 'section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
188- 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
189- 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'})
190- },
191- u'api_docs.language': {
192- 'Meta': {'object_name': 'Language'},
193- 'current_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'current_for_lang'", 'null': 'True', 'to': u"orm['api_docs.Version']"}),
194- 'development_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'development_for_lang'", 'null': 'True', 'to': u"orm['api_docs.Version']"}),
195- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
196- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
197- 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
198- 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Topic']"})
199- },
200- u'api_docs.namespace': {
201- 'Meta': {'ordering': "('name',)", 'object_name': 'Namespace'},
202- 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
203- 'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
204- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
205- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
206- 'platform_section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
207- 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
208- 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'})
209- },
210- u'api_docs.page': {
211- 'Meta': {'ordering': "('order_index',)", 'object_name': 'Page'},
212- 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
213- 'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
214- 'fullname': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
215- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
216- 'keywords': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
217- 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Namespace']", 'null': 'True', 'blank': 'True'}),
218- 'order_index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
219- 'section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
220- 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
221- 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
222- 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
223- 'title': ('django.db.models.fields.CharField', [], {'max_length': '64'})
224- },
225- u'api_docs.section': {
226- 'Meta': {'object_name': 'Section'},
227- 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
228- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
229- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
230- 'topic_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Version']"})
231- },
232- u'api_docs.topic': {
233- 'Meta': {'object_name': 'Topic'},
234- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
235- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
236- 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'})
237- },
238- u'api_docs.version': {
239- 'Meta': {'object_name': 'Version'},
240- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241- 'language': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Language']", 'null': 'True'}),
242- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
243- 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'})
244- }
245- }
246-
247- complete_apps = ['api_docs']
248\ No newline at end of file
249+from __future__ import unicode_literals
250+
251+from django.db import migrations, models
252+
253+
254+class Migration(migrations.Migration):
255+
256+ dependencies = [
257+ ]
258+
259+ operations = [
260+ migrations.CreateModel(
261+ name='Element',
262+ fields=[
263+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
264+ ('name', models.CharField(max_length=64)),
265+ ('description', models.CharField(default=b'', max_length=256, blank=True)),
266+ ('fullname', models.CharField(max_length=128)),
267+ ('keywords', models.CharField(default=b'', max_length=256, blank=True)),
268+ ('data', models.TextField(default=b'', blank=True)),
269+ ('source_file', models.CharField(max_length=128, null=True, blank=True)),
270+ ('source_format', models.CharField(max_length=32, null=True, blank=True)),
271+ ],
272+ options={
273+ 'ordering': ('name',),
274+ 'verbose_name': 'Rendered Element',
275+ 'verbose_name_plural': 'Rendered Elements',
276+ },
277+ ),
278+ migrations.CreateModel(
279+ name='Language',
280+ fields=[
281+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
282+ ('name', models.CharField(max_length=64)),
283+ ('slug', models.CharField(max_length=64)),
284+ ],
285+ ),
286+ migrations.CreateModel(
287+ name='Namespace',
288+ fields=[
289+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
290+ ('name', models.CharField(max_length=64)),
291+ ('display_name', models.CharField(default=b'', max_length=64, blank=True)),
292+ ('data', models.TextField(default=b'', blank=True)),
293+ ('source_file', models.CharField(max_length=128, null=True, blank=True)),
294+ ('source_format', models.CharField(max_length=32, null=True, blank=True)),
295+ ],
296+ options={
297+ 'ordering': ('name',),
298+ },
299+ ),
300+ migrations.CreateModel(
301+ name='Page',
302+ fields=[
303+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
304+ ('slug', models.CharField(max_length=64)),
305+ ('title', models.CharField(max_length=64)),
306+ ('description', models.CharField(default=b'', max_length=256, blank=True)),
307+ ('fullname', models.CharField(max_length=128)),
308+ ('keywords', models.CharField(default=b'', max_length=256, blank=True)),
309+ ('data', models.TextField(default=b'', blank=True)),
310+ ('source_file', models.CharField(max_length=128, null=True, blank=True)),
311+ ('source_format', models.CharField(max_length=32, null=True, blank=True)),
312+ ('order_index', models.PositiveIntegerField(default=0, blank=True)),
313+ ('namespace', models.ForeignKey(blank=True, to='api_docs.Namespace', null=True)),
314+ ],
315+ options={
316+ 'ordering': ('order_index',),
317+ 'verbose_name': 'Rendered Page',
318+ 'verbose_name_plural': 'Rendered Pages',
319+ },
320+ ),
321+ migrations.CreateModel(
322+ name='Section',
323+ fields=[
324+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
325+ ('name', models.CharField(max_length=64)),
326+ ('description', models.TextField(null=True, blank=True)),
327+ ],
328+ ),
329+ migrations.CreateModel(
330+ name='Topic',
331+ fields=[
332+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
333+ ('name', models.CharField(max_length=64)),
334+ ('slug', models.CharField(max_length=64)),
335+ ],
336+ ),
337+ migrations.CreateModel(
338+ name='Version',
339+ fields=[
340+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
341+ ('name', models.CharField(max_length=64)),
342+ ('slug', models.CharField(max_length=64)),
343+ ('language', models.ForeignKey(to='api_docs.Language', null=True)),
344+ ],
345+ ),
346+ migrations.AddField(
347+ model_name='section',
348+ name='topic_version',
349+ field=models.ForeignKey(to='api_docs.Version'),
350+ ),
351+ migrations.AddField(
352+ model_name='page',
353+ name='section',
354+ field=models.ForeignKey(to='api_docs.Section'),
355+ ),
356+ migrations.AddField(
357+ model_name='namespace',
358+ name='platform_section',
359+ field=models.ForeignKey(to='api_docs.Section'),
360+ ),
361+ migrations.AddField(
362+ model_name='language',
363+ name='current_version',
364+ field=models.ForeignKey(related_name='current_for_lang', blank=True, to='api_docs.Version', null=True),
365+ ),
366+ migrations.AddField(
367+ model_name='language',
368+ name='development_version',
369+ field=models.ForeignKey(related_name='development_for_lang', blank=True, to='api_docs.Version', null=True),
370+ ),
371+ migrations.AddField(
372+ model_name='language',
373+ name='topic',
374+ field=models.ForeignKey(to='api_docs.Topic'),
375+ ),
376+ migrations.AddField(
377+ model_name='element',
378+ name='namespace',
379+ field=models.ForeignKey(blank=True, to='api_docs.Namespace', null=True),
380+ ),
381+ migrations.AddField(
382+ model_name='element',
383+ name='section',
384+ field=models.ForeignKey(to='api_docs.Section'),
385+ ),
386+ ]
387
388=== modified file 'developer_portal/admin.py'
389--- developer_portal/admin.py 2015-12-08 10:25:29 +0000
390+++ developer_portal/admin.py 2016-01-19 00:21:38 +0000
391@@ -4,20 +4,12 @@
392 from reversion.admin import VersionAdmin
393
394 from cms.extensions import TitleExtensionAdmin
395-from .models import ExternalDocsBranch, SEOExtension
396-from django.core.management import call_command
397+from .models import SEOExtension
398
399 __all__ = (
400 )
401
402
403-def import_selected_external_docs_branches(modeladmin, request, queryset):
404- for branch in queryset:
405- call_command('import-external-docs-branches', branch.docs_namespace)
406- import_selected_external_docs_branches.short_description = \
407- "Import selected branches"
408-
409-
410 class RevisionAdmin(admin.ModelAdmin):
411 list_display = ('date_created', 'user', 'comment')
412 list_display_links = ('date_created', )
413@@ -35,13 +27,6 @@
414 admin.site.register(Version, VersionAdmin)
415
416
417-class ExternalDocsBranchAdmin(admin.ModelAdmin):
418- list_display = ('lp_origin', 'docs_namespace')
419- list_filter = ('lp_origin', 'docs_namespace')
420- actions = [import_selected_external_docs_branches]
421-
422-admin.site.register(ExternalDocsBranch, ExternalDocsBranchAdmin)
423-
424 class SEOExtensionAdmin(TitleExtensionAdmin):
425 pass
426
427
428=== modified file 'developer_portal/blog/views.py'
429--- developer_portal/blog/views.py 2015-12-08 10:25:29 +0000
430+++ developer_portal/blog/views.py 2016-01-19 00:21:38 +0000
431@@ -35,7 +35,9 @@
432 return super(MultiLangEntryIndex, self).get(request, *args, **kwargs)
433
434 def get_dated_queryset(self, ordering=None, **lookup):
435- return super(MultiLangEntryIndex, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
436+ if ordering:
437+ return super(MultiLangEntryIndex, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
438+ return super(MultiLangEntryIndex, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
439
440 class MultiLangEntryYear(MultiLangMixin, EntryYear):
441 def get(self, request, *args, **kwargs):
442@@ -43,7 +45,9 @@
443 return super(MultiLangEntryYear, self).get(request, *args, **kwargs)
444
445 def get_dated_queryset(self, ordering=None, **lookup):
446- return super(MultiLangEntryYear, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
447+ if ordering:
448+ return super(MultiLangEntryYear, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
449+ return super(MultiLangEntryYear, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
450
451 class MultiLangEntryMonth(MultiLangMixin, EntryMonth):
452 def get(self, request, *args, **kwargs):
453@@ -51,7 +55,9 @@
454 return super(MultiLangEntryMonth, self).get(request, *args, **kwargs)
455
456 def get_dated_queryset(self, ordering=None, **lookup):
457- return super(MultiLangEntryMonth, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
458+ if ordering:
459+ return super(MultiLangEntryMonth, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
460+ return super(MultiLangEntryMonth, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
461
462 class MultiLangEntryWeek(MultiLangMixin, EntryWeek):
463 def get(self, request, *args, **kwargs):
464@@ -59,7 +65,9 @@
465 return super(MultiLangEntryWeek, self).get(request, *args, **kwargs)
466
467 def get_dated_queryset(self, ordering=None, **lookup):
468- return super(MultiLangEntryWeek, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
469+ if ordering:
470+ return super(MultiLangEntryWeek, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
471+ return super(MultiLangEntryWeek, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
472
473 class MultiLangEntryDay(MultiLangMixin, EntryDay):
474 def get(self, request, *args, **kwargs):
475@@ -67,7 +75,9 @@
476 return super(MultiLangEntryDay, self).get(request, *args, **kwargs)
477
478 def get_dated_queryset(self, ordering=None, **lookup):
479- return super(MultiLangEntryDay, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
480+ if ordering:
481+ return super(MultiLangEntryDay, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
482+ return super(MultiLangEntryDay, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
483
484 class MultiLangEntryToday(MultiLangMixin, EntryToday):
485 def get(self, request, *args, **kwargs):
486@@ -75,4 +85,6 @@
487 return super(MultiLangEntryToday, self).get(request, *args, **kwargs)
488
489 def get_dated_queryset(self, ordering=None, **lookup):
490- return super(MultiLangEntryToday, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)
491+ if ordering:
492+ return super(MultiLangEntryToday, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
493+ return super(MultiLangEntryToday, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
494
495=== modified file 'developer_portal/management/commands/initdb.py'
496--- developer_portal/management/commands/initdb.py 2015-12-08 10:25:29 +0000
497+++ developer_portal/management/commands/initdb.py 2016-01-19 00:21:38 +0000
498@@ -1,17 +1,12 @@
499 #!/usr/bin/python
500
501 from django.core.management.base import BaseCommand
502-from optparse import make_option
503-
504 from django.conf import settings
505
506-import subprocess
507-import os
508-import sys
509-
510-from django.contrib.auth.models import User, Group, Permission
511-from django.contrib.contenttypes.models import ContentType
512+from django.contrib.auth.models import User, Permission
513 from cms.models.permissionmodels import PageUserGroup, GlobalPagePermission
514+from zinnia.models import Category
515+
516
517 class Command(BaseCommand):
518 help = "Make sure the Developer Portal database is set up properly."
519@@ -20,24 +15,25 @@
520
521 all_perms = Permission.objects.filter()
522
523- print "Creating admin user."
524+ print("Creating admin user.")
525 admin, created = User.objects.get_or_create(username='system')
526 admin.is_staff = True
527 admin.is_superuser = True
528 admin.save()
529
530 if hasattr(settings, 'ADMIN_GROUP') and settings.ADMIN_GROUP != "":
531- print "Configuring "+settings.ADMIN_GROUP+" group."
532- admins, created = PageUserGroup.objects.get_or_create(name=settings.ADMIN_GROUP, defaults={'created_by': admin})
533+ print("Configuring {} group.".format(settings.ADMIN_GROUP))
534+ admins, created = PageUserGroup.objects.get_or_create(
535+ name=settings.ADMIN_GROUP, defaults={'created_by': admin})
536 admins.permissions.add(*list(all_perms))
537
538- print "Configuring global permissions for group."
539+ print("Configuring global permissions for group.")
540 adminperms, created = GlobalPagePermission.objects.get_or_create(
541 # who:
542- group = admins,
543+ group=admins,
544
545 # what:
546- defaults = {
547+ defaults={
548 'can_change': True,
549 'can_add': True,
550 'can_delete': True,
551@@ -51,18 +47,20 @@
552 adminperms.sites.add(settings.SITE_ID)
553
554 if hasattr(settings, 'EDITOR_GROUP') and settings.EDITOR_GROUP != "":
555- print "Configuring "+settings.EDITOR_GROUP+" group."
556- editors, created = PageUserGroup.objects.get_or_create(name=settings.EDITOR_GROUP, defaults={'created_by': admin})
557- page_perms = Permission.objects.filter(content_type__app_label='cms', content_type__name='page')
558+ print("Configuring {} group.".format(settings.EDITOR_GROUP))
559+ editors, created = PageUserGroup.objects.get_or_create(
560+ name=settings.EDITOR_GROUP, defaults={'created_by': admin})
561+ page_perms = Permission.objects.filter(
562+ content_type__app_label='cms', content_type__model='page')
563 editors.permissions.add(*list(page_perms))
564
565- print "Configuring global permissions for group."
566+ print("Configuring global permissions for group.")
567 editorsperms, created = GlobalPagePermission.objects.get_or_create(
568 # who:
569- group = editors,
570+ group=editors,
571
572 # what:
573- defaults = {
574+ defaults={
575 'can_change': True,
576 'can_add': True,
577 'can_delete': True,
578@@ -74,3 +72,11 @@
579 }
580 )
581 editorsperms.sites.add(settings.SITE_ID)
582+
583+ print('Adding zinnia categories for the following: {}.'.format(
584+ ', '.join([a[0] for a in settings.LANGUAGES])))
585+ for lang in settings.LANGUAGES:
586+ if lang[1] == 'Simplified Chinese':
587+ Category.objects.get_or_create(title='Chinese', slug=lang[0])
588+ else:
589+ Category.objects.get_or_create(title=lang[1], slug=lang[0])
590
591=== modified file 'developer_portal/management/commands/update-template.py'
592--- developer_portal/management/commands/update-template.py 2015-07-09 07:23:54 +0000
593+++ developer_portal/management/commands/update-template.py 2016-01-19 00:21:38 +0000
594@@ -1,38 +1,35 @@
595 #!/usr/bin/python
596
597 from django.core.management.base import NoArgsCommand
598+from django.core.management import call_command
599
600-import subprocess
601 import os
602-import sys
603
604 from django.conf import settings
605
606-APP_NAME = "developer_portal"
607+APP_NAME = 'developer_portal'
608
609-DUMMY_LOCALE = "xx"
610+DUMMY_LOCALE = 'xx'
611
612
613 def update_template():
614- pwd = os.getcwd()
615- os.chdir(settings.PROJECT_PATH)
616- subprocess.call([sys.executable, "manage.py", "makemessages",
617- "--keep-pot", "--all", "-i", "env/*", "-i", "urls.py",
618- "-l", DUMMY_LOCALE])
619- project_locale_path = os.path.join(settings.PROJECT_PATH, "locale")
620+ call_command(
621+ 'makemessages',
622+ '--keep-pot', '-i', 'env/*', '-i', 'urls.py',
623+ '-l', DUMMY_LOCALE)
624+ project_locale_path = os.path.join(settings.PROJECT_PATH, 'locale')
625 os.rename(os.path.join(project_locale_path,
626- "%s/LC_MESSAGES/django.po" % DUMMY_LOCALE),
627- os.path.join(project_locale_path, "%s.pot" % APP_NAME))
628+ '%s/LC_MESSAGES/django.po' % DUMMY_LOCALE),
629+ os.path.join(project_locale_path, '%s.pot' % APP_NAME))
630 os.removedirs(os.path.join(project_locale_path,
631- "%s/LC_MESSAGES" % DUMMY_LOCALE))
632- old_pot_fn = os.path.join(project_locale_path, "django.pot")
633+ '%s/LC_MESSAGES' % DUMMY_LOCALE))
634+ old_pot_fn = os.path.join(project_locale_path, 'django.pot')
635 if os.path.exists(old_pot_fn):
636 os.remove(old_pot_fn)
637- os.chdir(pwd)
638
639
640 class Command(NoArgsCommand):
641- help = "Update translations template."
642+ help = 'Update translations template.'
643
644 def handle_noargs(self, **options):
645 update_template()
646
647=== added file 'developer_portal/migrations/0001_initial.py'
648--- developer_portal/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
649+++ developer_portal/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
650@@ -0,0 +1,36 @@
651+# -*- coding: utf-8 -*-
652+from __future__ import unicode_literals
653+
654+from django.db import migrations, models
655+
656+
657+class Migration(migrations.Migration):
658+
659+ dependencies = [
660+ ]
661+
662+ operations = [
663+ migrations.CreateModel(
664+ name='RawHtml',
665+ fields=[
666+ ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
667+ ('body', models.TextField(verbose_name='body')),
668+ ],
669+ options={
670+ 'abstract': False,
671+ },
672+ bases=('cms.cmsplugin',),
673+ ),
674+ migrations.CreateModel(
675+ name='SEOExtension',
676+ fields=[
677+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
678+ ('keywords', models.CharField(max_length=256)),
679+ ('extended_object', models.OneToOneField(editable=False, to='cms.Title')),
680+ ('public_extension', models.OneToOneField(related_name='draft_extension', null=True, editable=False, to='developer_portal.SEOExtension')),
681+ ],
682+ options={
683+ 'abstract': False,
684+ },
685+ ),
686+ ]
687
688=== removed file 'developer_portal/migrations/0001_initial.py'
689--- developer_portal/migrations/0001_initial.py 2015-12-08 10:25:29 +0000
690+++ developer_portal/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
691@@ -1,20 +0,0 @@
692-# -*- coding: utf-8 -*-
693-from south.utils import datetime_utils as datetime
694-from south.db import db
695-from south.v2 import SchemaMigration
696-from django.db import models
697-
698-
699-class Migration(SchemaMigration):
700-
701- def forwards(self, orm):
702- pass
703-
704- def backwards(self, orm):
705- pass
706-
707- models = {
708-
709- }
710-
711- complete_apps = ['developer_portal']
712\ No newline at end of file
713
714=== removed file 'developer_portal/migrations/0002_add_rawhtml_plugin.py'
715--- developer_portal/migrations/0002_add_rawhtml_plugin.py 2015-12-08 10:25:29 +0000
716+++ developer_portal/migrations/0002_add_rawhtml_plugin.py 1970-01-01 00:00:00 +0000
717@@ -1,53 +0,0 @@
718-# -*- coding: utf-8 -*-
719-from south.utils import datetime_utils as datetime
720-from south.db import db
721-from south.v2 import SchemaMigration
722-from django.db import models
723-
724-
725-class Migration(SchemaMigration):
726-
727- def forwards(self, orm):
728- # Adding model 'RawHtml'
729- db.create_table(u'developer_portal_rawhtml', (
730- (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)),
731- ('body', self.gf('django.db.models.fields.TextField')()),
732- ))
733- db.send_create_signal(u'developer_portal', ['RawHtml'])
734-
735-
736- def backwards(self, orm):
737- # Deleting model 'RawHtml'
738- db.delete_table(u'developer_portal_rawhtml')
739-
740-
741- models = {
742- 'cms.cmsplugin': {
743- 'Meta': {'object_name': 'CMSPlugin'},
744- 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
745- 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
746- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
747- 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
748- 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
749- 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
750- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
751- 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
752- 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
753- 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
754- 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
755- 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
756- },
757- 'cms.placeholder': {
758- 'Meta': {'object_name': 'Placeholder'},
759- 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
760- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
761- 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
762- },
763- u'developer_portal.rawhtml': {
764- 'Meta': {'object_name': 'RawHtml'},
765- 'body': ('django.db.models.fields.TextField', [], {}),
766- u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
767- }
768- }
769-
770- complete_apps = ['developer_portal']
771\ No newline at end of file
772
773=== removed file 'developer_portal/migrations/0003_add_external_docs_branches.py'
774--- developer_portal/migrations/0003_add_external_docs_branches.py 2015-12-08 10:25:29 +0000
775+++ developer_portal/migrations/0003_add_external_docs_branches.py 1970-01-01 00:00:00 +0000
776@@ -1,62 +0,0 @@
777-# -*- coding: utf-8 -*-
778-from south.utils import datetime_utils as datetime
779-from south.db import db
780-from south.v2 import SchemaMigration
781-from django.db import models
782-
783-
784-class Migration(SchemaMigration):
785-
786- def forwards(self, orm):
787- # Adding model 'ExternalDocsBranch'
788- db.create_table(u'developer_portal_externaldocsbranch', (
789- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
790- ('lp_origin', self.gf('django.db.models.fields.CharField')(max_length=200)),
791- ('docs_namespace', self.gf('django.db.models.fields.CharField')(max_length=120)),
792- ('index_doc', self.gf('django.db.models.fields.CharField')(max_length=120, blank=True)),
793- ))
794- db.send_create_signal(u'developer_portal', ['ExternalDocsBranch'])
795-
796-
797- def backwards(self, orm):
798- # Deleting model 'ExternalDocsBranch'
799- db.delete_table(u'developer_portal_externaldocsbranch')
800-
801-
802- models = {
803- 'cms.cmsplugin': {
804- 'Meta': {'object_name': 'CMSPlugin'},
805- 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
806- 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
807- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
808- 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
809- 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
810- 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
811- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
812- 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
813- 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
814- 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
815- 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
816- 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
817- },
818- 'cms.placeholder': {
819- 'Meta': {'object_name': 'Placeholder'},
820- 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
821- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
822- 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
823- },
824- u'developer_portal.externaldocsbranch': {
825- 'Meta': {'object_name': 'ExternalDocsBranch'},
826- 'docs_namespace': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
827- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
828- 'index_doc': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}),
829- 'lp_origin': ('django.db.models.fields.CharField', [], {'max_length': '200'})
830- },
831- u'developer_portal.rawhtml': {
832- 'Meta': {'object_name': 'RawHtml'},
833- 'body': ('django.db.models.fields.TextField', [], {}),
834- u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
835- }
836- }
837-
838- complete_apps = ['developer_portal']
839\ No newline at end of file
840
841=== removed file 'developer_portal/migrations/0004_auto__add_seoextension.py'
842--- developer_portal/migrations/0004_auto__add_seoextension.py 2015-12-08 10:25:29 +0000
843+++ developer_portal/migrations/0004_auto__add_seoextension.py 1970-01-01 00:00:00 +0000
844@@ -1,126 +0,0 @@
845-# -*- coding: utf-8 -*-
846-from south.utils import datetime_utils as datetime
847-from south.db import db
848-from south.v2 import SchemaMigration
849-from django.db import models
850-
851-
852-class Migration(SchemaMigration):
853-
854- def forwards(self, orm):
855- # Adding model 'SEOExtension'
856- db.create_table(u'developer_portal_seoextension', (
857- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
858- ('public_extension', self.gf('django.db.models.fields.related.OneToOneField')(related_name='draft_extension', unique=True, null=True, to=orm['developer_portal.SEOExtension'])),
859- ('extended_object', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.Title'], unique=True)),
860- ('keywords', self.gf('django.db.models.fields.CharField')(max_length=256)),
861- ))
862- db.send_create_signal(u'developer_portal', ['SEOExtension'])
863-
864-
865- def backwards(self, orm):
866- # Deleting model 'SEOExtension'
867- db.delete_table(u'developer_portal_seoextension')
868-
869-
870- models = {
871- 'cms.cmsplugin': {
872- 'Meta': {'object_name': 'CMSPlugin'},
873- 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
874- 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
875- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
876- 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
877- 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
878- 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
879- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
880- 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
881- 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
882- 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
883- 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
884- 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
885- },
886- 'cms.page': {
887- 'Meta': {'ordering': "('tree_id', 'lft')", 'unique_together': "(('publisher_is_draft', 'application_namespace'), ('reverse_id', 'site', 'publisher_is_draft'))", 'object_name': 'Page'},
888- 'application_namespace': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
889- 'application_urls': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
890- 'changed_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
891- 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
892- 'created_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
893- 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
894- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
895- 'in_navigation': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
896- 'is_home': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
897- 'languages': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
898- 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
899- 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
900- 'limit_visibility_in_menu': ('django.db.models.fields.SmallIntegerField', [], {'default': 'None', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
901- 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
902- 'navigation_extenders': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}),
903- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['cms.Page']"}),
904- 'placeholders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cms.Placeholder']", 'symmetrical': 'False'}),
905- 'publication_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
906- 'publication_end_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
907- 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
908- 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Page']"}),
909- 'reverse_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
910- 'revision_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
911- 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
912- 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_pages'", 'to': u"orm['sites.Site']"}),
913- 'soft_root': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
914- 'template': ('django.db.models.fields.CharField', [], {'default': "'INHERIT'", 'max_length': '100'}),
915- 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
916- 'xframe_options': ('django.db.models.fields.IntegerField', [], {'default': '0'})
917- },
918- 'cms.placeholder': {
919- 'Meta': {'object_name': 'Placeholder'},
920- 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
921- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
922- 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
923- },
924- 'cms.title': {
925- 'Meta': {'unique_together': "(('language', 'page'),)", 'object_name': 'Title'},
926- 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
927- 'has_url_overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
928- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
929- 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
930- 'menu_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
931- 'meta_description': ('django.db.models.fields.TextField', [], {'max_length': '155', 'null': 'True', 'blank': 'True'}),
932- 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'title_set'", 'to': "orm['cms.Page']"}),
933- 'page_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
934- 'path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
935- 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
936- 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
937- 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Title']"}),
938- 'publisher_state': ('django.db.models.fields.SmallIntegerField', [], {'default': '0', 'db_index': 'True'}),
939- 'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
940- 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
941- 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
942- },
943- u'developer_portal.externaldocsbranch': {
944- 'Meta': {'object_name': 'ExternalDocsBranch'},
945- 'docs_namespace': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
946- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
947- 'index_doc': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}),
948- 'lp_origin': ('django.db.models.fields.CharField', [], {'max_length': '200'})
949- },
950- u'developer_portal.rawhtml': {
951- 'Meta': {'object_name': 'RawHtml'},
952- 'body': ('django.db.models.fields.TextField', [], {}),
953- u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
954- },
955- u'developer_portal.seoextension': {
956- 'Meta': {'object_name': 'SEOExtension'},
957- 'extended_object': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.Title']", 'unique': 'True'}),
958- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
959- 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
960- 'public_extension': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'draft_extension'", 'unique': 'True', 'null': 'True', 'to': u"orm['developer_portal.SEOExtension']"})
961- },
962- u'sites.site': {
963- 'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"},
964- 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
965- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
966- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
967- }
968- }
969-
970- complete_apps = ['developer_portal']
971\ No newline at end of file
972
973=== modified file 'developer_portal/models.py'
974--- developer_portal/models.py 2015-11-02 16:47:26 +0000
975+++ developer_portal/models.py 2016-01-19 00:21:38 +0000
976@@ -1,7 +1,5 @@
977 from django.db import models
978-from django.utils.translation import ugettext_lazy as _
979
980-from cms.models import CMSPlugin
981 from cms.extensions import TitleExtension
982 from cms.extensions.extension_pool import extension_pool
983 from djangocms_text_ckeditor.html import extract_images
984@@ -20,25 +18,6 @@
985 AbstractText.save(self, *args, **kwargs)
986
987
988-class ExternalDocsBranch(models.Model):
989- # We originally assumed that branches would also live in LP,
990- # well, we were wrong, but let's keep the name around. It's
991- # no use having a schema/data migration just for this.
992- lp_origin = models.CharField(
993- max_length=200,
994- help_text=_('External branch location, ie: lp:snappy/15.04 or '
995- 'https://github.com/ubuntu-core/snappy.git'))
996- docs_namespace = models.CharField(
997- max_length=120,
998- help_text=_('Path alias we want to use for the docs, '
999- 'ie "snappy/guides/15.04" or '
1000- '"snappy/guides/latest", etc.'))
1001- index_doc = models.CharField(
1002- max_length=120,
1003- help_text=_('File name of doc to be used as index document, '
1004- 'ie "intro.md"'),
1005- blank=True)
1006-
1007 class SEOExtension(TitleExtension):
1008 keywords = models.CharField(max_length=256)
1009
1010
1011=== modified file 'developer_portal/settings.py'
1012--- developer_portal/settings.py 2015-12-08 10:25:29 +0000
1013+++ developer_portal/settings.py 2016-01-19 00:21:38 +0000
1014@@ -30,8 +30,6 @@
1015 # SECURITY WARNING: don't run with debug turned on in production!
1016 DEBUG = True
1017
1018-TEMPLATE_DEBUG = True
1019-
1020 ALLOWED_HOSTS = ['127.0.0.1', 'developer.ubuntu.com']
1021
1022
1023@@ -49,14 +47,13 @@
1024 # Allow login from Ubuntu SSO
1025 'django_openid_auth',
1026
1027- 'mptt', #utilities for implementing a modified pre-order traversal tree
1028 'menus', #helper for model independent hierarchical website navigation
1029- 'south', #intelligent schema and data migrations
1030 'sekizai', #for javascript and css management
1031 'reversion', #content versioning
1032 'django_pygments',
1033 'django_comments',
1034 'tagging',
1035+ 'template_debug',
1036
1037 'ckeditor',
1038 'djangocms_text_ckeditor',
1039@@ -66,6 +63,7 @@
1040 'djangocms_picture',
1041 'djangocms_video',
1042 'djangocms_snippet',
1043+ 'treebeard',
1044
1045 'cmsplugin_zinnia',
1046 'zinnia',
1047@@ -78,6 +76,8 @@
1048 'store_data',
1049
1050 'api_docs',
1051+
1052+ 'md_importer',
1053 ]
1054
1055 MIDDLEWARE_CLASSES = (
1056@@ -107,24 +107,29 @@
1057 #CACHE_MIDDLEWARE_SECONDS = 3600
1058 #CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True
1059
1060-TEMPLATE_CONTEXT_PROCESSORS = (
1061- 'django.contrib.auth.context_processors.auth',
1062- 'django.core.context_processors.i18n',
1063- 'django.core.context_processors.request',
1064- 'django.core.context_processors.media',
1065- 'django.core.context_processors.static',
1066-
1067- 'sekizai.context_processors.sekizai',
1068- 'cms.context_processors.cms_settings',
1069- 'django.contrib.messages.context_processors.messages',
1070-)
1071-
1072-TEMPLATE_DIRS = (
1073- # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
1074- # Always use forward slashes, even on Windows.
1075- # Don't forget to use absolute paths, not relative paths.
1076- os.path.join(PROJECT_PATH, "templates"),
1077-)
1078+TEMPLATES = [
1079+ {
1080+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
1081+ 'DIRS': [
1082+ os.path.join(PROJECT_PATH, "templates"),
1083+ ],
1084+ 'APP_DIRS': True,
1085+ 'OPTIONS': {
1086+ 'context_processors': [
1087+ 'django.core.context_processors.request',
1088+ 'django.contrib.auth.context_processors.auth',
1089+ 'django.core.context_processors.i18n',
1090+ 'django.core.context_processors.media',
1091+ 'django.core.context_processors.static',
1092+
1093+ 'sekizai.context_processors.sekizai',
1094+ 'cms.context_processors.cms_settings',
1095+ 'django.contrib.messages.context_processors.messages',
1096+ ]
1097+ }
1098+ }
1099+]
1100+
1101
1102 ROOT_URLCONF = 'developer_portal.urls'
1103
1104@@ -318,6 +323,23 @@
1105 #'PAGINATE_BY': 10,
1106 }
1107
1108+MIGRATION_MODULES = {
1109+ 'cms': 'cms.migrations',
1110+ 'cmsplugin_zinnia': 'cmsplugin_zinnia.migrations',
1111+ 'djangocms_link': 'djangocms_link.migrations',
1112+ 'djangocms_picture': 'djangocms_picture.migrations',
1113+ 'djangocms_snippet': 'djangocms_snippet.migrations',
1114+ 'djangocms_text_ckeditor': 'djangocms_text_ckeditor.migrations',
1115+ 'djangocms_video': 'djangocms_video.migrations',
1116+ 'django_comments': 'django_comments.migrations',
1117+ 'menus': 'menus.migrations',
1118+ 'rest_framework.authtoken': 'rest_framework.authtoken.migrations',
1119+ 'reversion': 'reversion.migrations',
1120+ 'tagging': 'tagging.migrations',
1121+ 'taggit': 'taggit.migrations',
1122+ 'zinnia': 'zinnia.migrations',
1123+}
1124+
1125 LOGGING = {
1126 'version': 1,
1127 'disable_existing_loggers': False,
1128
1129=== modified file 'developer_portal/urls.py'
1130--- developer_portal/urls.py 2015-12-08 10:25:29 +0000
1131+++ developer_portal/urls.py 2016-01-19 00:21:38 +0000
1132@@ -32,7 +32,7 @@
1133
1134 urlpatterns += i18n_patterns('',
1135 url(r'^search/', 'developer_portal.views.search', name='search'),
1136- url(r'^ckeditor/', include('ckeditor.urls')),
1137+ url(r'^ckeditor/', include('ckeditor_uploader.urls')),
1138 url(r'^', include('cms.urls')),
1139 ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
1140
1141
1142=== modified file 'locale/developer_portal.pot'
1143--- locale/developer_portal.pot 2015-12-14 13:22:16 +0000
1144+++ locale/developer_portal.pot 2016-01-19 00:21:38 +0000
1145@@ -8,7 +8,7 @@
1146 msgstr ""
1147 "Project-Id-Version: PACKAGE VERSION\n"
1148 "Report-Msgid-Bugs-To: \n"
1149-"POT-Creation-Date: 2015-12-14 13:22+0000\n"
1150+"POT-Creation-Date: 2016-01-19 00:07+0000\n"
1151 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1152 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1153 "Language-Team: LANGUAGE <LL@li.org>\n"
1154@@ -21,44 +21,28 @@
1155 msgid "Raw HTML"
1156 msgstr ""
1157
1158-#: developer_portal/models.py:29
1159-msgid ""
1160-"External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-"
1161-"core/snappy.git"
1162-msgstr ""
1163-
1164-#: developer_portal/models.py:33
1165-msgid ""
1166-"Path alias we want to use for the docs, ie \"snappy/guides/15.04\" or "
1167-"\"snappy/guides/latest\", etc."
1168-msgstr ""
1169-
1170-#: developer_portal/models.py:38
1171-msgid "File name of doc to be used as index document, ie \"intro.md\""
1172-msgstr ""
1173-
1174-#: developer_portal/settings.py:189 developer_portal/settings.py:197
1175+#: developer_portal/settings.py:194 developer_portal/settings.py:202
1176 #: templates/translations_dashboard.html:12
1177 msgid "English"
1178 msgstr ""
1179
1180-#: developer_portal/settings.py:190 developer_portal/settings.py:204
1181+#: developer_portal/settings.py:195 developer_portal/settings.py:209
1182 #: templates/translations_dashboard.html:13
1183 msgid "Simplified Chinese"
1184 msgstr ""
1185
1186-#: developer_portal/settings.py:212
1187+#: developer_portal/settings.py:217
1188 msgid "Spanish"
1189 msgstr ""
1190
1191-#: developer_portal/settings.py:252
1192+#: developer_portal/settings.py:257
1193 msgid "Page content"
1194 msgstr ""
1195
1196 #. Translators: this is the default text that will be shown
1197 #. to editors when editing a page. You can use some HTML,
1198 #. but don't go wild :)
1199-#: developer_portal/settings.py:260
1200+#: developer_portal/settings.py:265
1201 msgid "<p>Add content here...</p>"
1202 msgstr ""
1203
1204@@ -68,6 +52,40 @@
1205 "profile</a> to use the developer site"
1206 msgstr ""
1207
1208+#: md_importer/models.py:10
1209+msgid ""
1210+"External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-"
1211+"core/snappy.git"
1212+msgstr ""
1213+
1214+#: md_importer/models.py:14
1215+msgid "For use with git branches, ie: \"master\" or \"15.04\" or \"1.x\"."
1216+msgstr ""
1217+
1218+#: md_importer/models.py:19
1219+msgid "Command to run after checkout of the branch."
1220+msgstr ""
1221+
1222+#: md_importer/models.py:37
1223+msgid ""
1224+"File or directory to import from the branch. Ie: \"docs/intro.md\" (file) or "
1225+"\"docs\" (complete directory), etc."
1226+msgstr ""
1227+
1228+#: md_importer/models.py:43
1229+msgid ""
1230+"Article URL (for a specific file) or article namespace for a directory or a "
1231+"set of files."
1232+msgstr ""
1233+
1234+#: md_importer/models.py:56
1235+msgid "Datetime"
1236+msgstr ""
1237+
1238+#: md_importer/models.py:56
1239+msgid "Datetime of last import."
1240+msgstr ""
1241+
1242 #: store_data/cms_plugins.py:11
1243 msgid "Snap list - Gadget"
1244 msgstr ""
1245@@ -101,9 +119,8 @@
1246
1247 #: templates/base.html:34 templates/base.html.py:51
1248 #: templates/menu/menu_footer_first_level.html:8
1249-#: templates/menu/menu_header_first_level.html:8
1250-#: templates/menu/sub_menu.html:8 templates/menu/sub_menu.html.py:22
1251-#: templates/menu/sub_menu.html:25
1252+#: templates/menu/menu_header_first_level.html:8 templates/menu/sub_menu.html:8
1253+#: templates/menu/sub_menu.html.py:22 templates/menu/sub_menu.html:25
1254 msgid "Overview"
1255 msgstr ""
1256
1257@@ -175,15 +192,23 @@
1258 msgid "Go to the top of the page"
1259 msgstr ""
1260
1261+#: templates/comments/zinnia/entry/form.html:25
1262+msgid "Comment as"
1263+msgstr ""
1264+
1265+#: templates/comments/zinnia/entry/form.html:25 templates/error_base.html:63
1266+#: templates/website_base.html:84
1267+msgid "Log out"
1268+msgstr ""
1269+
1270+#: templates/comments/zinnia/entry/form.html:28
1271+msgid "Post"
1272+msgstr ""
1273+
1274 #: templates/error_base.html:54 templates/website_base.html:74
1275 msgid "Jump to content"
1276 msgstr ""
1277
1278-#: templates/error_base.html:63 templates/website_base.html:84
1279-#: templates/comments/zinnia/entry/form.html:25
1280-msgid "Log out"
1281-msgstr ""
1282-
1283 #: templates/error_base.html:69 templates/website_base.html:90
1284 msgid "Log in"
1285 msgstr ""
1286@@ -204,6 +229,14 @@
1287 "href='https://bugs.launchpad.net/ubuntudeveloperportal'>report it"
1288 msgstr ""
1289
1290+#: templates/menu/menu_language_chooser.html:6
1291+msgid "View in:"
1292+msgstr ""
1293+
1294+#: templates/menu/menu_language_chooser.html:10
1295+msgid "Change to language:"
1296+msgstr ""
1297+
1298 #: templates/search_results.html:4 templates/search_results.html.py:11
1299 msgid "Search results for:"
1300 msgstr ""
1301@@ -224,8 +257,7 @@
1302 msgid "A simple dashboard to keep track of updates between languages."
1303 msgstr ""
1304
1305-#: templates/translations_dashboard.html:11
1306-#: templates/zinnia/entry_list.html:23
1307+#: templates/translations_dashboard.html:11 templates/zinnia/entry_list.html:23
1308 msgid "Page"
1309 msgstr ""
1310
1311@@ -277,22 +309,6 @@
1312 msgid "Back to top"
1313 msgstr ""
1314
1315-#: templates/comments/zinnia/entry/form.html:25
1316-msgid "Comment as"
1317-msgstr ""
1318-
1319-#: templates/comments/zinnia/entry/form.html:28
1320-msgid "Post"
1321-msgstr ""
1322-
1323-#: templates/menu/menu_language_chooser.html:6
1324-msgid "View in:"
1325-msgstr ""
1326-
1327-#: templates/menu/menu_language_chooser.html:10
1328-msgid "Change to language:"
1329-msgstr ""
1330-
1331 #: templates/zinnia/_entry_detail_base.html:33
1332 #, python-format
1333 msgid "%(percent)s%% of %(object)s still remains to read."
1334@@ -334,7 +350,7 @@
1335 msgid "Authors"
1336 msgstr ""
1337
1338-#: templates/zinnia/author_list.html:21 templates/zinnia/category_list.html:19
1339+#: templates/zinnia/author_list.html:21 templates/zinnia/category_list.html:20
1340 #: templates/zinnia/tag_list.html:20
1341 #, python-format
1342 msgid "%(entry_count)s entry"
1343@@ -354,21 +370,20 @@
1344 msgid "RSS Feed of latest discussions"
1345 msgstr ""
1346
1347-#: templates/zinnia/category_list.html:4
1348-#: templates/zinnia/category_list.html:12
1349+#: templates/zinnia/category_list.html:5 templates/zinnia/category_list.html:13
1350 msgid "Category list"
1351 msgstr ""
1352
1353-#: templates/zinnia/category_list.html:6
1354+#: templates/zinnia/category_list.html:7
1355 msgid "Categories"
1356 msgstr ""
1357
1358-#: templates/zinnia/category_list.html:18
1359+#: templates/zinnia/category_list.html:19
1360 #, python-format
1361 msgid "Show all entries in %(category)s"
1362 msgstr ""
1363
1364-#: templates/zinnia/category_list.html:24
1365+#: templates/zinnia/category_list.html:25
1366 msgid "No categories yet."
1367 msgstr ""
1368
1369
1370=== added directory 'md_importer'
1371=== added file 'md_importer/TODO'
1372--- md_importer/TODO 1970-01-01 00:00:00 +0000
1373+++ md_importer/TODO 2016-01-19 00:21:38 +0000
1374@@ -0,0 +1,7 @@
1375+- fix branch indicator (devel vs. stable, 1.x vs devel, etc.)
1376+- unpublished docs: some articles show up with a blue circle, so
1377+ unpublished although they're accessible (not a real issue - content is
1378+ always updated correctly)
1379+- add comment at top of RawHTML plugins, so users know not to edit them,
1380+ currently blocked on LP: #1523925 (RawHTML plugin strips comments)
1381+- investigate what the api importer does (upload to swift)
1382
1383=== added file 'md_importer/__init__.py'
1384=== added file 'md_importer/admin.py'
1385--- md_importer/admin.py 1970-01-01 00:00:00 +0000
1386+++ md_importer/admin.py 2016-01-19 00:21:38 +0000
1387@@ -0,0 +1,36 @@
1388+from django.contrib import admin
1389+
1390+from .models import (
1391+ ExternalDocsBranch, ExternalDocsBranchImportDirective,
1392+ ImportedArticle,
1393+)
1394+from django.core.management import call_command
1395+
1396+__all__ = (
1397+)
1398+
1399+
1400+def import_selected_external_docs_branches(modeladmin, request, queryset):
1401+ branches = []
1402+ for branch in queryset:
1403+ branches.append(branch.origin)
1404+ call_command('import_md', *branches)
1405+ import_selected_external_docs_branches.short_description = \
1406+ "Import selected branches"
1407+
1408+
1409+@admin.register(ExternalDocsBranch)
1410+class ExternalDocsBranchAdmin(admin.ModelAdmin):
1411+ list_display = ('origin', 'post_checkout_command', 'branch_name',)
1412+ list_filter = ('origin', 'post_checkout_command', 'branch_name',)
1413+ actions = [import_selected_external_docs_branches]
1414+
1415+
1416+@admin.register(ExternalDocsBranchImportDirective)
1417+class ExternalDocsBranchImportDirectiveAdmin(admin.ModelAdmin):
1418+ pass
1419+
1420+
1421+@admin.register(ImportedArticle)
1422+class ImportedArticleAdmin(admin.ModelAdmin):
1423+ pass
1424
1425=== added directory 'md_importer/importer'
1426=== added file 'md_importer/importer/__init__.py'
1427--- md_importer/importer/__init__.py 1970-01-01 00:00:00 +0000
1428+++ md_importer/importer/__init__.py 2016-01-19 00:21:38 +0000
1429@@ -0,0 +1,5 @@
1430+from developer_portal.settings import LANGUAGE_CODE
1431+
1432+DEFAULT_LANG = LANGUAGE_CODE
1433+HOME_PAGE_URL = '/{}/'.format(DEFAULT_LANG)
1434+SUPPORTED_ARTICLE_TYPES = ['.md', '.html']
1435
1436=== added file 'md_importer/importer/article.py'
1437--- md_importer/importer/article.py 1970-01-01 00:00:00 +0000
1438+++ md_importer/importer/article.py 2016-01-19 00:21:38 +0000
1439@@ -0,0 +1,163 @@
1440+from bs4 import BeautifulSoup
1441+import codecs
1442+import logging
1443+import markdown
1444+import os
1445+import re
1446+import sys
1447+
1448+from . import (
1449+ DEFAULT_LANG,
1450+ SUPPORTED_ARTICLE_TYPES,
1451+)
1452+from .publish import get_or_create_page, slugify
1453+
1454+if sys.version_info.major == 2:
1455+ from urlparse import urlparse
1456+else:
1457+ from urllib.parse import urlparse
1458+
1459+
1460+class Article:
1461+ def __init__(self, fn, write_to):
1462+ self.html = None
1463+ self.page = None
1464+ self.title = ""
1465+ self.fn = fn
1466+ self.write_to = slugify(self.fn)
1467+ self.full_url = write_to
1468+ self.slug = os.path.basename(self.full_url)
1469+
1470+ def _find_local_images(self):
1471+ '''Local images are currently not supported.'''
1472+ soup = BeautifulSoup(self.html, 'html5lib')
1473+ local_images = []
1474+ for img in soup.find_all('img'):
1475+ if img.has_attr('src'):
1476+ (scheme, netloc, path, params, query, fragment) = \
1477+ urlparse(img.attrs['src'])
1478+ if scheme not in ['http', 'https']:
1479+ local_images.extend([img.attrs['src']])
1480+ return local_images
1481+
1482+ def read(self):
1483+ if os.path.splitext(self.fn)[1] not in SUPPORTED_ARTICLE_TYPES:
1484+ logging.error("Don't know how to interpret '{}'.".format(
1485+ self.fn))
1486+ return False
1487+ with codecs.open(self.fn, 'r', encoding='utf-8') as f:
1488+ if self.fn.endswith('.md'):
1489+ self.html = markdown.markdown(
1490+ f.read(),
1491+ output_format='html5',
1492+ extensions=['pymdownx.github'])
1493+ elif self.fn.endswith('.html'):
1494+ self.html = f.read()
1495+ local_images = self._find_local_images()
1496+ if local_images:
1497+ logging.error('Found the following local image(s): {}'.format(
1498+ ', '.join(local_images)
1499+ ))
1500+ return False
1501+ self.title = self._read_title()
1502+ self._remove_body_and_html_tags()
1503+ self._use_developer_site_style()
1504+ return True
1505+
1506+ def _read_title(self):
1507+ soup = BeautifulSoup(self.html, 'html5lib')
1508+ if soup.title:
1509+ return soup.title.text
1510+ if soup.h1:
1511+ return soup.h1.text
1512+ return slugify(self.fn).replace('-', ' ').title()
1513+
1514+ def _remove_body_and_html_tags(self):
1515+ self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
1516+ flags=re.MULTILINE)
1517+ self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
1518+ flags=re.MULTILINE)
1519+
1520+ def _use_developer_site_style(self):
1521+ begin = (u"<div class=\"row no-border\">"
1522+ "\n<div class=\"eight-col\">\n")
1523+ end = u"</div>\n</div>"
1524+ self.html = begin + self.html + end
1525+ self.html = self.html.replace(
1526+ "<pre><code>",
1527+ "</div><div class=\"twelve-col\"><pre><code>")
1528+ self.html = self.html.replace(
1529+ "</code></pre>",
1530+ "</code></pre></div><div class=\"eight-col\">")
1531+
1532+ def replace_links(self, titles, url_map):
1533+ soup = BeautifulSoup(self.html, 'html5lib')
1534+ change = False
1535+ for link in soup.find_all('a'):
1536+ if not link.has_attr('class') or \
1537+ 'headeranchor-link' not in link.attrs['class']:
1538+ for title in titles:
1539+ if title.endswith(link.attrs['href']) and \
1540+ link.attrs['href'] != url_map[title].full_url:
1541+ link.attrs['href'] = url_map[title].full_url
1542+ change = True
1543+ if change:
1544+ self.html = soup.prettify()
1545+ return change
1546+
1547+ def add_to_db(self):
1548+ '''Publishes pages in their branch alias namespace.'''
1549+ self.page = get_or_create_page(
1550+ title=self.title, full_url=self.full_url, menu_title=self.title,
1551+ html=self.html)
1552+ if not self.page:
1553+ return False
1554+ self.full_url = self.page.get_absolute_url()
1555+ return True
1556+
1557+ def publish(self):
1558+ if self.page.is_dirty(DEFAULT_LANG):
1559+ self.page.publish(DEFAULT_LANG)
1560+ self.page = self.page.get_public_object()
1561+ return self.page
1562+
1563+
1564+class SnappyArticle(Article):
1565+ release_alias = None
1566+
1567+ def read(self):
1568+ if not Article.read(self):
1569+ return False
1570+ self.release_alias = re.findall(r'snappy/guides/(\S+?)/\S+?',
1571+ self.full_url)[0]
1572+ self._make_snappy_mods()
1573+ return True
1574+
1575+ def _make_snappy_mods(self):
1576+ # Make sure the reader knows which documentation she is browsing
1577+ if self.release_alias != 'current':
1578+ before = (u"<div class=\"row no-border\">\n"
1579+ "<div class=\"eight-col\">\n")
1580+ after = (u"<div class=\"row no-border\">\n"
1581+ "<div class=\"box pull-three three-col\">"
1582+ "<p>You are browsing the Snappy <code>%s</code> "
1583+ "documentation.</p>"
1584+ "<p><a href=\"/snappy/guides/current/%s\">"
1585+ "Back to the latest stable release &rsaquo;"
1586+ "</a></p></div>\n"
1587+ "<div class=\"eight-col\">\n") % (self.release_alias,
1588+ self.slug, )
1589+ self.html = self.html.replace(before, after)
1590+
1591+ def add_to_db(self):
1592+ if self.release_alias == "current":
1593+ # Add a guides/<page> redirect to guides/current/<page>
1594+ page = get_or_create_page(
1595+ title=self.title,
1596+ full_url=self.full_url.replace('/current', ''),
1597+ redirect="/snappy/guides/current/{}".format(self.slug))
1598+ if not page:
1599+ return False
1600+ else:
1601+ self.title += " (%s)" % (self.release_alias,)
1602+ return Article.add_to_db(self)
1603
1604=== added file 'md_importer/importer/process.py'
1605--- md_importer/importer/process.py 1970-01-01 00:00:00 +0000
1606+++ md_importer/importer/process.py 2016-01-19 00:21:38 +0000
1607@@ -0,0 +1,60 @@
1608+from django.core.management import call_command
1609+
1610+import datetime
1611+import pytz
1612+import shutil
1613+import tempfile
1614+
1615+from md_importer.importer.repo import create_repo
1616+
1617+from md_importer.models import (
1618+ ExternalDocsBranchImportDirective,
1619+ ImportedArticle,
1620+)
1621+
1622+
1623+def process_branch(branch):
1624+ tempdir = tempfile.mkdtemp()
1625+ repo = create_repo(tempdir, branch.origin, branch.branch_name,
1626+ branch.post_checkout_command)
1627+ if repo.get() != 0:
1628+ return False
1629+ for directive in ExternalDocsBranchImportDirective.objects.filter(
1630+ external_docs_branch=branch):
1631+ repo.add_directive(directive.import_from,
1632+ directive.write_to)
1633+ if not repo.execute_import_directives():
1634+ return False
1635+ if not repo.publish():
1636+ return False
1637+ timestamp = datetime.datetime.now(pytz.utc)
1638+
1639+ # Update data in ImportedArticle table
1640+ for page in repo.pages:
1641+ if ImportedArticle.objects.filter(branch=branch, page=page).count():
1642+ imported_article = ImportedArticle.objects.filter(
1643+ branch=branch, page=page)[0]
1644+ imported_article.last_import = datetime.datetime.now(pytz.utc)
1645+ imported_article.save()
1646+ else:
1647+ ImportedArticle.objects.get_or_create(
1648+ branch=branch,
1649+ page=page,
1650+ last_import=datetime.datetime.now(pytz.utc))
1651+
1652+ # Remove old entries
1653+ for imported_article in ImportedArticle.objects.filter(
1654+ branch=branch, last_import__lt=timestamp):
1655+ imported_article.page.delete()
1656+ imported_article.delete()
1657+
1658+ # The import is done, now let's clean up.
1659+ imported_page_ids = [p.id for p in repo.pages
1660+ if p.changed_by in ['python-api', 'script']]
1661+ ImportedArticle.objects.filter(
1662+ branch=branch).filter(id__in=imported_page_ids).delete()
1663+ shutil.rmtree(tempdir)
1664+
1665+ # https://stackoverflow.com/questions/33284171/
1666+ call_command('cms', 'fix-tree')
1667+ return True
1668
1669=== added file 'md_importer/importer/publish.py'
1670--- md_importer/importer/publish.py 1970-01-01 00:00:00 +0000
1671+++ md_importer/importer/publish.py 2016-01-19 00:21:38 +0000
1672@@ -0,0 +1,75 @@
1673+from md_importer.importer import DEFAULT_LANG, HOME_PAGE_URL
1674+
1675+from cms.api import create_page, add_plugin
1676+from cms.models import Title
1677+from djangocms_text_ckeditor.html import clean_html
1678+
1679+import logging
1680+import re
1681+import os
1682+
1683+
1684+def slugify(filename):
1685+ return os.path.basename(filename).replace('.md', '').replace('.html', '')
1686+
1687+
1688+def _find_parent(full_url):
1689+ if full_url == HOME_PAGE_URL:
1690+ # If we set up the homepage, we don't need a parent.
1691+ return None
1692+ parent_url = re.sub(
1693+ r'^\/{}\/'.format(DEFAULT_LANG),
1694+ '',
1695+ os.path.dirname(full_url))
1696+
1697+ parent_pages = Title.objects.select_related('page').filter(
1698+ path__regex=parent_url, language=DEFAULT_LANG).filter(
1699+ publisher_is_draft=True)
1700+ if not parent_pages:
1701+ logging.error('Parent {} not found.'.format(
1702+ parent_url))
1703+ return None
1704+ return parent_pages[0].page
1705+
1706+
1707+def get_or_create_page(title, full_url, menu_title=None,
1708+ in_navigation=True, redirect=None, html=None):
1709+ # First check if pages already exist.
1710+ pages = Title.objects.select_related('page').filter(
1711+ path__regex=full_url).filter(publisher_is_draft=True)
1712+ if pages:
1713+ page = pages[0].page
1714+ if page.get_title() != title:
1715+ page.title = title
1716+ if page.get_menu_title() != menu_title:
1717+ page.menu_title = menu_title
1718+ if page.in_navigation != in_navigation:
1719+ page.in_navigation = in_navigation
1720+ if page.get_redirect() != redirect:
1721+ page.redirect = redirect
1722+ if html:
1723+ # We create the page, so we know there's just one placeholder
1724+ placeholder = page.placeholders.all()[0]
1725+ if placeholder.get_plugins():
1726+ plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
1727+ if plugin.body != clean_html(html, full=False):
1728+ plugin.body = html
1729+ plugin.save()
1730+ else:
1731+ add_plugin(
1732+ placeholder, 'RawHtmlPlugin',
1733+ DEFAULT_LANG, body=html)
1734+ else:
1735+ parent = _find_parent(full_url)
1736+ if not parent:
1737+ return None
1738+ slug = os.path.basename(full_url)
1739+ page = create_page(
1740+ title, 'default.html', DEFAULT_LANG, slug=slug, parent=parent,
1741+ menu_title=menu_title, in_navigation=in_navigation,
1742+ position='last-child', redirect=redirect)
1743+ placeholder = page.placeholders.get()
1744+ add_plugin(placeholder, 'RawHtmlPlugin', DEFAULT_LANG, body=html)
1745+ placeholder = page.placeholders.all()[0]
1746+ plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
1747+ return page
1748
1749=== added file 'md_importer/importer/repo.py'
1750--- md_importer/importer/repo.py 1970-01-01 00:00:00 +0000
1751+++ md_importer/importer/repo.py 2016-01-19 00:21:38 +0000
1752@@ -0,0 +1,174 @@
1753+from . import (
1754+ DEFAULT_LANG,
1755+ SUPPORTED_ARTICLE_TYPES,
1756+)
1757+from .article import Article, SnappyArticle
1758+from .publish import get_or_create_page, slugify
1759+from .source import SourceCode
1760+
1761+import glob
1762+import logging
1763+import os
1764+
1765+
1766+def create_repo(tempdir, origin, branch_name, post_checkout_command):
1767+ if os.path.exists(origin):
1768+ if 'snappy' in origin:
1769+ repo_class = SnappyRepo
1770+ else:
1771+ repo_class = Repo
1772+ else:
1773+ if origin.startswith('lp:snappy') or \
1774+ 'snappy' in origin.split(':')[1].split('.git')[0].split('/'):
1775+ repo_class = SnappyRepo
1776+ else:
1777+ repo_class = Repo
1778+ return repo_class(tempdir, origin, branch_name, post_checkout_command)
1779+
1780+
1781+class Repo:
1782+ def __init__(self, tempdir, origin, branch_name, post_checkout_command):
1783+ self.directives = []
1784+ self.imported_articles = []
1785+ self.url_map = {}
1786+ self.titles = {}
1787+ self.index_doc_url = None
1788+ self.index_page = None
1789+ self.release_alias = None
1790+ # On top of the pages in imported_articles this also
1791+ # includes index_page
1792+ self.pages = []
1793+ self.origin = origin
1794+ self.branch_name = branch_name
1795+ self.post_checkout_command = post_checkout_command
1796+ branch_nick = os.path.basename(self.origin.replace('.git', ''))
1797+ self.checkout_location = os.path.join(
1798+ tempdir, branch_nick)
1799+ self.index_doc_title = branch_nick
1800+ self.article_class = Article
1801+
1802+ def get(self):
1803+ sourcecode = SourceCode(self.origin, self.checkout_location,
1804+ self.branch_name, self.post_checkout_command)
1805+ if sourcecode.get() != 0:
1806+ logging.error(
1807+ 'Could not check out branch "{}".'.format(self.origin))
1808+ return 1
1809+ return 0
1810+
1811+ def add_directive(self, import_from, write_to):
1812+ self.directives += [
1813+ {
1814+ 'import_from': os.path.join(self.checkout_location,
1815+ import_from),
1816+ 'write_to': write_to
1817+ }
1818+ ]
1819+
1820+ def execute_import_directives(self):
1821+ import_list = []
1822+ # Import single files first
1823+ for directive in [d for d in self.directives
1824+ if os.path.isfile(d['import_from'])]:
1825+ import_list += [
1826+ (directive['import_from'], directive['write_to'])
1827+ ]
1828+ # Import directories next
1829+ for directive in [d for d in self.directives
1830+ if os.path.isdir(d['import_from'])]:
1831+ for fn in glob.glob('{}/*'.format(directive['import_from'])):
1832+ if fn not in [a[0] for a in import_list]:
1833+ import_list += [
1834+ (fn, os.path.join(directive['write_to'], slugify(fn)))
1835+ ]
1836+ # If we import into a namespace and don't have an index doc,
1837+ # we need to write one.
1838+ if directive['write_to'] not in [x[1] for x in import_list]:
1839+ self.index_doc_url = directive['write_to']
1840+ if self.index_doc_url:
1841+ if not self._create_fake_index_page():
1842+ logging.error('Importing of {} aborted.'.format(self.origin))
1843+ return False
1844+ # The actual import
1845+ for entry in import_list:
1846+ article = self._read_article(entry[0], entry[1])
1847+ if article:
1848+ self.imported_articles += [article]
1849+ self.titles[article.fn] = article.title
1850+ self.url_map[article.fn] = article
1851+ elif os.path.splitext(entry[0])[1] in SUPPORTED_ARTICLE_TYPES:
1852+ # In this case the article was supported but still reading
1853+ # it failed, importing should be stopped here to avoid
1854+ # problems.
1855+ logging.error('Importing of {} aborted.'.format(self.origin))
1856+ return False
1857+ if self.index_doc_url:
1858+ self._write_fake_index_doc()
1859+ return True
1860+
1861+ def _read_article(self, fn, write_to):
1862+ article = self.article_class(fn, write_to)
1863+ if article.read():
1864+ return article
1865+ return None
1866+
1867+ def publish(self):
1868+ for article in self.imported_articles:
1869+ if not article.add_to_db():
1870+ logging.error('Publishing of {} aborted.'.format(self.origin))
1871+ return False
1872+ article.replace_links(self.titles, self.url_map)
1873+ self.pages = []
1874+ for article in self.imported_articles:
1875+ self.pages.extend([article.publish()])
1876+ if self.index_page:
1877+ self.index_page.publish(DEFAULT_LANG)
1878+ self.pages.extend([self.index_page])
1879+ return True
1880+
1881+ def _create_fake_index_page(self):
1882+ '''Creates a fake index page at the top of the branches
1883+ docs namespace.'''
1884+
1885+ if self.index_doc_url.endswith('current'):
1886+ redirect = '/snappy/guides'
1887+ else:
1888+ redirect = None
1889+ self.index_page = get_or_create_page(
1890+ title=self.index_doc_title, full_url=self.index_doc_url,
1891+ in_navigation=False, redirect=redirect, html='',
1892+ menu_title=None)
1893+ if not self.index_page:
1894+ return False
1895+ return True
1896+
1897+ def _write_fake_index_doc(self):
1898+ list_pages = ''
1899+ for article in [a for a
1900+ in self.imported_articles
1901+ if a.full_url.startswith(self.index_doc_url)]:
1902+ list_pages += '<li><a href=\"{}\">{}</a></li>'.format(
1903+ os.path.basename(article.full_url), article.title)
1904+ self.index_page.html = (
1905+ u'<div class=\"row\"><div class=\"eight-col\">\n'
1906+ '<p>This section contains documentation for the '
1907+ '<code>{}</code> Snappy branch.</p>'
1908+ '<p><ul class=\"list-ubuntu\">{}</ul></p>\n'
1909+ '<p>Auto-imported from <a '
1910+ 'href=\"{}\">{}</a>.</p>\n'
1911+ '</div></div>'.format(self.release_alias, list_pages,
1912+ self.origin, self.origin))
1913+
1914+
1915+class SnappyRepo(Repo):
1916+ def __init__(self, tempdir, origin, branch_name, post_checkout_command):
1917+ Repo.__init__(self, tempdir, origin, branch_name,
1918+ post_checkout_command)
1919+ self.article_class = SnappyArticle
1920+ self.index_doc_title = 'Snappy documentation'
1921+
1922+ def _create_fake_index_page(self):
1923+ self.release_alias = os.path.basename(self.index_doc_url)
1924+ if not self.index_doc_url.endswith('current'):
1925+ self.index_doc_title += ' ({})'.format(self.release_alias)
1926+ return Repo._create_fake_index_page(self)
1927
1928=== added file 'md_importer/importer/source.py'
1929--- md_importer/importer/source.py 1970-01-01 00:00:00 +0000
1930+++ md_importer/importer/source.py 2016-01-19 00:21:38 +0000
1931@@ -0,0 +1,60 @@
1932+import logging
1933+import os
1934+import shutil
1935+import subprocess
1936+
1937+
1938+class SourceCode():
1939+ def __init__(self, origin, checkout_location, branch_name,
1940+ post_checkout_command):
1941+ self.origin = origin
1942+ self.checkout_location = checkout_location
1943+ self.branch_name = branch_name
1944+ self.post_checkout_command = post_checkout_command
1945+
1946+ def get(self):
1947+ res = self._get_repo()
1948+ if res == 0 and self.post_checkout_command:
1949+ res = self._post_checkout()
1950+ return res
1951+ return res
1952+
1953+ def _get_repo(self):
1954+ if os.path.exists(self.origin):
1955+ shutil.copytree(
1956+ self.origin,
1957+ self.checkout_location)
1958+ return 0
1959+ if self.origin.startswith('lp:') and \
1960+ os.path.exists('/usr/bin/bzr'):
1961+ return subprocess.call([
1962+ 'bzr', 'checkout', '--lightweight', self.origin,
1963+ self.checkout_location])
1964+ if self.origin.startswith('https://github.com') and \
1965+ self.origin.endswith('.git') and \
1966+ os.path.exists('/usr/bin/git'):
1967+ retcode = subprocess.call([
1968+ 'git', 'clone', '--quiet', self.origin,
1969+ self.checkout_location])
1970+ if retcode == 0 and self.branch_name:
1971+ pwd = os.getcwd()
1972+ os.chdir(self.checkout_location)
1973+ retcode = subprocess.call(['git', 'checkout', '--quiet',
1974+ self.branch_name])
1975+ os.chdir(pwd)
1976+ return retcode
1977+ logging.error(
1978+ 'Repo format "{}" not understood.'.format(self.origin))
1979+ return 1
1980+
1981+ def _post_checkout(self):
1982+ pwd = os.getcwd()
1983+ os.chdir(self.checkout_location)
1984+ process = subprocess.Popen(self.post_checkout_command.split(),
1985+ stdout=subprocess.PIPE)
1986+ (out, err) = process.communicate()
1987+ retcode = process.wait()
1988+ os.chdir(pwd)
1989+ if retcode != 0:
1990+ logging.error(out)
1991+ return retcode
1992
1993=== added directory 'md_importer/management'
1994=== added file 'md_importer/management/__init__.py'
1995=== added directory 'md_importer/management/commands'
1996=== added file 'md_importer/management/commands/__init__.py'
1997=== renamed file 'developer_portal/management/commands/import-external-docs-branches.py' => 'md_importer/management/commands/import_md.py'
1998--- developer_portal/management/commands/import-external-docs-branches.py 2015-12-08 10:25:29 +0000
1999+++ md_importer/management/commands/import_md.py 2016-01-19 00:21:38 +0000
2000@@ -1,300 +1,9 @@
2001+import logging
2002+
2003 from django.core.management.base import BaseCommand
2004-from django.core.management import call_command
2005-from django.db import transaction
2006-
2007-from cms.api import create_page, add_plugin
2008-from cms.models import Page, Title
2009-from cms.utils import page_resolver
2010-
2011-from bs4 import BeautifulSoup
2012-import codecs
2013-import glob
2014-import logging
2015-import markdown
2016-import os
2017-import re
2018-import shutil
2019-import subprocess
2020-import sys
2021-import tempfile
2022-
2023-from developer_portal.models import ExternalDocsBranch
2024-
2025-DOCS_DIRNAME = 'docs'
2026-
2027-
2028-class DBActions:
2029- added_pages = []
2030- removed_pages = []
2031-
2032- def add_page(self, **kwargs):
2033- self.added_pages += [kwargs]
2034-
2035- def remove_page(self, page_id):
2036- self.removed_pages += [page_id]
2037-
2038- @transaction.commit_on_success()
2039- def run(self):
2040- for added_page in self.added_pages:
2041- page = get_or_create_page(**added_page)
2042- page.publish('en')
2043-
2044- # Only remove pages created by a script!
2045- Page.objects.filter(id__in=self.removed_pages,
2046- created_by="script").delete()
2047-
2048- # https://stackoverflow.com/questions/33284171/
2049- call_command('cms', 'fix-mptt')
2050-
2051-
2052-class MarkdownFile:
2053- html = None
2054-
2055- def __init__(self, fn, docs_namespace, db_actions, slug_override=None):
2056- self.fn = fn
2057- self.docs_namespace = docs_namespace
2058- self.db_actions = db_actions
2059- if slug_override:
2060- self.slug = slug_override
2061- else:
2062- self.slug = slugify(self.fn)
2063- self.full_url = os.path.join(self.docs_namespace, self.slug)
2064- with codecs.open(self.fn, 'r', encoding='utf-8') as f:
2065- self.html = markdown.markdown(
2066- f.read(),
2067- output_format="html5",
2068- extensions=['markdown.extensions.tables'])
2069- self.release_alias = self._get_release_alias()
2070- self.title = self._read_title()
2071- self._remove_body_and_html_tags()
2072- self._use_developer_site_style()
2073-
2074- def _get_release_alias(self):
2075- alias = re.findall(r'/tmp/tmp\S+?/(\S+?)/%s/\S+?' % DOCS_DIRNAME,
2076- self.fn)
2077- return alias[0]
2078-
2079- def _read_title(self):
2080- soup = BeautifulSoup(self.html, 'html5lib')
2081- if soup.title:
2082- return soup.title.text
2083- if soup.h1:
2084- return soup.h1.text
2085- return slugify(self.fn).replace('-', ' ').title()
2086-
2087- def _remove_body_and_html_tags(self):
2088- self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
2089- flags=re.MULTILINE)
2090- self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
2091- flags=re.MULTILINE)
2092-
2093- def _use_developer_site_style(self):
2094- begin = (u"<div class=\"row no-border\">"
2095- "\n<div class=\"eight-col\">\n")
2096- end = u"</div>\n</div>"
2097- self.html = begin + self.html + end
2098- self.html = self.html.replace(
2099- "<pre><code>",
2100- "</div><div class=\"twelve-col\"><pre><code>")
2101- self.html = self.html.replace(
2102- "</code></pre>",
2103- "</code></pre></div><div class=\"eight-col\">")
2104-
2105- def replace_links(self, titles, url_map):
2106- for title in titles:
2107- local_md_fn = os.path.basename(title)
2108- url = u'/'+url_map[title]
2109- # Replace links of the form <a href="/path/somefile.md"> first
2110- href = u"<a href=\"{}\">".format(url)
2111- md_href = u"<a href=\"{}\">".format(local_md_fn)
2112- self.html = self.html.replace(md_href, href)
2113-
2114- # Now we can replace free-standing "somefile.md" references in
2115- # the HTML
2116- link = href + u"{}</a>".format(titles[title])
2117- self.html = self.html.replace(local_md_fn, link)
2118-
2119- def publish(self):
2120- '''Publishes pages in their branch alias namespace.'''
2121- self.db_actions.add_page(
2122- title=self.title, full_url=self.full_url, menu_title=self.title,
2123- html=self.html)
2124-
2125-
2126-class SnappyMarkdownFile(MarkdownFile):
2127- def __init__(self, fn, docs_namespace, db_actions):
2128- MarkdownFile.__init__(self, fn, docs_namespace, db_actions)
2129- self._make_snappy_mods()
2130-
2131- def _make_snappy_mods(self):
2132- # Make sure the reader knows which documentation she is browsing
2133- if self.release_alias != 'current':
2134- before = (u"<div class=\"row no-border\">\n"
2135- "<div class=\"eight-col\">\n")
2136- after = (u"<div class=\"row no-border\">\n"
2137- "<div class=\"box pull-three three-col\">"
2138- "<p>You are browsing the Snappy <code>%s</code> "
2139- "documentation.</p>"
2140- "<p><a href=\"/snappy/guides/current/%s\">"
2141- "Back to the latest stable release &rsaquo;"
2142- "</a></p></div>\n"
2143- "<div class=\"eight-col\">\n") % (self.release_alias,
2144- self.slug, )
2145- self.html = self.html.replace(before, after)
2146-
2147- def publish(self):
2148- if self.release_alias == "current":
2149- # Add a guides/<page> redirect to guides/current/<page>
2150- self.db_actions.add_page(
2151- title=self.title,
2152- full_url=self.full_url.replace('/current', ''),
2153- redirect="/snappy/guides/current/%s" % (self.slug))
2154- else:
2155- self.title += " (%s)" % (self.release_alias,)
2156- MarkdownFile.publish(self)
2157-
2158-
2159-def slugify(filename):
2160- return os.path.basename(filename).replace('.md', '')
2161-
2162-
2163-class LocalBranch:
2164- titles = {}
2165- url_map = {}
2166-
2167- def __init__(self, dirname, external_branch, db_actions):
2168- self.dirname = dirname
2169- self.docs_path = os.path.join(self.dirname, DOCS_DIRNAME)
2170- self.doc_fns = glob.glob(self.docs_path+'/*.md')
2171- self.md_files = []
2172- self.external_branch = external_branch
2173- self.docs_namespace = self.external_branch.docs_namespace
2174- self.release_alias = os.path.basename(self.docs_namespace)
2175- self.index_doc_title = self.release_alias.title()
2176- self.index_doc = self.external_branch.index_doc
2177- self.db_actions = db_actions
2178- self.markdown_class = MarkdownFile
2179-
2180- def import_markdown(self):
2181- for doc_fn in self.doc_fns:
2182- if self.index_doc and os.path.basename(doc_fn) == self.index_doc:
2183- md_file = self.markdown_class(
2184- doc_fn,
2185- os.path.dirname(self.docs_namespace),
2186- self.db_actions,
2187- slug_override=os.path.basename(self.docs_namespace))
2188- self.md_files.insert(0, md_file)
2189- else:
2190- md_file = self.markdown_class(doc_fn, self.docs_namespace,
2191- self.db_actions)
2192- self.md_files += [md_file]
2193- self.titles[md_file.fn] = md_file.title
2194- self.url_map[md_file.fn] = md_file.full_url
2195- if not self.index_doc:
2196- self._create_fake_index_doc()
2197- for md_file in self.md_files:
2198- md_file.replace_links(self.titles, self.url_map)
2199-
2200- def remove_old_pages(self):
2201- imported_page_urls = set([md_file.full_url
2202- for md_file in self.md_files])
2203- index_doc = page_resolver.get_page_queryset_from_path(
2204- self.docs_namespace)
2205- db_pages = []
2206- if len(index_doc):
2207- # All pages in this namespace currently in the database
2208- db_pages = index_doc[0].get_descendants().all()
2209- for db_page in db_pages:
2210- still_relevant = False
2211- for url in imported_page_urls:
2212- if url in db_page.get_absolute_url():
2213- still_relevant = True
2214- break
2215- # At this point we know that there's no match and the page
2216- # can be deleted.
2217- if not still_relevant:
2218- self.db_actions.remove_page(db_page.id)
2219-
2220- def publish(self):
2221- for md_file in self.md_files:
2222- md_file.publish()
2223-
2224- def _create_fake_index_doc(self):
2225- '''Creates a fake index page at the top of the branches
2226- docs namespace.'''
2227-
2228- if self.docs_namespace == "current":
2229- redirect = "/snappy/guides"
2230- else:
2231- redirect = None
2232-
2233- in_navigation = False
2234- menu_title = None
2235- list_pages = ""
2236- for page in self.md_files:
2237- list_pages += "<li><a href=\"%s\">%s</a></li>" \
2238- % (os.path.basename(page.full_url), page.title)
2239- landing = (
2240- u"<div class=\"row\"><div class=\"eight-col\">\n"
2241- "<p>This section contains documentation for the "
2242- "<code>%s</code> Snappy branch.</p>"
2243- "<p><ul class=\"list-ubuntu\">%s</ul></p>\n"
2244- "<p>Auto-imported from <a "
2245- "href=\"https://github.com/ubuntu-core/snappy\">%s</a>.</p>\n"
2246- "</div></div>") % (self.release_alias, list_pages,
2247- self.external_branch.lp_origin)
2248- self.db_actions.add_page(
2249- title=self.index_doc_title, full_url=self.docs_namespace,
2250- in_navigation=in_navigation, redirect=redirect, html=landing,
2251- menu_title=menu_title)
2252-
2253-
2254-class SnappyLocalBranch(LocalBranch):
2255- def __init__(self, dirname, external_branch, db_actions):
2256- LocalBranch.__init__(self, dirname, external_branch, db_actions)
2257- self.markdown_class = SnappyMarkdownFile
2258- self.index_doc_title = 'Snappy documentation'
2259- if self.release_alias != 'current':
2260- self.index_doc_title += ' (%s)' % self.release_alias
2261-
2262-
2263-def get_or_create_page(title, full_url, menu_title=None,
2264- in_navigation=True, redirect=None, html=None):
2265- # First check if pages already exist.
2266- pages = Title.objects.select_related('page').filter(path__regex=full_url)
2267- if pages:
2268- page = pages[0].page
2269- page.title = title
2270- page.publisher_is_draft = True
2271- page.menu_title = menu_title
2272- page.in_navigation = in_navigation
2273- page.redirect = redirect
2274- if html:
2275- # We create the page, so we know there's just one placeholder
2276- placeholder = page.placeholders.all()[0]
2277- if placeholder.get_plugins():
2278- plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
2279- plugin.body = html
2280- plugin.save()
2281- else:
2282- add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
2283- else:
2284- parent_pages = Title.objects.select_related('page').filter(
2285- path__regex=os.path.dirname(full_url))
2286- if not parent_pages:
2287- print('Parent %s not found.' % os.path.dirname(full_url))
2288- sys.exit(1)
2289- parent = parent_pages[0].page
2290-
2291- slug = os.path.basename(full_url)
2292- page = create_page(
2293- title, "default.html", "en", slug=slug, parent=parent,
2294- menu_title=menu_title, in_navigation=in_navigation,
2295- position="last-child", redirect=redirect)
2296- if html:
2297- placeholder = page.placeholders.get()
2298- add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
2299- return page
2300+
2301+from md_importer.importer.process import process_branch
2302+from md_importer.models import ExternalDocsBranch
2303
2304
2305 def import_branches(selection):
2306@@ -302,64 +11,26 @@
2307 logging.error('No branches registered in the '
2308 'ExternalDocsBranch table yet.')
2309 return
2310- tempdir = tempfile.mkdtemp()
2311- db_actions = DBActions()
2312 for branch in ExternalDocsBranch.objects.filter(
2313- docs_namespace__regex=selection):
2314- checkout_location = os.path.join(
2315- tempdir, os.path.basename(branch.docs_namespace))
2316- sourcecode = SourceCode(branch.lp_origin, checkout_location)
2317- if sourcecode.get() != 0:
2318- logging.error(
2319- 'Could not check out branch "%s".' % branch.lp_origin)
2320- if os.path.exists(checkout_location):
2321- shutil.rmtree(checkout_location)
2322+ origin__regex=selection, active=True):
2323+ if not process_branch(branch):
2324 break
2325- if branch.lp_origin.startswith('lp:snappy') or \
2326- 'snappy' in branch.lp_origin.split(':')[1].split('.git')[0].split('/'):
2327- local_branch = SnappyLocalBranch(checkout_location, branch,
2328- db_actions)
2329- else:
2330- local_branch = LocalBranch(checkout_location, branch, db_actions)
2331- local_branch.import_markdown()
2332- local_branch.publish()
2333- local_branch.remove_old_pages()
2334- shutil.rmtree(tempdir)
2335- db_actions.run()
2336-
2337-
2338-class SourceCode():
2339- def __init__(self, branch_origin, checkout_location):
2340- self.branch_origin = branch_origin
2341- self.checkout_location = checkout_location
2342-
2343- def get(self):
2344- if self.branch_origin.startswith('lp:') and \
2345- os.path.exists('/usr/bin/bzr'):
2346- return subprocess.call([
2347- 'bzr', 'checkout', '--lightweight', self.branch_origin,
2348- self.checkout_location])
2349- if self.branch_origin.startswith('https://github.com') and \
2350- self.branch_origin.endswith('.git') and \
2351- os.path.exists('/usr/bin/git'):
2352- return subprocess.call([
2353- 'git', 'clone', '-q', self.branch_origin,
2354- self.checkout_location])
2355- logging.error(
2356- 'Branch format "{}" not understood.'.format(self.branch_origin))
2357- return 1
2358
2359
2360 class Command(BaseCommand):
2361 help = "Import external branches for documentation."
2362
2363+ def add_arguments(self, parser):
2364+ parser.add_argument('branches', nargs='*')
2365+
2366 def handle(*args, **options):
2367 logging.basicConfig(
2368 level=logging.ERROR,
2369 format='%(asctime)s %(levelname)-8s %(message)s',
2370 datefmt='%F %T')
2371- if len(args) < 2 or args[1] == "all":
2372- selection = '.*'
2373+ branches = options['branches']
2374+ if not branches:
2375+ import_branches('.*')
2376 else:
2377- selection = args[1]
2378- import_branches(selection)
2379+ for b in branches:
2380+ import_branches(b)
2381
2382=== added directory 'md_importer/migrations'
2383=== added file 'md_importer/migrations/0001_initial.py'
2384--- md_importer/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
2385+++ md_importer/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
2386@@ -0,0 +1,46 @@
2387+# -*- coding: utf-8 -*-
2388+from __future__ import unicode_literals
2389+
2390+from django.db import migrations, models
2391+
2392+
2393+class Migration(migrations.Migration):
2394+
2395+ dependencies = [
2396+ ('cms', '0013_urlconfrevision'),
2397+ ]
2398+
2399+ operations = [
2400+ migrations.CreateModel(
2401+ name='ExternalDocsBranch',
2402+ fields=[
2403+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
2404+ ('origin', models.CharField(help_text='External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-core/snappy.git', max_length=200)),
2405+ ('branch_name', models.CharField(help_text='For use with git branches, ie: "master" or "15.04" or "1.x".', max_length=200, blank=True)),
2406+ ('post_checkout_command', models.CharField(help_text='Command to run after checkout of the branch.', max_length=100, blank=True)),
2407+ ('active', models.BooleanField(default=True)),
2408+ ],
2409+ options={
2410+ 'verbose_name': 'external docs branch',
2411+ 'verbose_name_plural': 'external docs branches',
2412+ },
2413+ ),
2414+ migrations.CreateModel(
2415+ name='ExternalDocsBranchImportDirective',
2416+ fields=[
2417+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
2418+ ('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, blank=True)),
2419+ ('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, blank=True)),
2420+ ('external_docs_branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
2421+ ],
2422+ ),
2423+ migrations.CreateModel(
2424+ name='ImportedArticle',
2425+ fields=[
2426+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
2427+ ('last_import', models.DateTimeField(help_text='Datetime of last import.', verbose_name='Datetime')),
2428+ ('branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
2429+ ('page', models.ForeignKey(to='cms.Page')),
2430+ ],
2431+ ),
2432+ ]
2433
2434=== added file 'md_importer/migrations/__init__.py'
2435=== added file 'md_importer/models.py'
2436--- md_importer/models.py 1970-01-01 00:00:00 +0000
2437+++ md_importer/models.py 2016-01-19 00:21:38 +0000
2438@@ -0,0 +1,56 @@
2439+from django.db import models
2440+from django.utils.translation import ugettext_lazy as _
2441+
2442+from cms.models import Page
2443+
2444+
2445+class ExternalDocsBranch(models.Model):
2446+ origin = models.CharField(
2447+ max_length=200,
2448+ help_text=_('External branch location, ie: lp:snappy/15.04 or '
2449+ 'https://github.com/ubuntu-core/snappy.git'))
2450+ branch_name = models.CharField(
2451+ max_length=200,
2452+ help_text=_('For use with git branches, ie: "master" or "15.04" '
2453+ 'or "1.x".'),
2454+ blank=True)
2455+ post_checkout_command = models.CharField(
2456+ max_length=100,
2457+ help_text=_('Command to run after checkout of the branch.'),
2458+ blank=True)
2459+ active = models.BooleanField(default=True)
2460+
2461+ def __str__(self):
2462+ if self.branch_name:
2463+ return "{} - {}".format(self.origin, self.branch_name)
2464+ return "{}".format(self.origin)
2465+
2466+ class Meta:
2467+ verbose_name = "external docs branch"
2468+ verbose_name_plural = "external docs branches"
2469+
2470+
2471+class ExternalDocsBranchImportDirective(models.Model):
2472+ external_docs_branch = models.ForeignKey(ExternalDocsBranch)
2473+ import_from = models.CharField(
2474+ max_length=150,
2475+ help_text=_('File or directory to import from the branch. '
2476+ 'Ie: "docs/intro.md" (file) or '
2477+ '"docs" (complete directory), etc.'),
2478+ blank=True)
2479+ write_to = models.CharField(
2480+ max_length=150,
2481+ help_text=_('Article URL (for a specific file) or article namespace '
2482+ 'for a directory or a set of files.'),
2483+ blank=True)
2484+
2485+ def __str__(self):
2486+ return "{} -- {}".format(self.external_docs_branch,
2487+ self.import_from)
2488+
2489+
2490+class ImportedArticle(models.Model):
2491+ page = models.ForeignKey(Page)
2492+ branch = models.ForeignKey(ExternalDocsBranch)
2493+ last_import = models.DateTimeField(
2494+ _('Datetime'), help_text=_('Datetime of last import.'))
2495
2496=== added directory 'md_importer/tests'
2497=== added file 'md_importer/tests/__init__.py'
2498=== added directory 'md_importer/tests/data'
2499=== added directory 'md_importer/tests/data/link-broken-test'
2500=== added file 'md_importer/tests/data/link-broken-test/file1.md'
2501--- md_importer/tests/data/link-broken-test/file1.md 1970-01-01 00:00:00 +0000
2502+++ md_importer/tests/data/link-broken-test/file1.md 2016-01-19 00:21:38 +0000
2503@@ -0,0 +1,5 @@
2504+# Test
2505+
2506+This is a link to [the file3 file](file3.md).
2507+
2508+That's it.
2509
2510=== added file 'md_importer/tests/data/link-broken-test/file2.md'
2511--- md_importer/tests/data/link-broken-test/file2.md 1970-01-01 00:00:00 +0000
2512+++ md_importer/tests/data/link-broken-test/file2.md 2016-01-19 00:21:38 +0000
2513@@ -0,0 +1,3 @@
2514+# File 2
2515+
2516+Here's just some text.
2517
2518=== added directory 'md_importer/tests/data/link-test'
2519=== added file 'md_importer/tests/data/link-test/file1.md'
2520--- md_importer/tests/data/link-test/file1.md 1970-01-01 00:00:00 +0000
2521+++ md_importer/tests/data/link-test/file1.md 2016-01-19 00:21:38 +0000
2522@@ -0,0 +1,5 @@
2523+# Test
2524+
2525+This is a link to [the file2 file](file2.md).
2526+
2527+That's it.
2528
2529=== added file 'md_importer/tests/data/link-test/file2.md'
2530--- md_importer/tests/data/link-test/file2.md 1970-01-01 00:00:00 +0000
2531+++ md_importer/tests/data/link-test/file2.md 2016-01-19 00:21:38 +0000
2532@@ -0,0 +1,3 @@
2533+# File 2
2534+
2535+Here's just some text.
2536
2537=== added directory 'md_importer/tests/data/link2-test'
2538=== added file 'md_importer/tests/data/link2-test/file1.md'
2539--- md_importer/tests/data/link2-test/file1.md 1970-01-01 00:00:00 +0000
2540+++ md_importer/tests/data/link2-test/file1.md 2016-01-19 00:21:38 +0000
2541@@ -0,0 +1,5 @@
2542+# Test
2543+
2544+This is a link to [the file3 file](file3.md).
2545+
2546+That's it.
2547
2548=== added file 'md_importer/tests/data/link2-test/file3.md'
2549--- md_importer/tests/data/link2-test/file3.md 1970-01-01 00:00:00 +0000
2550+++ md_importer/tests/data/link2-test/file3.md 2016-01-19 00:21:38 +0000
2551@@ -0,0 +1,3 @@
2552+# File 3
2553+
2554+Here's just some text.
2555
2556=== added directory 'md_importer/tests/data/local-image-test'
2557=== added file 'md_importer/tests/data/local-image-test/test1.md'
2558--- md_importer/tests/data/local-image-test/test1.md 1970-01-01 00:00:00 +0000
2559+++ md_importer/tests/data/local-image-test/test1.md 2016-01-19 00:21:38 +0000
2560@@ -0,0 +1,6 @@
2561+# Local image test
2562+
2563+Here is a local image:
2564+
2565+![Alt text for a remote image](img.jpg)
2566+
2567
2568=== added directory 'md_importer/tests/data/remote-image-test'
2569=== added file 'md_importer/tests/data/remote-image-test/test1.md'
2570--- md_importer/tests/data/remote-image-test/test1.md 1970-01-01 00:00:00 +0000
2571+++ md_importer/tests/data/remote-image-test/test1.md 2016-01-19 00:21:38 +0000
2572@@ -0,0 +1,6 @@
2573+# Local image test
2574+
2575+Here is a local image:
2576+
2577+![Alt text for a remote image](http://design.ubuntu.com/wp-content/uploads/ubuntu-logo112.png)
2578+
2579
2580=== added directory 'md_importer/tests/data/snapcraft-test'
2581=== added file 'md_importer/tests/data/snapcraft-test/CONTRIBUTING.md'
2582--- md_importer/tests/data/snapcraft-test/CONTRIBUTING.md 1970-01-01 00:00:00 +0000
2583+++ md_importer/tests/data/snapcraft-test/CONTRIBUTING.md 2016-01-19 00:21:38 +0000
2584@@ -0,0 +1,32 @@
2585+# Snapcraft Contribution Guide
2586+
2587+Welcome to Snapcraft! We're a pretty friendly community and we're thrilled that
2588+you want to make Snapcraft even better. However, we do ask that you follow some
2589+general guidelines while doing so, just so we can keep things organized around
2590+here.
2591+
2592+1. Sign the [contributor license agreement][1].
2593+
2594+ This is how you give us permission to use your contributions.
2595+
2596+2. We use a forking, feature-based workflow.
2597+
2598+ Make a fork of Snapcraft, and create a branch named specifically for the
2599+ feature on which you'd like to work. Make your changes there, adding new
2600+ tests as needed, and make sure the existing tests continue to pass when your
2601+ changes are complete (for information about running the tests, see the
2602+ [HACKING][2] document).
2603+
2604+3. Squash commits into one, well-formatted commit.
2605+
2606+ If you really feel like there should be more than one commit in your branch,
2607+ then you're probably trying to introduce more than one feature and you should
2608+ make another branch for it.
2609+
2610+4. Submit a pull request to get changes from your branch into master.
2611+
2612+ If you want to get the change into 1.x as well, make a note of it on the pull
2613+ request and it can be cherry-picked after the merge.
2614+
2615+[1]: http://www.ubuntu.com/legal/contributors/
2616+[2]: HACKING.md
2617
2618=== added file 'md_importer/tests/data/snapcraft-test/HACKING.md'
2619--- md_importer/tests/data/snapcraft-test/HACKING.md 1970-01-01 00:00:00 +0000
2620+++ md_importer/tests/data/snapcraft-test/HACKING.md 2016-01-19 00:21:38 +0000
2621@@ -0,0 +1,48 @@
2622+# Snapcraft
2623+
2624+## Running
2625+
2626+To see all the commands and options, run `snapcraft --help`.
2627+
2628+## Testing
2629+
2630+Simply run the top level testing script:
2631+
2632+ ./runtests.sh
2633+
2634+- If you want to get a test coverage report, install python3-coverage before running the tests:
2635+
2636+ sudo apt-get install python3-coverage
2637+
2638+
2639+- If you don't want to run the plainbox integration tests, you can skip them by setting SNAPCRAFT_TESTS_SKIP_PLAINBOX=1 in your environment.
2640+
2641+- If you are on 15.04 or earlier, you will need to run:
2642+
2643+ sudo add-apt-repository ppa:hardware-certification/public
2644+
2645+### PPA
2646+
2647+You can install the daily build PPA by running:
2648+
2649+ sudo add-apt-repository ppa:snappy-dev/snapcraft-daily
2650+
2651+## Hacking
2652+
2653+We'd love the help!
2654+
2655+- Submit pull requests against [snapcraft](https://github.com/ubuntu-core/snapcraft/pulls)
2656+- Our mailing list is snappy-devel@lists.ubuntu.com
2657+- We can also be found on the #snappy IRC channel on Freenode
2658+
2659+### Project Layout
2660+
2661+- **bin:** Holds the main snapcraft script. Putting this bin in your PATH or directly running scripts from it will find the rest of the source tree automatically.
2662+
2663+- **examples:** Entering the subdirectories and running `../../bin/snapcraft snap` will generally yield interesting results. These examples will give you an idea of what snapcraft can do. These examples are not used during automated testing, they are simply for experimenting.
2664+
2665+- **plugins:** Holds yaml metadata for the current snapcraft plugins.
2666+
2667+- **tests:** Tests, obviously. `unit` holds Python unit tests and `plainbox` holds plainbox integration tests.
2668+
2669+- **snapcraft:** The Python module that houses the core snapcraft logic. The `plugins` subdirectory holds the code for each plugin.
2670
2671=== added file 'md_importer/tests/data/snapcraft-test/README.md'
2672--- md_importer/tests/data/snapcraft-test/README.md 1970-01-01 00:00:00 +0000
2673+++ md_importer/tests/data/snapcraft-test/README.md 2016-01-19 00:21:38 +0000
2674@@ -0,0 +1,33 @@
2675+[![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]
2676+
2677+# Snapcraft
2678+
2679+Snapcraft is a delightful packaging tool
2680+
2681+Snapcraft helps you assemble a whole project in a single tree out of
2682+many pieces. It can drive a very wide range of build and packaging systems,
2683+so that you can simply list all the upstream projects you want and have
2684+them built and installed together as a single tree.
2685+
2686+![Snapcraft Overview][overview-image]
2687+
2688+For example, say you want to make a product that includes PyPI packages,
2689+Node.js packages from NPM, Java, and a bunch of daemons written in C and
2690+C++ that are built with autotools, snapcraft would make assembling the
2691+final tree very easy.
2692+
2693+Snapcraft allows easy crafting of snap packages for the [snappy Ubuntu Core](http://ubuntu.com/snappy)
2694+transactional update system.
2695+
2696+## More Information
2697+
2698+* [Introduction](docs/intro.md) to all the details about the concepts behind snapcraft.
2699+* [Hacking guide](HACKING.md) to contribute if you're interested in developing Snapcraft.
2700+
2701+[travis-image]: https://travis-ci.org/ubuntu-core/snapcraft.svg?branch=master
2702+[travis-url]: https://travis-ci.org/ubuntu-core/snapcraft
2703+
2704+[coveralls-image]: https://coveralls.io/repos/ubuntu-core/snapcraft/badge.svg?branch=master&service=github
2705+[coveralls-url]: https://coveralls.io/github/ubuntu-core/snapcraft?branch=master
2706+
2707+[overview-image]: https://rawgit.com/ted-gould/snapcraft/snapcraft-overview-diagram/docs/snapcraft%20overview.svg
2708
2709=== added directory 'md_importer/tests/data/snapcraft-test/docs'
2710=== added file 'md_importer/tests/data/snapcraft-test/docs/get-started.md'
2711--- md_importer/tests/data/snapcraft-test/docs/get-started.md 1970-01-01 00:00:00 +0000
2712+++ md_importer/tests/data/snapcraft-test/docs/get-started.md 2016-01-19 00:21:38 +0000
2713@@ -0,0 +1,47 @@
2714+# Setting up your Ubuntu development host
2715+
2716+Ubuntu is a great and convenient OS for developers. Snappy developer tools are
2717+readily available to enable app developers familiar with Ubuntu to port and
2718+write new software for a snappy-based system easily.
2719+
2720+For app developers that want the latest stable tools to work on Snappy
2721+technology, we recommend to use the latest classic Ubuntu Long-Term Support
2722+(LTS) release as the host. At the time of writing this is Ubuntu 14.04 LTS. For
2723+those not using an Ubuntu machine (and you should), you can use a VM
2724+(VirtualBox, VMware, Vagrant) to execute your Ubuntu development host.
2725+
2726+This version of snapcraft only works on Ubuntu 16.04 (Xenial Xerus), for
2727+previous versions of snapcraft, refer to the
2728+[1.x documentation](https://github.com/ubuntu-core/snapcraft/blob/1.x/docs/get-started.md).
2729+
2730+Once your Ubuntu host system is up and running, you can then enable the
2731+snappy-tools PPA to get the latest tools to develop for Snappy. A PPA is a
2732+Personal Package Archive that developers can subscribe to install and get
2733+frequent updates of the software the archive contains. Open up a terminal with
2734+`Ctrl+Alt+T` and type the following command to add the snappy-tools PPA to
2735+your system:
2736+
2737+ $ sudo apt-add-repository ppa:snappy-dev/tools
2738+ $ sudo apt update
2739+
2740+After that, running the following command will install the `snappy-tools`
2741+package, which will in turn install the optimal selection of Snappy development
2742+software to your system.
2743+
2744+ $ sudo apt install snappy-tools
2745+
2746+The snappy-tools PPA is officially supported by the Snappy Core team for
2747+Ubuntu LTS releases. In addition to it, we try to keep snappy-tools also
2748+conveniently available for the latest Ubuntu stable release as well as the
2749+current development release for those who prefer those as host. For a
2750+production environment however, we recommend using an Ubuntu LTS-based host.
2751+
2752+This is the most important selection of tools you will get after installation:
2753+
2754+ snappy try - try snaps from a .snap, the [stage] or [snap] dir
2755+ snappy-remote - run snappy operations on remote snappy target by IP
2756+ snapcraft - the snap build tool for all snaps
2757+
2758+# Next
2759+
2760+How about putting together [your first snap](your-first-snap.md) now?
2761
2762=== added file 'md_importer/tests/data/snapcraft-test/docs/intro.md'
2763--- md_importer/tests/data/snapcraft-test/docs/intro.md 1970-01-01 00:00:00 +0000
2764+++ md_importer/tests/data/snapcraft-test/docs/intro.md 2016-01-19 00:21:38 +0000
2765@@ -0,0 +1,104 @@
2766+# Intro
2767+
2768+Snapcraft is a build and packaging tool which helps you package your software
2769+as a snap. It makes it easy to incorporate components from different sources
2770+and build technologies or solutions.
2771+
2772+# Key concepts
2773+
2774+A `.snap` package for the Ubuntu Core system contains all its
2775+dependencies. This has a couple of advantages over traditional `deb` or
2776+`rpm` based dependency handling, the most important being that a
2777+developer can always be assured that there are no regressions triggered by
2778+changes to the system underneath their app.
2779+
2780+Snapcraft makes bundling these dependencies easy by allowing you to
2781+specify them as "parts" in the `snapcraft.yaml` file.
2782+
2783+# Snappy
2784+
2785+Snappy Ubuntu Core is a new rendition of Ubuntu with transactional updates - a
2786+minimal server image with the same libraries as today's Ubuntu, but
2787+applications are provided through a simpler mechanism.
2788+
2789+Snappy apps and Ubuntu Core itself can be upgraded atomically and rolled back
2790+if needed. Apps are also strictly confined and sandboxed to safeguard your
2791+data and system.
2792+
2793+## Parts
2794+
2795+A central aspect of a snapcraft recipe is a "part". A part is a piece
2796+of software or data that the snap package requires to work or to
2797+build other parts. Each part is managed by a snapcraft plugin and parts
2798+are usually independent of each other.
2799+
2800+## Plugin
2801+
2802+Each part has a `plugin` associated to it, this `plugin` provides the mechanism
2803+to handle it. Parts are driven through plugins, there are a variety of plugins
2804+already included for python 2 and 3, go, java, and cmake or autotools based
2805+projects.
2806+
2807+## Lifecycle
2808+
2809+Each part goes through the following steps:
2810+
2811+### Pull
2812+
2813+The first is that each part is pulled. This step will download
2814+content, e.g. checkout a git repository or download a binary component
2815+like the Java SDK. Snapcraft will create a `parts/` directory with
2816+sub-directories like `parts/part-name/src` for each part that contains
2817+the downloaded content.
2818+
2819+### Build
2820+
2821+The next step is that each part is built in its `parts/part-name/build`
2822+directory and installs itself into `parts/part-name/install`.
2823+
2824+### Stage
2825+
2826+After the build of each part the parts are combined into a single
2827+directory tree that is called the "staging area". It can be found
2828+under the `./stage` directory.
2829+
2830+This `./stage` directory is useful for building outside code that isn't in the
2831+`snapcraft.yaml` recipe against the snap contents. For example, you might
2832+build a local project against the libraries in `./stage` by running `snapcraft
2833+shell make`. Though in general, you are encouraged to add even local
2834+projects to snapcraft.yaml with a local `source:` path.
2835+
2836+For rapid iteration one can run `snappy try` against this directory to have it
2837+mounted in a `snappy` capable system.
2838+
2839+### Strip
2840+
2841+The strip step moves the data into a `./snap` directory. It contains only
2842+the content that will be put into the final snap package, unlike the staging
2843+area which may include some development files not destined for your package.
2844+
2845+The Snappy metadata information about your project will also now be placed in
2846+`./snap/meta`. Snapcraft takes care of generating all the meta-data Snappy
2847+expects. For a breakdown of what this is, have a look at our [Snappy developer
2848+reference](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
2849+
2850+This `./snap` directory is useful for inspecting what is going into your snap
2851+and to make any final post-processing on snapcraft's output.
2852+
2853+For rapid iteration one can run `snappy try` against this directory to have it
2854+mounted in a `snappy` capable system.
2855+
2856+### Snap
2857+
2858+The final step builds a snap package out of the `snap` directory. This `.snap`
2859+file can be uploaded to the Ubuntu Store and published directly to Snappy
2860+users.
2861+
2862+This command can also be used with a directory argument for projects that
2863+are not following the snapcraft lifecycle but which follow the internal
2864+snap format.
2865+
2866+# Next
2867+
2868+After introducing the key concept of snapcraft it is probably a good
2869+time [get set up](get-started.md) to create your first snap with snapcraft.
2870
2871=== added file 'md_importer/tests/data/snapcraft-test/docs/ros-snap.md'
2872--- md_importer/tests/data/snapcraft-test/docs/ros-snap.md 1970-01-01 00:00:00 +0000
2873+++ md_importer/tests/data/snapcraft-test/docs/ros-snap.md 2016-01-19 00:21:38 +0000
2874@@ -0,0 +1,366 @@
2875+# Using Snappy with ROS
2876+
2877+The [Robot Operating System][1] (ROS) is an open-source collection of tools and
2878+libraries meant to aid in the development of complex robotic systems. ROS is
2879+excellent at what it does, but there are a few things it doesn't do:
2880+
2881+- Security: How does one prevent a rogue application from interfering with one's
2882+ finely-honed ROS ecosystem? Confinement and access control isn't one of the
2883+ many problems solved by ROS.
2884+
2885+- Deployment: How does one deploy an entire ROS project in one step, without
2886+ worrying about dependencies?
2887+
2888+- Updating: How does one allow end-users to update the entire ROS project in one
2889+ step, while retaining for the possibility of rolling back the change if
2890+ the update goes sideways?
2891+
2892+These are all problems solved by Snappy; the combination of the two is a perfect
2893+match for consumer robotic systems.
2894+
2895+
2896+## Package your current ROS project as a .snap
2897+
2898+Let's assume you already have a ROS project. It can be as simple or as
2899+complicated as you like, but for this example, our project will be made up of:
2900+
2901+- A ROS package containing a C++ "talker."
2902+- A ROS package containing a Python "listener," as well as a launch file to
2903+ bring both the talker and listener up at the same time with roscore.
2904+
2905+Our objective will be to create a .snap containing these pieces and their
2906+dependencies. The easiest way to do that is with Snapcraft, using its Catkin
2907+plugin. But before we get to that, let's create our project. Prerequisites for
2908+this walkthrough:
2909+
2910+- Installed Snapcraft (see [Getting Started][2]).
2911+- Installed/configured ROS Indigo (see the [Indigo installation tutorial][3]).
2912+- An empty ROS workspace (see the Catkin [workspace tutorial][4]).
2913+- General ROS experience (at least go through the [C++ pub/sub tutorial][5]).
2914+- General Snapcraft experience (read [Your first snap][6]).
2915+
2916+
2917+### C++ "talker"
2918+
2919+First, create the package:
2920+
2921+ $ catkin_create_pkg talker roscpp std_msgs
2922+
2923+Now make sure the `package.xml` is setup correctly (comments removed for
2924+brevity):
2925+
2926+```xml
2927+<?xml version="1.0"?>
2928+<package>
2929+ <name>talker</name>
2930+ <version>0.0.0</version>
2931+ <description>The talker package</description>
2932+ <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
2933+ <license>TODO</license>
2934+ <buildtool_depend>catkin</buildtool_depend>
2935+ <build_depend>roscpp</build_depend>
2936+ <build_depend>std_msgs</build_depend>
2937+ <run_depend>roscpp</run_depend>
2938+ <run_depend>std_msgs</run_depend>
2939+</package>
2940+```
2941+
2942+Also make sure the `CMakeLists.txt` is setup correctly. Importantly, make sure
2943+that you've specified install rules, since Snapcraft uses `make install`.
2944+Anything that doesn't have an install rule won't end up in the final .snap:
2945+
2946+```cmake
2947+cmake_minimum_required(VERSION 2.8.3)
2948+project(talker)
2949+
2950+find_package(catkin REQUIRED COMPONENTS
2951+ roscpp
2952+ std_msgs
2953+)
2954+
2955+catkin_package()
2956+
2957+include_directories(${catkin_INCLUDE_DIRS})
2958+
2959+add_executable(talker_node src/talker_node.cpp)
2960+
2961+target_link_libraries(talker_node ${catkin_LIBRARIES})
2962+
2963+install(TARGETS talker_node
2964+ ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
2965+ LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
2966+ RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
2967+)
2968+```
2969+
2970+Finally, for `src/talker_node.cpp`, we have this:
2971+
2972+```cpp
2973+#include <sstream>
2974+
2975+#include <ros/ros.h>
2976+#include <std_msgs/String.h>
2977+
2978+int main(int argc, char **argv)
2979+{
2980+ ros::init(argc, argv, "talker");
2981+
2982+ ros::NodeHandle nodeHandle;
2983+
2984+ ros::Publisher publisher = nodeHandle.advertise<std_msgs::String>("chatter", 1);
2985+
2986+ ros::Rate loopRate(10);
2987+
2988+ int count = 0;
2989+ while (ros::ok())
2990+ {
2991+ std_msgs::String message;
2992+
2993+ std::stringstream stream;
2994+ stream << "Hello world " << count++;
2995+ message.data = stream.str();
2996+
2997+ ROS_INFO("%s", message.data.c_str());
2998+
2999+ publisher.publish(message);
3000+
3001+ ros::spinOnce();
3002+
3003+ loopRate.sleep();
3004+ }
3005+
3006+ return 0;
3007+}
3008+```
3009+
3010+The "talker" is now complete.
3011+
3012+
3013+### The Python "listener"
3014+
3015+First, create the package:
3016+
3017+ $ catkin_create_pkg listener rospy std_msgs
3018+
3019+Now make sure the `package.xml` is setup correctly (comments removed for
3020+brevity):
3021+
3022+```xml
3023+<?xml version="1.0"?>
3024+<package>
3025+ <name>listener</name>
3026+ <version>0.0.0</version>
3027+ <description>The listener package</description>
3028+ <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
3029+ <license>TODO</license>
3030+ <buildtool_depend>catkin</buildtool_depend>
3031+ <build_depend>rospy</build_depend>
3032+ <build_depend>std_msgs</build_depend>
3033+ <run_depend>rospy</run_depend>
3034+ <run_depend>std_msgs</run_depend>
3035+</package>
3036+```
3037+
3038+Also make sure the `CMakeLists.txt` is setup correctly. Again, it's important to remember the install rules:
3039+
3040+```cmake
3041+cmake_minimum_required(VERSION 2.8.3)
3042+project(listener)
3043+
3044+find_package(catkin REQUIRED COMPONENTS
3045+ rospy
3046+ std_msgs
3047+)
3048+
3049+catkin_package()
3050+
3051+install(PROGRAMS
3052+ scripts/listener_node
3053+ DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
3054+)
3055+
3056+install(FILES
3057+ talk_and_listen.launch
3058+ DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
3059+)
3060+```
3061+
3062+Now for `scripts/listener_node`, we have this:
3063+
3064+```python
3065+#!/usr/bin/env python
3066+
3067+import rospy
3068+from std_msgs.msg import String
3069+
3070+def callback(data):
3071+ rospy.loginfo('I heard %s', data.data)
3072+
3073+def listener():
3074+ rospy.init_node('listener')
3075+
3076+ rospy.Subscriber('babble', String, callback)
3077+
3078+ rospy.spin()
3079+
3080+if __name__ == '__main__':
3081+ listener()
3082+```
3083+
3084+Make sure this script is executable:
3085+
3086+ $ chmod +x scripts/listener_node
3087+
3088+Note that the listener subscribes to the `babble` topic, but the talker
3089+publishes on `chatter`. We need to make sure we account for that in
3090+`talk_and_listen.launch`:
3091+
3092+```xml
3093+<launch>
3094+ <node name = "talker" pkg = "talker" type = "talker_node"
3095+ output = "screen" />
3096+
3097+ <node name = "listener" pkg = "listener" type = "listener_node"
3098+ output = "screen">
3099+ <remap from = "babble" to = "chatter" />
3100+ </node>
3101+</launch>
3102+```
3103+
3104+The "listener" is now complete.
3105+
3106+
3107+### Verify functionality
3108+
3109+You can verify that everything works by getting in the workspace root and
3110+running:
3111+
3112+ $ catkin_make
3113+ $ catkin_make install
3114+ $ roslaunch listener talk_and_listen.launch
3115+
3116+You should see them communicating, looking something like this:
3117+
3118+ ...
3119+ [INFO] [WallTime: 1449512660.316140] I heard Hello world 2
3120+ [ INFO] [1449512660.415941529]: Hello world 3
3121+ [INFO] [WallTime: 1449512660.416330] I heard Hello world 3
3122+ [ INFO] [1449512660.515954882]: Hello world 4
3123+ [INFO] [WallTime: 1449512660.516307] I heard Hello world 4
3124+ [ INFO] [1449512660.615954473]: Hello world 5
3125+ [INFO] [WallTime: 1449512660.616306] I heard Hello world 5
3126+ ...
3127+
3128+### Put it all in a .snap
3129+
3130+Let's get back to our stated objective, which was to create a .snap containing
3131+these pieces and their dependencies. As mentioned, the easiest way to create a
3132+.snap with complex dependencies is to use Snapcraft. Snapcraft contains a plugin
3133+especially for Catkin, which makes creating a ROS .snap particularly easy.
3134+
3135+We tell Snapcraft how to create the .snap via a file named `snapcraft.yaml`;
3136+let's create that file in the workspace root, containing the following:
3137+
3138+```yaml
3139+name: ros-talker-and-listener
3140+version: 1.0
3141+summary: ROS Example
3142+description: Contains talker/listener ROS packages and a .launch file.
3143+
3144+binaries:
3145+ launch-project:
3146+ exec: roslaunch listener talk_and_listen.launch
3147+
3148+parts:
3149+ foo:
3150+ plugin: catkin
3151+ source: .
3152+ catkin-packages:
3153+ - talker
3154+ - listener
3155+ stage-packages:
3156+ - ros-indigo-ros-core
3157+```
3158+
3159+Most of this file should look familiar to you if you've met the prerequisites,
3160+but let's focus on a few specific pieces.
3161+
3162+```yaml
3163+# ...
3164+binaries:
3165+ launch-project:
3166+ exec: roslaunch listener talk_and_listen.launch
3167+# ...
3168+```
3169+
3170+Even though the `talker` and `listener` packages will be installed, they'll be
3171+within a confined ROS installation, so the user won't be able to simply call
3172+`rosrun` or `roslaunch`. Instead, you have control over how your .snap is used,
3173+and here we specify that we only want a single binary, called "launch-project",
3174+which results in the `roslaunch` call you see. If this seems confusing now, it
3175+will make more sense when we actually use it.
3176+
3177+```yaml
3178+# ...
3179+parts:
3180+ foo:
3181+ plugin: catkin
3182+ source: .
3183+ catkin-packages:
3184+ - talker
3185+ - listener
3186+ stage-packages:
3187+ - ros-indigo-ros-core
3188+# ...
3189+```
3190+
3191+This is specifying that the .snap is made up of a single part, called "foo,"
3192+which utilizes the Catkin plugin. It states that the workspace is in the same
3193+path as the `snapcraft.yaml`, and it specifies which ROS packages should be
3194+included in the .snap (`talker` and `listener`). Finally, and this is important,
3195+it specifies that the Ubuntu package containing `roscore` (ros-indigo-ros-core)
3196+should be installed into the .snap.
3197+
3198+That last point is worth discussing. Currently, since .snaps cannot depend upon
3199+each other, any .snap that uses roscore must distribute roscore within it. We're
3200+working on some new features that will enable sharing roscore between snaps, but
3201+until then there are some limitations to keep in mind:
3202+
3203+- `roscore` must be bundled into each .snap that requires it.
3204+- Snappy's port negotiation feature is still a work-in-progress, which means
3205+ that only a single .snap that runs `roscore` can be installed at a time or
3206+ they will fight for the same port.
3207+
3208+Now that we understand the `snapcraft.yaml`, let's create the .snap!
3209+
3210+ $ snapcraft snap
3211+
3212+This will take some time to pull down the dependencies etc., but in the end
3213+you'll have a .snap.
3214+
3215+
3216+### Take the .snap for a test drive
3217+
3218+You can transfer your newly-minted .snap to your Ubuntu Core machine and install
3219+it at the same time via `snappy-remote`, for example:
3220+
3221+ $ snappy-remote --url=ssh://<host>:<port> install \
3222+ ros-talker-and-listener_1.0_amd64.snap
3223+
3224+Now on the Ubuntu Core machine, take a look in `/apps/bin/`, and you'll see the
3225+binary you requested, called `ros-talker-and-listener.launch-project`. Test it
3226+out:
3227+
3228+ $ ros-talker-and-listener.launch-project
3229+
3230+And you should see the talker and listener communicating like before. As usual,
3231+ctrl+c will stop it. Note also that, since ROS is running in a confined
3232+environment, its log isn't in `$HOME/.ros` as usual, but in
3233+`$HOME/apps/ros-talker-and-listener.sideload/1.0/ros`.
3234+
3235+[1]: http://www.ros.org/
3236+[2]: get-started.md
3237+[3]: http://wiki.ros.org/indigo/Installation/Ubuntu
3238+[4]: http://wiki.ros.org/catkin/Tutorials/create_a_workspace
3239+[5]: http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber%28c%2B%2B%29
3240+[6]: your-first-snap.md
3241
3242=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg'
3243--- md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg 1970-01-01 00:00:00 +0000
3244+++ md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg 2016-01-19 00:21:38 +0000
3245@@ -0,0 +1,1199 @@
3246+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3247+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3248+
3249+<svg
3250+ xmlns:dc="http://purl.org/dc/elements/1.1/"
3251+ xmlns:cc="http://creativecommons.org/ns#"
3252+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
3253+ xmlns:svg="http://www.w3.org/2000/svg"
3254+ xmlns="http://www.w3.org/2000/svg"
3255+ xmlns:xlink="http://www.w3.org/1999/xlink"
3256+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
3257+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
3258+ width="629.42749"
3259+ height="583.61816"
3260+ viewBox="0 0 629.4275 583.61817"
3261+ id="svg2"
3262+ version="1.1"
3263+ inkscape:version="0.91 r13725"
3264+ sodipodi:docname="snapcraft overview.svg">
3265+ <defs
3266+ id="defs4">
3267+ <marker
3268+ inkscape:isstock="true"
3269+ style="overflow:visible"
3270+ id="marker9243"
3271+ refX="0"
3272+ refY="0"
3273+ orient="auto"
3274+ inkscape:stockid="Arrow2Mend">
3275+ <path
3276+ inkscape:connector-curvature="0"
3277+ transform="scale(-0.6,-0.6)"
3278+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3279+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3280+ id="path9245" />
3281+ </marker>
3282+ <marker
3283+ inkscape:isstock="true"
3284+ style="overflow:visible"
3285+ id="marker9024"
3286+ refX="0"
3287+ refY="0"
3288+ orient="auto"
3289+ inkscape:stockid="Arrow2Mend">
3290+ <path
3291+ inkscape:connector-curvature="0"
3292+ transform="scale(-0.6,-0.6)"
3293+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3294+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3295+ id="path9026" />
3296+ </marker>
3297+ <marker
3298+ inkscape:isstock="true"
3299+ style="overflow:visible"
3300+ id="marker8885"
3301+ refX="0"
3302+ refY="0"
3303+ orient="auto"
3304+ inkscape:stockid="Arrow2Mend">
3305+ <path
3306+ inkscape:connector-curvature="0"
3307+ transform="scale(-0.6,-0.6)"
3308+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3309+ style="fill:#ffffff;fill-opacity:0;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3310+ id="path8887" />
3311+ </marker>
3312+ <linearGradient
3313+ inkscape:collect="always"
3314+ id="linearGradient7897">
3315+ <stop
3316+ style="stop-color:#ffffff;stop-opacity:0"
3317+ offset="0"
3318+ id="stop7899" />
3319+ <stop
3320+ style="stop-color:#ffffff;stop-opacity:1"
3321+ offset="1"
3322+ id="stop7901" />
3323+ </linearGradient>
3324+ <marker
3325+ inkscape:isstock="true"
3326+ style="overflow:visible"
3327+ id="marker7803"
3328+ refX="0"
3329+ refY="0"
3330+ orient="auto"
3331+ inkscape:stockid="Arrow2Mend">
3332+ <path
3333+ inkscape:connector-curvature="0"
3334+ transform="scale(-0.6,-0.6)"
3335+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3336+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3337+ id="path7805" />
3338+ </marker>
3339+ <marker
3340+ inkscape:isstock="true"
3341+ style="overflow:visible"
3342+ id="marker5350"
3343+ refX="0"
3344+ refY="0"
3345+ orient="auto"
3346+ inkscape:stockid="Arrow2Mend">
3347+ <path
3348+ transform="scale(-0.6,-0.6)"
3349+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3350+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3351+ id="path5352"
3352+ inkscape:connector-curvature="0" />
3353+ </marker>
3354+ <marker
3355+ inkscape:isstock="true"
3356+ style="overflow:visible"
3357+ id="marker5271"
3358+ refX="0"
3359+ refY="0"
3360+ orient="auto"
3361+ inkscape:stockid="Arrow2Mend">
3362+ <path
3363+ transform="scale(-0.6,-0.6)"
3364+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3365+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3366+ id="path5273"
3367+ inkscape:connector-curvature="0" />
3368+ </marker>
3369+ <marker
3370+ inkscape:isstock="true"
3371+ style="overflow:visible"
3372+ id="marker5198"
3373+ refX="0"
3374+ refY="0"
3375+ orient="auto"
3376+ inkscape:stockid="Arrow2Mend">
3377+ <path
3378+ transform="scale(-0.6,-0.6)"
3379+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3380+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3381+ id="path5200"
3382+ inkscape:connector-curvature="0" />
3383+ </marker>
3384+ <marker
3385+ inkscape:isstock="true"
3386+ style="overflow:visible"
3387+ id="marker4983"
3388+ refX="0"
3389+ refY="0"
3390+ orient="auto"
3391+ inkscape:stockid="Arrow2Mend">
3392+ <path
3393+ transform="scale(-0.6,-0.6)"
3394+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3395+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3396+ id="path4985"
3397+ inkscape:connector-curvature="0" />
3398+ </marker>
3399+ <marker
3400+ inkscape:isstock="true"
3401+ style="overflow:visible"
3402+ id="marker4880"
3403+ refX="0"
3404+ refY="0"
3405+ orient="auto"
3406+ inkscape:stockid="Arrow2Mend">
3407+ <path
3408+ transform="scale(-0.6,-0.6)"
3409+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3410+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3411+ id="path4882"
3412+ inkscape:connector-curvature="0" />
3413+ </marker>
3414+ <marker
3415+ inkscape:stockid="Arrow2Mend"
3416+ orient="auto"
3417+ refY="0"
3418+ refX="0"
3419+ id="marker4828"
3420+ style="overflow:visible"
3421+ inkscape:isstock="true"
3422+ inkscape:collect="always">
3423+ <path
3424+ id="path4830"
3425+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3426+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3427+ transform="scale(-0.6,-0.6)"
3428+ inkscape:connector-curvature="0" />
3429+ </marker>
3430+ <marker
3431+ inkscape:isstock="true"
3432+ style="overflow:visible"
3433+ id="marker4782"
3434+ refX="0"
3435+ refY="0"
3436+ orient="auto"
3437+ inkscape:stockid="Arrow2Mend"
3438+ inkscape:collect="always">
3439+ <path
3440+ transform="scale(-0.6,-0.6)"
3441+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3442+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3443+ id="path4784"
3444+ inkscape:connector-curvature="0" />
3445+ </marker>
3446+ <marker
3447+ inkscape:stockid="Arrow2Mend"
3448+ orient="auto"
3449+ refY="0"
3450+ refX="0"
3451+ id="Arrow2Mend"
3452+ style="overflow:visible"
3453+ inkscape:isstock="true"
3454+ inkscape:collect="always">
3455+ <path
3456+ id="path4401"
3457+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3458+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3459+ transform="scale(-0.6,-0.6)"
3460+ inkscape:connector-curvature="0" />
3461+ </marker>
3462+ <marker
3463+ inkscape:stockid="Arrow1Lend"
3464+ orient="auto"
3465+ refY="0"
3466+ refX="0"
3467+ id="Arrow1Lend"
3468+ style="overflow:visible"
3469+ inkscape:isstock="true">
3470+ <path
3471+ id="path4377"
3472+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
3473+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
3474+ transform="matrix(-0.8,0,0,-0.8,-10,0)"
3475+ inkscape:connector-curvature="0" />
3476+ </marker>
3477+ <marker
3478+ inkscape:stockid="Arrow2Mend"
3479+ orient="auto"
3480+ refY="0"
3481+ refX="0"
3482+ id="Arrow2Mend-9"
3483+ style="overflow:visible"
3484+ inkscape:isstock="true">
3485+ <path
3486+ inkscape:connector-curvature="0"
3487+ id="path4401-8"
3488+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3489+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3490+ transform="scale(-0.6,-0.6)" />
3491+ </marker>
3492+ <linearGradient
3493+ id="linearGradient2795">
3494+ <stop
3495+ id="stop2797"
3496+ offset="0"
3497+ style="stop-color:#b8b8b8;stop-opacity:0.49803922;" />
3498+ <stop
3499+ id="stop2799"
3500+ offset="1"
3501+ style="stop-color:#7f7f7f;stop-opacity:0;" />
3502+ </linearGradient>
3503+ <linearGradient
3504+ id="linearGradient2787">
3505+ <stop
3506+ id="stop2789"
3507+ offset="0"
3508+ style="stop-color:#7f7f7f;stop-opacity:0.5;" />
3509+ <stop
3510+ id="stop2791"
3511+ offset="1"
3512+ style="stop-color:#7f7f7f;stop-opacity:0;" />
3513+ </linearGradient>
3514+ <linearGradient
3515+ id="linearGradient3676">
3516+ <stop
3517+ id="stop3678"
3518+ offset="0"
3519+ style="stop-color:#b2b2b2;stop-opacity:0.5;" />
3520+ <stop
3521+ id="stop3680"
3522+ offset="1"
3523+ style="stop-color:#b3b3b3;stop-opacity:0;" />
3524+ </linearGradient>
3525+ <linearGradient
3526+ id="linearGradient3236">
3527+ <stop
3528+ id="stop3244"
3529+ offset="0"
3530+ style="stop-color:#f4f4f4;stop-opacity:1" />
3531+ <stop
3532+ id="stop3240"
3533+ offset="1"
3534+ style="stop-color:white;stop-opacity:1" />
3535+ </linearGradient>
3536+ <linearGradient
3537+ id="linearGradient4671">
3538+ <stop
3539+ id="stop4673"
3540+ offset="0"
3541+ style="stop-color:#ffd43b;stop-opacity:1;" />
3542+ <stop
3543+ id="stop4675"
3544+ offset="1"
3545+ style="stop-color:#ffe873;stop-opacity:1" />
3546+ </linearGradient>
3547+ <linearGradient
3548+ id="linearGradient4689">
3549+ <stop
3550+ id="stop4691"
3551+ offset="0"
3552+ style="stop-color:#5a9fd4;stop-opacity:1;" />
3553+ <stop
3554+ id="stop4693"
3555+ offset="1"
3556+ style="stop-color:#306998;stop-opacity:1;" />
3557+ </linearGradient>
3558+ <linearGradient
3559+ gradientTransform="translate(100.2702,99.61116)"
3560+ gradientUnits="userSpaceOnUse"
3561+ xlink:href="#linearGradient4671"
3562+ id="linearGradient2987"
3563+ y2="144.75717"
3564+ x2="-65.308502"
3565+ y1="144.75717"
3566+ x1="224.23996" />
3567+ <linearGradient
3568+ gradientTransform="translate(100.2702,99.61116)"
3569+ gradientUnits="userSpaceOnUse"
3570+ xlink:href="#linearGradient4689"
3571+ id="linearGradient2990"
3572+ y2="76.313133"
3573+ x2="26.670298"
3574+ y1="77.475983"
3575+ x1="172.94208" />
3576+ <linearGradient
3577+ y2="144.75717"
3578+ x2="-65.308502"
3579+ y1="144.75717"
3580+ x1="224.23996"
3581+ gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
3582+ gradientUnits="userSpaceOnUse"
3583+ id="linearGradient2255"
3584+ xlink:href="#linearGradient4671"
3585+ inkscape:collect="always" />
3586+ <linearGradient
3587+ y2="76.313133"
3588+ x2="26.670298"
3589+ y1="76.176224"
3590+ x1="172.94208"
3591+ gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
3592+ gradientUnits="userSpaceOnUse"
3593+ id="linearGradient2258"
3594+ xlink:href="#linearGradient4689"
3595+ inkscape:collect="always" />
3596+ <radialGradient
3597+ gradientUnits="userSpaceOnUse"
3598+ gradientTransform="matrix(1,0,0,0.177966,0,108.7434)"
3599+ r="29.036913"
3600+ fy="132.28575"
3601+ fx="61.518883"
3602+ cy="132.28575"
3603+ cx="61.518883"
3604+ id="radialGradient2801"
3605+ xlink:href="#linearGradient2795"
3606+ inkscape:collect="always" />
3607+ <linearGradient
3608+ y2="137.27299"
3609+ x2="112.03144"
3610+ y1="192.35176"
3611+ x1="150.96111"
3612+ gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
3613+ gradientUnits="userSpaceOnUse"
3614+ id="linearGradient1475"
3615+ xlink:href="#linearGradient4671"
3616+ inkscape:collect="always" />
3617+ <linearGradient
3618+ y2="114.39767"
3619+ x2="135.66525"
3620+ y1="20.603781"
3621+ x1="26.648937"
3622+ gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
3623+ gradientUnits="userSpaceOnUse"
3624+ id="linearGradient1478"
3625+ xlink:href="#linearGradient4689"
3626+ inkscape:collect="always" />
3627+ <radialGradient
3628+ r="29.036913"
3629+ fy="132.28575"
3630+ fx="61.518883"
3631+ cy="132.28575"
3632+ cx="61.518883"
3633+ gradientTransform="matrix(2.382716e-8,-0.296405,1.43676,4.683673e-7,-128.544,150.5202)"
3634+ gradientUnits="userSpaceOnUse"
3635+ id="radialGradient1480"
3636+ xlink:href="#linearGradient2795"
3637+ inkscape:collect="always" />
3638+ <linearGradient
3639+ inkscape:collect="always"
3640+ xlink:href="#linearGradient7897"
3641+ id="linearGradient7903"
3642+ x1="574.78748"
3643+ y1="518.93152"
3644+ x2="627.95801"
3645+ y2="543.29303"
3646+ gradientUnits="userSpaceOnUse"
3647+ gradientTransform="translate(0,0.39292731)" />
3648+ <marker
3649+ inkscape:isstock="true"
3650+ style="overflow:visible"
3651+ id="marker9243-9"
3652+ refX="0"
3653+ refY="0"
3654+ orient="auto"
3655+ inkscape:stockid="Arrow2Mend">
3656+ <path
3657+ inkscape:connector-curvature="0"
3658+ transform="scale(-0.6,-0.6)"
3659+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
3660+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
3661+ id="path9245-6" />
3662+ </marker>
3663+ </defs>
3664+ <sodipodi:namedview
3665+ id="base"
3666+ pagecolor="#ffffff"
3667+ bordercolor="#666666"
3668+ borderopacity="1.0"
3669+ inkscape:pageopacity="1"
3670+ inkscape:pageshadow="2"
3671+ inkscape:zoom="1.5438176"
3672+ inkscape:cx="314.71375"
3673+ inkscape:cy="291.80908"
3674+ inkscape:document-units="px"
3675+ inkscape:current-layer="layer1"
3676+ showgrid="false"
3677+ units="px"
3678+ inkscape:showpageshadow="false"
3679+ showguides="false"
3680+ inkscape:guide-bbox="true"
3681+ inkscape:window-width="1486"
3682+ inkscape:window-height="1036"
3683+ inkscape:window-x="940"
3684+ inkscape:window-y="212"
3685+ inkscape:window-maximized="0"
3686+ fit-margin-top="20"
3687+ fit-margin-left="20"
3688+ fit-margin-right="20"
3689+ fit-margin-bottom="20"
3690+ showborder="true">
3691+ <sodipodi:guide
3692+ position="470.86538,187.82982"
3693+ orientation="1,0"
3694+ id="guide4356" />
3695+ <sodipodi:guide
3696+ position="586.76952,189.57274"
3697+ orientation="0,1"
3698+ id="guide4358" />
3699+ <sodipodi:guide
3700+ position="381.97649,255.80368"
3701+ orientation="1,0"
3702+ id="guide4360" />
3703+ <sodipodi:guide
3704+ position="548.4253,272.36142"
3705+ orientation="1,0"
3706+ id="guide4362" />
3707+ <sodipodi:guide
3708+ position="508.33815,349.92133"
3709+ orientation="0,1"
3710+ id="guide4364" />
3711+ <sodipodi:guide
3712+ position="612.91332,113.75575"
3713+ orientation="0,1"
3714+ id="guide4366" />
3715+ <sodipodi:guide
3716+ position="548.4253,36.195832"
3717+ orientation="0,1"
3718+ id="guide4688" />
3719+ <sodipodi:guide
3720+ position="186.76952,113.75575"
3721+ orientation="1,0"
3722+ id="guide4690" />
3723+ <sodipodi:guide
3724+ position="68.250992,243.60325"
3725+ orientation="1,0"
3726+ id="guide4694" />
3727+ <sodipodi:guide
3728+ position="420.02074,458.45319"
3729+ orientation="0,1"
3730+ id="guide5148" />
3731+ <sodipodi:guide
3732+ position="170.49847,514.08991"
3733+ orientation="1,0"
3734+ id="guide5150" />
3735+ <sodipodi:guide
3736+ position="222.20129,491.61043"
3737+ orientation="1,0"
3738+ id="guide5152" />
3739+ <sodipodi:guide
3740+ position="160.94469,342.68385"
3741+ orientation="0,1"
3742+ id="guide5154" />
3743+ <sodipodi:guide
3744+ position="400.91318,430.63483"
3745+ orientation="0,1"
3746+ id="guide5156" />
3747+ <sodipodi:guide
3748+ position="303.40842,400.56852"
3749+ orientation="1,0"
3750+ id="guide5158" />
3751+ </sodipodi:namedview>
3752+ <metadata
3753+ id="metadata7">
3754+ <rdf:RDF>
3755+ <cc:Work
3756+ rdf:about="">
3757+ <dc:format>image/svg+xml</dc:format>
3758+ <dc:type
3759+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
3760+ <dc:title></dc:title>
3761+ </cc:Work>
3762+ </rdf:RDF>
3763+ </metadata>
3764+ <g
3765+ inkscape:label="Layer 1"
3766+ inkscape:groupmode="layer"
3767+ id="layer1"
3768+ transform="translate(-139.15641,-459.62397)">
3769+ <g
3770+ id="g4174"
3771+ transform="translate(89.103862,103.1549)">
3772+ <rect
3773+ y="534.89746"
3774+ x="70.052551"
3775+ height="124.65837"
3776+ width="92.022942"
3777+ id="rect4136"
3778+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:0.13855423;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3779+ <rect
3780+ y="538.51373"
3781+ x="73.530533"
3782+ height="55"
3783+ width="85"
3784+ id="rect4138"
3785+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3786+ <text
3787+ sodipodi:linespacing="125%"
3788+ id="text4142"
3789+ y="561.84375"
3790+ x="116.18498"
3791+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3792+ xml:space="preserve"><tspan
3793+ y="561.84375"
3794+ x="116.18498"
3795+ id="tspan4144"
3796+ sodipodi:role="line">Source</tspan><tspan
3797+ id="tspan4146"
3798+ y="580.59375"
3799+ x="116.18498"
3800+ sodipodi:role="line">Code</tspan></text>
3801+ <rect
3802+ y="600.51373"
3803+ x="73.530533"
3804+ height="55"
3805+ width="85"
3806+ id="rect4138-1"
3807+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3808+ <text
3809+ sodipodi:linespacing="125%"
3810+ id="text4148"
3811+ y="623.04871"
3812+ x="115.86811"
3813+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3814+ xml:space="preserve"><tspan
3815+ y="623.04871"
3816+ x="115.86811"
3817+ id="tspan4150"
3818+ sodipodi:role="line">snapcraft</tspan><tspan
3819+ id="tspan4152"
3820+ y="641.79871"
3821+ x="115.86811"
3822+ sodipodi:role="line">yaml</tspan></text>
3823+ </g>
3824+ <path
3825+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
3826+ d="m 521.13289,693.32077 c 126.36166,0 166.44881,84.53159 166.44881,160.34858"
3827+ id="path4368"
3828+ inkscape:connector-curvature="0"
3829+ sodipodi:nodetypes="cc" />
3830+ <g
3831+ id="g4174-4"
3832+ transform="translate(524.45885,136.18127)">
3833+ <rect
3834+ y="534.89746"
3835+ x="70.052551"
3836+ height="124.65837"
3837+ width="92.022942"
3838+ id="rect4136-5"
3839+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:0.13855423;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3840+ <rect
3841+ y="538.51373"
3842+ x="73.530533"
3843+ height="55"
3844+ width="85"
3845+ id="rect4138-8"
3846+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3847+ <text
3848+ sodipodi:linespacing="125%"
3849+ id="text4142-2"
3850+ y="571.44373"
3851+ x="116.18498"
3852+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3853+ xml:space="preserve"><tspan
3854+ id="tspan4146-4"
3855+ y="571.44373"
3856+ x="116.18498"
3857+ sodipodi:role="line">Binaries</tspan></text>
3858+ <rect
3859+ y="600.51373"
3860+ x="73.530533"
3861+ height="55"
3862+ width="85"
3863+ id="rect4138-1-6"
3864+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3865+ <text
3866+ sodipodi:linespacing="125%"
3867+ id="text4148-2"
3868+ y="623.04871"
3869+ x="115.86811"
3870+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3871+ xml:space="preserve"><tspan
3872+ y="623.04871"
3873+ x="115.86811"
3874+ id="tspan4150-4"
3875+ sodipodi:role="line">package</tspan><tspan
3876+ id="tspan4152-7"
3877+ y="641.79871"
3878+ x="115.86811"
3879+ sodipodi:role="line">metadata</tspan></text>
3880+ </g>
3881+ <g
3882+ id="g4235"
3883+ transform="translate(287.58171,67.973856)">
3884+ <rect
3885+ y="801.38177"
3886+ x="338.99783"
3887+ height="122.00436"
3888+ width="122.00436"
3889+ id="rect4227"
3890+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3891+ <text
3892+ sodipodi:linespacing="125%"
3893+ id="text4229"
3894+ y="855.71198"
3895+ x="399.80914"
3896+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3897+ xml:space="preserve"><tspan
3898+ y="855.71198"
3899+ x="399.80914"
3900+ id="tspan4231"
3901+ sodipodi:role="line">Snappy</tspan><tspan
3902+ id="tspan4233"
3903+ y="885.71198"
3904+ x="399.80914"
3905+ sodipodi:role="line">Store</tspan></text>
3906+ </g>
3907+ <g
3908+ id="g4235-9"
3909+ transform="translate(42.701512,-168.19172)">
3910+ <rect
3911+ y="801.38177"
3912+ x="338.99783"
3913+ height="122.00436"
3914+ width="122.00436"
3915+ id="rect4227-0"
3916+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3917+ <text
3918+ sodipodi:linespacing="125%"
3919+ id="text4229-8"
3920+ y="869.47595"
3921+ x="399.82413"
3922+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3923+ xml:space="preserve"><tspan
3924+ id="tspan4233-2"
3925+ y="869.47595"
3926+ x="399.82413"
3927+ sodipodi:role="line">Snapcraft</tspan></text>
3928+ </g>
3929+ <path
3930+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker4782);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
3931+ d="m 610.02178,929.48634 c -284.09585,0 -170.80609,-75.81699 -284.09585,-75.81699"
3932+ id="path4692"
3933+ inkscape:connector-curvature="0" />
3934+ <path
3935+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker4828);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
3936+ d="m 610.02178,929.40975 c -284.09585,0 -170.80609,75.81695 -284.09585,75.81695"
3937+ id="path4692-3"
3938+ inkscape:connector-curvature="0" />
3939+ <path
3940+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker4880);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
3941+ d="m 610.02178,929.48634 -284.09585,0"
3942+ id="path4711"
3943+ inkscape:connector-curvature="0"
3944+ sodipodi:nodetypes="cc" />
3945+ <g
3946+ id="g4174-4-4"
3947+ transform="translate(374.16692,301.34465)">
3948+ <rect
3949+ y="534.89746"
3950+ x="70.052551"
3951+ height="187"
3952+ width="92.022942"
3953+ id="rect4136-5-5"
3954+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:0.13855423;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
3955+ <g
3956+ id="g4347"
3957+ transform="translate(0,-2.8292561e-6)">
3958+ <rect
3959+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
3960+ id="rect4138-8-8"
3961+ width="85"
3962+ height="55"
3963+ x="73.530533"
3964+ y="538.51373" />
3965+ <text
3966+ xml:space="preserve"
3967+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3968+ x="116.18498"
3969+ y="571.44373"
3970+ id="text4142-2-1"
3971+ sodipodi:linespacing="125%"><tspan
3972+ sodipodi:role="line"
3973+ x="116.18498"
3974+ y="571.44373"
3975+ id="tspan4146-4-4">Binaries</tspan></text>
3976+ </g>
3977+ <g
3978+ id="g4341"
3979+ transform="translate(0,-0.25683877)">
3980+ <rect
3981+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
3982+ id="rect4138-1-6-3"
3983+ width="85"
3984+ height="55"
3985+ x="73.530533"
3986+ y="600.51373" />
3987+ <text
3988+ xml:space="preserve"
3989+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
3990+ x="115.86811"
3991+ y="623.04871"
3992+ id="text4148-2-9"
3993+ sodipodi:linespacing="125%"><tspan
3994+ sodipodi:role="line"
3995+ id="tspan4150-4-6"
3996+ x="115.86811"
3997+ y="623.04871">package</tspan><tspan
3998+ sodipodi:role="line"
3999+ x="115.86811"
4000+ y="641.79871"
4001+ id="tspan4152-7-0">metadata</tspan></text>
4002+ </g>
4003+ <g
4004+ id="g4335">
4005+ <rect
4006+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4007+ id="rect4138-1-6-3-3"
4008+ width="85"
4009+ height="55"
4010+ x="74.001198"
4011+ y="662.00006" />
4012+ <text
4013+ xml:space="preserve"
4014+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4015+ x="116.33875"
4016+ y="684.53503"
4017+ id="text4148-2-9-4"
4018+ sodipodi:linespacing="125%"><tspan
4019+ sodipodi:role="line"
4020+ x="116.33875"
4021+ y="684.53503"
4022+ id="tspan4152-7-0-7">digital</tspan><tspan
4023+ sodipodi:role="line"
4024+ x="116.33875"
4025+ y="703.28503"
4026+ id="tspan4354">signature</tspan></text>
4027+ </g>
4028+ </g>
4029+ <g
4030+ id="g5113"
4031+ transform="translate(-6.743845,-3.9339095)">
4032+ <rect
4033+ y="502.17679"
4034+ x="256.82809"
4035+ height="69.124413"
4036+ width="119.70325"
4037+ id="rect4981"
4038+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:0.13855423;fill-rule:nonzero;stroke:#7e7e7e;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:3, 1;stroke-dashoffset:1;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
4039+ <text
4040+ sodipodi:linespacing="125%"
4041+ id="text5107"
4042+ y="533.09399"
4043+ x="316.85104"
4044+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4045+ xml:space="preserve"><tspan
4046+ y="533.09399"
4047+ x="316.85104"
4048+ id="tspan5109"
4049+ sodipodi:role="line">Snapcraft</tspan><tspan
4050+ id="tspan5111"
4051+ y="551.84399"
4052+ x="316.85104"
4053+ sodipodi:role="line">Cloud Parts</tspan></text>
4054+ </g>
4055+ <g
4056+ id="g5113-2"
4057+ transform="translate(146.95962,-3.6528961)">
4058+ <rect
4059+ y="502.17679"
4060+ x="256.82809"
4061+ height="69.124413"
4062+ width="250"
4063+ id="rect4981-9"
4064+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:0.13855423;fill-rule:nonzero;stroke:#7e7e7e;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:3, 1;stroke-dashoffset:1;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
4065+ <text
4066+ sodipodi:linespacing="125%"
4067+ id="text5107-3"
4068+ y="494.31686"
4069+ x="383.92838"
4070+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4071+ xml:space="preserve"><tspan
4072+ id="tspan5111-3"
4073+ y="494.31686"
4074+ x="383.92838"
4075+ sodipodi:role="line">Common Repositories</tspan></text>
4076+ </g>
4077+ <path
4078+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker5350);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4079+ d="m 442.56482,584.78891 0,27.81836"
4080+ id="path5160"
4081+ inkscape:connector-curvature="0" />
4082+ <path
4083+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker5271);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4084+ d="m 266.10088,700.55825 95.25682,0"
4085+ id="path5162"
4086+ inkscape:connector-curvature="0" />
4087+ <path
4088+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker5198);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4089+ d="m 309.65488,584.78891 c 0,44.67797 6.46286,84.29806 51.70282,84.57906"
4090+ id="path5164"
4091+ inkscape:connector-curvature="0" />
4092+ <path
4093+ inkscape:connector-curvature="0"
4094+ id="path5463-4"
4095+ style="fill:#1b1817;fill-opacity:1;fill-rule:evenodd;stroke:none"
4096+ d="m 439.90668,510.58478 c -11.2425,0 -20.36,9.11625 -20.36,20.3625 0,8.995 5.83375,16.6275 13.925,19.32125 1.01875,0.18625 1.39,-0.4425 1.39,-0.9825 0,-0.48375 -0.0175,-1.76375 -0.0275,-3.4625 -5.66375,1.23 -6.85875,-2.73 -6.85875,-2.73 -0.92625,-2.3525 -2.26125,-2.97875 -2.26125,-2.97875 -1.84875,-1.2625 0.14,-1.2375 0.14,-1.2375 2.04375,0.14375 3.11875,2.09875 3.11875,2.09875 1.81625,3.11125 4.76625,2.2125 5.92625,1.69125 0.185,-1.315 0.71125,-2.2125 1.2925,-2.72125 -4.52125,-0.515 -9.275,-2.26125 -9.275,-10.06375 0,-2.22375 0.79375,-4.04 2.09625,-5.46375 -0.21,-0.515 -0.90875,-2.585 0.2,-5.38875 0,0 1.70875,-0.5475 5.59875,2.08625 1.62375,-0.45125 3.36625,-0.67625 5.0975,-0.685 1.73,0.009 3.47125,0.23375 5.0975,0.685 3.8875,-2.63375 5.59375,-2.08625 5.59375,-2.08625 1.11125,2.80375 0.4125,4.87375 0.20375,5.38875 1.305,1.42375 2.0925,3.24 2.0925,5.46375 0,7.8225 -4.76125,9.54375 -9.29625,10.0475 0.73,0.62875 1.38125,1.87125 1.38125,3.77125 0,2.72125 -0.025,4.9175 -0.025,5.585 0,0.545 0.3675,1.17875 1.4,0.98 8.085,-2.69875 13.91375,-10.325 13.91375,-19.31875 0,-11.24625 -9.1175,-20.3625 -20.36375,-20.3625" />
4097+ <g
4098+ id="g5903"
4099+ transform="matrix(0.33153681,0,0,0.33153681,478.85446,509.13681)">
4100+ <path
4101+ id="path1948"
4102+ d="m 60.510156,6.3979729 c -4.583653,0.021298 -8.960939,0.4122177 -12.8125,1.09375 C 36.35144,9.4962267 34.291407,13.691825 34.291406,21.429223 l 0,10.21875 26.8125,0 0,3.40625 -26.8125,0 -10.0625,0 c -7.792459,0 -14.6157592,4.683717 -16.7500002,13.59375 -2.46182,10.212966 -2.5710151,16.586023 0,27.25 1.9059283,7.937852 6.4575432,13.593748 14.2500002,13.59375 l 9.21875,0 0,-12.25 c 0,-8.849902 7.657144,-16.656248 16.75,-16.65625 l 26.78125,0 c 7.454951,0 13.406253,-6.138164 13.40625,-13.625 l 0,-25.53125 c 0,-7.266339 -6.12998,-12.7247775 -13.40625,-13.9375001 -4.605987,-0.7667253 -9.385097,-1.1150483 -13.96875,-1.09375 z m -14.5,8.2187501 c 2.769547,0 5.03125,2.298646 5.03125,5.125 -2e-6,2.816336 -2.261703,5.09375 -5.03125,5.09375 -2.779476,-1e-6 -5.03125,-2.277415 -5.03125,-5.09375 -1e-6,-2.826353 2.251774,-5.125 5.03125,-5.125 z"
4103+ style="fill:url(#linearGradient1478);fill-opacity:1"
4104+ inkscape:connector-curvature="0" />
4105+ <path
4106+ id="path1950"
4107+ d="m 91.228906,35.054223 0,11.90625 c 0,9.230755 -7.825895,16.999999 -16.75,17 l -26.78125,0 c -7.335833,0 -13.406249,6.278483 -13.40625,13.625 l 0,25.531247 c 0,7.26634 6.318588,11.54032 13.40625,13.625 8.487331,2.49561 16.626237,2.94663 26.78125,0 6.750155,-1.95439 13.406253,-5.88761 13.40625,-13.625 l 0,-10.218747 -26.78125,0 0,-3.40625 26.78125,0 13.406254,0 c 7.79246,0 10.69625,-5.435408 13.40624,-13.59375 2.79933,-8.398886 2.68022,-16.475776 0,-27.25 -1.92578,-7.757441 -5.60387,-13.59375 -13.40624,-13.59375 l -10.062504,0 z m -15.0625,64.65625 c 2.779478,3e-6 5.03125,2.277417 5.03125,5.093747 -2e-6,2.82635 -2.251775,5.125 -5.03125,5.125 -2.76955,0 -5.03125,-2.29865 -5.03125,-5.125 2e-6,-2.81633 2.261697,-5.093747 5.03125,-5.093747 z"
4108+ style="fill:url(#linearGradient1475);fill-opacity:1"
4109+ inkscape:connector-curvature="0" />
4110+ <ellipse
4111+ transform="matrix(0.73406,0,0,0.809524,16.24958,27.00935)"
4112+ id="path1894"
4113+ style="opacity:0.44382025;fill:url(#radialGradient1480);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
4114+ cx="61.518883"
4115+ cy="132.28575"
4116+ rx="48.948284"
4117+ ry="8.6066771" />
4118+ </g>
4119+ <image
4120+ y="510.65253"
4121+ x="539.22363"
4122+ id="image7798"
4123+ xlink:href="data:image/png;base64,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
4278+Gt0EzcEAAAAASUVORK5CYII=
4279+"
4280+ preserveAspectRatio="none"
4281+ height="38.276688"
4282+ width="38.276688" />
4283+ <text
4284+ xml:space="preserve"
4285+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:24px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4286+ x="591.80872"
4287+ y="531.6861"
4288+ id="text8871"
4289+ sodipodi:linespacing="125%"><tspan
4290+ sodipodi:role="line"
4291+ id="tspan8873"
4292+ x="591.80872"
4293+ y="531.6861">……</tspan></text>
4294+ <rect
4295+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient7903);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4296+ id="rect7801"
4297+ width="97.053047"
4298+ height="81.728882"
4299+ x="574.12695"
4300+ y="494.32266" />
4301+ <g
4302+ id="g8879"
4303+ transform="translate(-0.27784156,15.003444)">
4304+ <path
4305+ sodipodi:nodetypes="czsssc"
4306+ inkscape:connector-curvature="0"
4307+ id="path8875"
4308+ d="m 249.76963,912.25001 c -9.61837,-7.34936 -19.09612,0.32907 -18.33733,7.25045 0.75879,6.92138 6.8411,11.37657 13.72067,11.42422 l 50.15039,0.3473 c 4.03101,0.0279 12.08612,-8.47417 10.0023,-16.84414 -2.08381,-8.36998 -14.30884,-16.77469 -26.39494,-5.93887"
4309+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
4310+ <path
4311+ sodipodi:nodetypes="cc"
4312+ inkscape:connector-curvature="0"
4313+ id="path8877"
4314+ d="m 243.62484,909.3225 c 0.55568,-15.55913 26.95063,-23.61653 39.4535,-3.3341"
4315+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
4316+ </g>
4317+ <g
4318+ id="g4347-5"
4319+ transform="translate(143.43127,303.02174)">
4320+ <g
4321+ id="g9018"
4322+ transform="translate(51.678531,-9.1687717)">
4323+ <rect
4324+ ry="5"
4325+ rx="5"
4326+ y="533.23474"
4327+ x="70.613205"
4328+ height="54.72216"
4329+ width="28.32032"
4330+ id="rect4138-8-8-9-9"
4331+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
4332+ <rect
4333+ y="538.65265"
4334+ x="73.53054"
4335+ height="43.886337"
4336+ width="22.485647"
4337+ id="rect4138-8-8-9"
4338+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.50221306;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
4339+ </g>
4340+ </g>
4341+ <g
4342+ id="g9789"
4343+ transform="translate(7.125194,1.2954898)">
4344+ <rect
4345+ y="1003.0229"
4346+ x="253.4267"
4347+ height="7.7959223"
4348+ width="18.228931"
4349+ id="rect9022"
4350+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
4351+ <g
4352+ id="g9756">
4353+ <rect
4354+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4355+ id="rect9022-7"
4356+ width="5.1752357"
4357+ height="5.5057745"
4358+ x="284.96884"
4359+ y="1004.7013" />
4360+ <path
4361+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4362+ d="m 263.23546,1007.3397 23.17593,0.229"
4363+ id="path9237"
4364+ inkscape:connector-curvature="0"
4365+ sodipodi:nodetypes="cc" />
4366+ <path
4367+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4368+ d="m 287.55645,1005.5162 0,-7.78642"
4369+ id="path9239"
4370+ inkscape:connector-curvature="0" />
4371+ <circle
4372+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:url(#marker9243);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4373+ id="path9241"
4374+ cx="287.55646"
4375+ cy="996.92834"
4376+ r="1.2954898" />
4377+ <path
4378+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
4379+ d="m 288.13774,996.68547 c 0,0 14.33135,-5.91067 14.41232,-0.72871 0.081,5.18194 -14.41232,0.72871 -14.41232,0.72871 z"
4380+ id="path9595"
4381+ inkscape:connector-curvature="0"
4382+ sodipodi:nodetypes="czc" />
4383+ <path
4384+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
4385+ d="m 286.87091,996.68547 c 0,0 -14.33134,-5.91067 -14.41231,-0.72871 -0.081,5.18194 14.41231,0.72871 14.41231,0.72871 z"
4386+ id="path9595-5"
4387+ inkscape:connector-curvature="0"
4388+ sodipodi:nodetypes="czc" />
4389+ </g>
4390+ <g
4391+ transform="translate(0,0.51071554)"
4392+ id="g9764">
4393+ <rect
4394+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4395+ id="rect9022-7-5"
4396+ width="5.1752357"
4397+ height="5.5057745"
4398+ x="-240.11348"
4399+ y="1004.1906"
4400+ transform="scale(-1,1)" />
4401+ <path
4402+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4403+ d="m 261.84687,1006.8289 -23.17593,0.229"
4404+ id="path9237-4"
4405+ inkscape:connector-curvature="0"
4406+ sodipodi:nodetypes="cc" />
4407+ <path
4408+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
4409+ d="m 237.52588,1005.0054 0,-7.78637"
4410+ id="path9239-4"
4411+ inkscape:connector-curvature="0" />
4412+ <circle
4413+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-end:url(#marker9243-9);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
4414+ id="path9241-9"
4415+ cx="-237.52586"
4416+ cy="996.41754"
4417+ r="1.2954898"
4418+ transform="scale(-1,1)" />
4419+ <path
4420+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
4421+ d="m 236.94459,996.17473 c 0,0 -14.33135,-5.9107 -14.41232,-0.7287 -0.081,5.18187 14.41232,0.7287 14.41232,0.7287 z"
4422+ id="path9595-59"
4423+ inkscape:connector-curvature="0"
4424+ sodipodi:nodetypes="czc" />
4425+ <path
4426+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
4427+ d="m 238.21142,996.17473 c 0,0 14.33134,-5.9107 14.41231,-0.7287 0.081,5.18187 -14.41231,0.7287 -14.41231,0.7287 z"
4428+ id="path9595-5-0"
4429+ inkscape:connector-curvature="0"
4430+ sodipodi:nodetypes="czc" />
4431+ </g>
4432+ <path
4433+ inkscape:connector-curvature="0"
4434+ id="path9772"
4435+ d="m 257.53179,1008.2639 -3.72453,8.5826"
4436+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
4437+ <path
4438+ inkscape:connector-curvature="0"
4439+ id="path9772-8"
4440+ d="m 267.80726,1008.2639 3.72453,8.5826"
4441+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
4442+ </g>
4443+ </g>
4444+</svg>
4445
4446=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md'
4447--- md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md 1970-01-01 00:00:00 +0000
4448+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md 2016-01-19 00:21:38 +0000
4449@@ -0,0 +1,233 @@
4450+# Snapcraft: Advanced features
4451+
4452+Once you have built [your first snap](your-first-snap.md), you will probably
4453+want to learn more about snapcraft's more advanced features. Having a look at
4454+our selection of examples is a good idea, as we want it to be a good showcase
4455+of what is possible and generally relevant.
4456+
4457+## Examples
4458+
4459+Our showcase can be found in the actual source of `snapcraft` itself. Check
4460+it out by simply running:
4461+
4462+ git clone https://github.com/ubuntu-core/snapcraft
4463+ cd snapcraft/examples
4464+
4465+Inspecting the source locally will make easier to build the examples and
4466+play around with them. (You can
4467+[view them online](https://github.com/ubuntu-core/snapcraft/tree/master/examples)
4468+as well.)
4469+
4470+### Playing around with the examples
4471+
4472+If you just cloned the `snapcraft` source and inspect the examples, you
4473+can start off your explorations by reading the accompanying `snapcraft.yaml`
4474+file and running:
4475+
4476+ ../../bin/snapcraft snap
4477+
4478+This will inform you of all the steps taken during the creation of the snap.
4479+
4480+## Defining your parts
4481+
4482+Once you have noted down all the general information about your snap
4483+(like description, summary information and everything else), naming
4484+the individual parts will define the stucture of your `snapcraft.yaml` file.
4485+Think of parts as individual components of your snap: Where do you pull them
4486+from? How are they built?
4487+
4488+### Prerequisites during the build
4489+
4490+The example named `downloader-with-wiki-parts` shows how very easy you can
4491+make sure that the relevant build dependencies are installed:
4492+
4493+ build-packages: [libssl-dev]
4494+
4495+The above will install the `libssl-dev` package from the Ubuntu archive before
4496+an attempted build. If you need a specific version of libssl-dev or a custom
4497+build, you will need to specify a separate part.
4498+
4499+Also note that the above will not define which libraries are shipped with the
4500+app. It merely makes sure you have all the relevant build tools installed.
4501+
4502+
4503+### Pulling and building
4504+
4505+If you just intend to pull and build the source, take a look at the `gopaste`
4506+example. In its `snapcraft.yaml` file you can find just this one `parts`
4507+paragraph:
4508+
4509+ parts:
4510+ gopaste:
4511+ plugin: go
4512+ source: git://github.com/wisnij/gopaste/gopasted
4513+
4514+It starts off with the name of the specific part (`gopaste` here), the origin
4515+of the part (it's a `git` URL) and how to build it (plugin: `go`).
4516+Other possible scenarios would be Bazaar or Mercurial branches, or local
4517+directories.
4518+
4519+### Mixing and matching plugins
4520+
4521+An interesting example is `py2-project` because it defines two parts
4522+`spongeshaker` using the `python2` plugin, and `make-project` using the
4523+`make` plugin.
4524+
4525+ parts:
4526+ spongeshaker:
4527+ plugin: python2
4528+ source: git://github.com/markokr/spongeshaker.git
4529+ make-project:
4530+ plugin: make
4531+ source: .
4532+
4533+The example above mixes and matches parts of different origin. Locally it
4534+provides a binary we intend to ship (the `sha3sum.py` script) and a
4535+`Makefile` (to install our script in the right place).
4536+
4537+`spongeshaker` is a python library we will need to pull from git, build as
4538+a python project and bundle along with our script.
4539+
4540+A possible use-case for the above would be if you just intend to ship a small
4541+binary which you maintain, but require a library you need to build from git
4542+as opposed to simply including it from the Ubuntu archives.
4543+
4544+What's happening during the `snapcraft` run is:
4545+
4546+1. Get `spongeshaker` from `git`.
4547+1. Build it as a python project (which will include installing the python
4548+ library in the right place).
4549+1. Running `make` (from the local `Makefile`) and thus installing our
4550+ `sha3sum.py` script in the right place.
4551+
4552+### Putting your parts in order
4553+
4554+If your app is comprised of multiple parts, it might be necessary to build
4555+and stage parts in a particular order. This can be done by using the `after`
4556+keyword:
4557+
4558+ parts:
4559+ pipelinetest:
4560+ plugin: make
4561+ source: lp:~mterry/+junk/pipelinetest
4562+ after:
4563+ - libpipeline
4564+ libpipeline:
4565+ plugin: autotools
4566+ source: lp:~mterry/libpipeline/printf
4567+
4568+
4569+In the case of the `libpipeline` example above, the part named `pipelinetest`
4570+will be built after `libpipeline`. Especially if you need specific
4571+functionality during a build or as part of checks during the `stage` phase,
4572+this will be handy.
4573+
4574+### Re-using parts
4575+
4576+With snapcraft we want to make it easy to learn from other app vendors and
4577+re-use parts which have worked well for them.
4578+
4579+In the `downloader-with-wiki-parts` example, you can see that the `main`
4580+part is built:
4581+
4582+ after:
4583+ - curl
4584+
4585+As we never define the `curl` part in the above example, `snapcraft` will
4586+check the Ubuntu Wiki, which is where we currently host examples of
4587+successful snapcraft parts. The build order in this case would be `curl`,
4588+then `main`.
4589+
4590+
4591+## Finishing steps
4592+
4593+### Individual files
4594+
4595+If you are planning to provide binaries and services to the users of your
4596+apps, you need to specify them in your definition first. It's just a matter
4597+of enumerating them.
4598+
4599+The `godd` example has one binary:
4600+
4601+ binaries:
4602+ godd:
4603+ exec: ./bin/godd
4604+
4605+The above will take care of making the script executable, adding it to the
4606+user's path and install it in the right place.
4607+
4608+For a simple service we can take a look at `gopaste`:
4609+
4610+ services:
4611+ gopaste:
4612+ description: "gopaste"
4613+ start: bin/gopasted
4614+
4615+You define a name for the service, describe it (so log messages are more
4616+descriptive), declare how to run the service and that's it. For more
4617+thoughts on services and their security, visit the
4618+[snappy policy](https://developer.ubuntu.com/en/snappy/guides/security-policy/).
4619+
4620+
4621+### Limiting the number of installed files
4622+
4623+To check the list of files included in your snap, you can use `unsquashfs -l`
4624+on the resulting `.snap` file. If you find that certain files should not be
4625+shipped to the user (download size being just one factor), you can
4626+explicitly tell `snapcraft` which files to snap:
4627+
4628+ snap:
4629+ - usr/lib/x86_64-linux-gnu/libgudev-1.0.so*
4630+ - usr/lib/x86_64-linux-gnu/libobject-2.0.so*
4631+ - usr/lib/x86_64-linux-gnu/libglib-2.0.so*
4632+ - bin/godd*
4633+
4634+Here `godd` further defines the list of files to be placed in the app
4635+during the `snap` phase. As you can see above, globs (using asterisks as
4636+wildcard characters) are a good way of handling complexities within the
4637+directory structure.
4638+
4639+In the `webcam-webui` example you can see the following part called `cam`:
4640+
4641+ cam:
4642+ plugin: go
4643+ go-packages:
4644+ - github.com/mikix/golang-static-http
4645+ stage-packages:
4646+ - fswebcam
4647+ filesets:
4648+ fswebcam:
4649+ - usr/bin/fswebcam
4650+ - lib
4651+ - usr/lib
4652+ go-server:
4653+ - bin/golang-*
4654+ stage:
4655+ - $fswebcam
4656+ - $go-server
4657+ snap:
4658+ - $fswebcam
4659+ - $go-server
4660+ - -usr/share/doc
4661+
4662+In the `stage` definition you can see how named filesets are re-used
4663+(`$fswebcam` and `$go-server`).
4664+
4665+Another feature used in the `snap` definition is an exclude (`-usr/share/doc`
4666+in this case), meaning that files in these directories will not be installed.
4667+
4668+
4669+### node.js
4670+
4671+Snapping node.js apps has never been this easy. Take a look at the `shout`
4672+example and see how short and sweet it is. To bundle node packages, you simply
4673+do something like:
4674+
4675+ parts:
4676+ shout:
4677+ plugin: nodejs
4678+ node-packages:
4679+ - shout
4680+
4681+`node-packages` simply lists which packages (including their dependencies) to
4682+add to the snap.
4683
4684=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md'
4685--- md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md 1970-01-01 00:00:00 +0000
4686+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md 2016-01-19 00:21:38 +0000
4687@@ -0,0 +1,130 @@
4688+# Snapcraft parts
4689+
4690+Parts are the main building block to create snaps using Snapcraft. Parts have
4691+their own private space and lifecycle. Each part uses a `plugin`, which tells
4692+the part how to behave and what to do with the information inside it.
4693+
4694+As seen in the [article about snapcraft.yaml syntax](snapcraft-syntax.md)
4695+parts have general keywords that apply to all of them. In one case, you may
4696+want to enhance your part's functionality using `stage-packages` which end up
4697+bringing Ubuntu deb-based packages into your part, `filesets` to declare
4698+inclusion and exclusion sets, `organize` to make the artifact output for your
4699+part neater, `stage` and `snap` to make certain only the right set of files is
4700+seen at each step (making use of `filesets` or not). An example integrating
4701+these concepts for a part called `example-part` using a hypothetical plugin
4702+called `sample` would look like:
4703+
4704+ parts:
4705+ example-part:
4706+ type: sample
4707+ stage-packages:
4708+ - gpg
4709+ - wget
4710+ organize:
4711+ opt/bin: bin
4712+ filesets:
4713+ binaries:
4714+ - bin/*
4715+ - usr/bin/*
4716+ headers:
4717+ - *.h
4718+ - -include
4719+ stage:
4720+ - $binaries
4721+ - test/bin/test_app
4722+ - $headers
4723+ snap:
4724+ - $binaries
4725+
4726+In this example, imagine that the `sample` plugin actually builds something in
4727+its private *build* location using its private *source* directory as a base,
4728+and that it *installs* the usual set of files from its private install
4729+directory.
4730+
4731+This `sample` plugin makes use of `stage-packages`, these packages will be
4732+fetched from the Ubuntu deb archive using the *series* (release, i.e.; trusty,
4733+vivid, wily, ...) that is being used on the host. In this case, the part will
4734+be enhanced by the *gpg* and *wget* deb packages and its necessary
4735+dependencies to work isolated inside the part.
4736+
4737+When reaching the *stage* phase, the components in the private part's
4738+*install* directory will be exposed there, but since we used the organize
4739+keyword the contents in the install directory will be exposed to other parts
4740+in a cleaner form if desired or required; it is important to notice that in
4741+the event of using `filesets` they will follow the organized files and not
4742+the internal layout.
4743+
4744+The concept of `filesets` basically allows the creation of sets named after
4745+the keywords defined within, in this case *binaries* and *headers*, these are
4746+not necessarily needed but allow for variable expansion in the common
4747+targets: `stage` and `snap`. An inclusion is defined by just listing the
4748+target file, it can be globbed with `*` and a file can be explicitly
4749+excluded by prepending a `-` (when using `*` at the beginning of a path it
4750+needs to be quoted).
4751+
4752+The `stage` keyword will replace *$binaries* with all the *binaries* defined
4753+in `filesets`, but it also adds *test/bin/test_app* to the `stage` file set;
4754+*$headers* will basically *include* all the header files except those that
4755+live in *include* as it has a `-` in front of it. These are the files that
4756+will make it to the *stage* directory.
4757+
4758+The behavior for `snap` is identical to `stage` with the exception of applying
4759+this in the snap directory, which is the final layout for the snap, this is
4760+where everything should look clean and crisp for a good quality snap.
4761+
4762+
4763+## Snapcraft for Python with PIP
4764+
4765+Snapcraft includes support for Python 2.x and Python 3.x parts; here's how a
4766+`snapcraft.yaml` parts section will look like:
4767+
4768+ parts:
4769+ spongeshaker:
4770+ plugin: python3
4771+ source: git://github.com/markokr/spongeshaker.git
4772+
4773+A Python part will typically make sure required Python packages are installed
4774+on the build host and embed the following pieces in your snap:
4775+
4776+ * latest Python runtime from the latest Ubuntu packages of your current
4777+ Ubuntu release
4778+ * latest PIP for this Python version as downloaded from PyPy
4779+ * latest versions of your PIP requirements
4780+
4781+The proper `PYTHONPATH` environment variable will also be set in the wrapper
4782+scripts generated by snapcraft or when running your app locally.
4783+
4784+Python parts support standard snapcraft options and the requirements option
4785+to point PIP at its requirements file.
4786+
4787+Why embed a Python runtime? While Snappy does currently include a Python
4788+runtime, this might not be the one you need, and it might be updated to a
4789+different version or removed in a Snappy update. This is why applications
4790+using Python should embed their copy of the Python runtime.
4791+
4792+
4793+## Snapcraft for Java, Maven or Ant
4794+
4795+Snapcraft includes support for building parts with Apache Maven or Ant;
4796+here's how a snapcraft.yaml parts section will look like:
4797+
4798+ parts:
4799+ webapp:
4800+ plugin: maven
4801+ source: git://github.com/lool/snappy-mvn-demo.git
4802+
4803+A Maven part will typically:
4804+
4805+ * make sure the tool is installed on the build host
4806+ * embed a Java runtime in your snap
4807+ * run `mvn package` and copy the resulting `*.jar` and `*.war` files in
4808+ your snaps `jar/` and `war/` directories
4809+
4810+An Ant part works similarly, except it runs ant and sets the proper
4811+`CLASSPATH` environment variable in the wrapper scripts generated by
4812+snapcraft or when running the app locally.
4813+
4814+If you only need to embed a Java runtime, add a part with the jdk type. This
4815+will pull a relocatable OpenJDK via the default-jdk Ubuntu package and will
4816+set the proper `JAVA_HOME` and `PATH` environment variables in wrapper
4817+scripts generated by snapcraft or when running the app locally.
4818
4819=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md'
4820--- md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md 1970-01-01 00:00:00 +0000
4821+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md 2016-01-19 00:21:38 +0000
4822@@ -0,0 +1,83 @@
4823+# Syntax of the snapcraft.yaml file
4824+
4825+The `snapcraft.yaml` file is the main entry point to create a snap through
4826+Snapcraft. The main building blocks are parts and each defined part is
4827+independent from each other. In addition to parts, there are attributes
4828+that define the metadata for the snap package.
4829+
4830+What follows is a list of all the attributes the `snapcraft.yaml` file can
4831+contain.
4832+
4833+* `name` (string)
4834+ The name of the resulting snap.
4835+* `version` (string)
4836+ The version of the resulting snap.
4837+* `summary` (string)
4838+ A 78 character long summary for the snap.
4839+* `description` (string)
4840+ The description for the snap, this can and is expected to be a longer
4841+ explanation for the snap.
4842+* `config` (string)
4843+ Path to the runnable in snap that will be used as the [Snappy config
4844+ interface](https://developer.ubuntu.com/snappy/guides/config-command/).
4845+* `services` (yaml subsection)
4846+ A set of keys representing service names with values as defined by the
4847+ [Snappy packaging spec](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
4848+* `binaries` (yaml subsection)
4849+ A set of keys representing binary names with values as defined by the
4850+ [Snappy packaging spec](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
4851+* `icon` (string)
4852+ Path to the icon that will be used for the snap.
4853+* `license` (yaml subsection)
4854+ License the snap will carry.
4855+ * `text` (string)
4856+ The license text for the snap itself.
4857+ * `accept-required` (boolean)
4858+ If true, license acceptance is required for the package to be activated.
4859+ A good example for this one is the Sun JRE/JDK being bundled in a snap.
4860+* `framework-policy` (string)
4861+ A relative path to a directory containing additional policies, used if
4862+ creating a framework and want to extend permissions to snap apps.
4863+* `parts` (yaml subsection)
4864+ A map of part names to their own part configuration. Order in the file is
4865+ not relevant (to aid copy-and-pasting).
4866+ * `plugin` (string)
4867+ Specifies the plugin name that will manage this part. Snapcraft will pass
4868+ to it all the other user-specified part options. If plugin is not
4869+ defined, [the wiki](https://wiki.ubuntu.com/Snappy/Parts) will be
4870+ searched for the part, the local values defined in the part will be used
4871+ to compose the final part.
4872+ * `after` (list of strings)
4873+ Specifies any parts that should be built before this part is. This is
4874+ mostly useful when a part needs a library or build tool built by another
4875+ part. If the part defined in after is not defined locally, the part will
4876+ be searched for in [the wiki](https://wiki.ubuntu.com/Snappy/Parts).
4877+ *If a part is supposed to run after another, the prerequisite part will
4878+ be staged before the dependent part starts its lifecycle.*
4879+ * `stage-packages` (list of strings)
4880+ A list of Ubuntu packages to use that would support the part creation.
4881+ * `filesets` (yaml subsection)
4882+ A dictionary with filesets, the key being a recognizable user defined
4883+ string and its value a list of strings of files to be included or
4884+ excluded. Globbing is achieved with `*` for either inclusions or
4885+ exclusion. Exclusions are denoted by a `-`. Globbing is computed from
4886+ the private sections of the part.
4887+ * `organize` (yaml subsection)
4888+ A dictionary exposing replacements, the key is the internal name whilst
4889+ the value the exposed name, filesets will refer to the exposed named
4890+ applied after organization is applied.
4891+ * `stage` (list of strings)
4892+ A list of files from a part's installation to expose in stage. Rules
4893+ applying to the list here are the same as those of filesets. Referencing
4894+ of fileset keys is done with a $ prefixing the fileset key, which will
4895+ expand with the value of such key.
4896+ * `snap` (list of strings)
4897+ A list of files from a part's installation to expose in snap. Rules
4898+ applying to the list here are the same as those of filesets. Referencing
4899+ of fileset keys is done with a `$` prefixing the fileset key, which will
4900+ expand with the value of such key.
4901+
4902+The `snapcraft.yaml` in any project is validated to be compliant to these
4903+keywords, if there is any missing expected component or invalid value,
4904+`snapcraft` will exit with an error.
4905+
4906
4907=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md'
4908--- md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md 1970-01-01 00:00:00 +0000
4909+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md 2016-01-19 00:21:38 +0000
4910@@ -0,0 +1,43 @@
4911+# Trying snapcraft
4912+
4913+In the example directory, you can look at each individual `snapcraft.yaml`
4914+to see how each project is composed. Within each of these directories try
4915+running `snapcraft` to build a snap for each of these or go through the
4916+lifecycle by running:
4917+
4918+ $ snapcraft pull
4919+ $ snapcraft build
4920+ $ snapcraft stage
4921+ $ snapcraft strip
4922+ $ snapcraft snap
4923+
4924+That sequence of commands basically went through the lifecycle of all the
4925+defined parts. To quickly inspect the parts a Snapcraft project has, open
4926+the `snapcraft.yaml` file of the corresponding example and look at the keys
4927+inside the parts entry.
4928+
4929+
4930+## Sideloading your snap
4931+
4932+Consider the `downloader-with-wiki-parts` example and a Snappy Ubuntu Core
4933+on 192.168.10.10, to install the built snap by following Trying snapcraft
4934+and run:
4935+
4936+ snappy-remote --url ssh://192.168.10.10 install downloader_1.0_amd64.snap
4937+
4938+If this is the first time connecting to the system, snappy-remote will try
4939+and use existing ssh keys for the user to avoid the necessity of password
4940+prompts.
4941+
4942+After installing a summary of installed snaps will be presented, on vanilla
4943+x86-64 bit system it would look a lot like this:
4944+
4945+ Name Date Version Developer
4946+ ubuntu-core 2015-09-17 5 ubuntu
4947+ downloader 2015-10-01 ICIEPfXHQOaC sideload
4948+ generic-amd64 2015-10-01 1.4 canonical
4949+
4950+Take notice of the sideload word in the downloader snap, this indicates that
4951+the snap did not come signed from the store, if an app is sideloaded, it
4952+also fakes the version to allow easy iteration without the need to change the
4953+metadata.
4954
4955=== added file 'md_importer/tests/data/snapcraft-test/docs/your-first-snap.md'
4956--- md_importer/tests/data/snapcraft-test/docs/your-first-snap.md 1970-01-01 00:00:00 +0000
4957+++ md_importer/tests/data/snapcraft-test/docs/your-first-snap.md 2016-01-19 00:21:38 +0000
4958@@ -0,0 +1,318 @@
4959+# Snapcraft Tutorial
4960+
4961+Let's make a snap from scratch using Snapcraft! We'll pick something a little
4962+interesting: a webcam server.
4963+
4964+## Preparation
4965+
4966+You'll want a webcam and a Snappy device. We'll assume you have those,
4967+but if you need help setting up a Snappy install, there is help
4968+[online](https://developer.ubuntu.com/en/snappy/start/).
4969+
4970+(Even if you don't have either of those, you can still follow along. You just
4971+won't be able to use the final snap package you create. But you'll get to see
4972+how Snapcraft works, which is still super rewarding.)
4973+
4974+## Approach
4975+
4976+This example is easy because we won't be doing much of the heavy lifting
4977+ourselves. We're going to integrate a couple pieces of code together to make
4978+an interesting app.
4979+
4980+Namely, we'll combine a web server with a webcam program and combine
4981+them to serve a new frame every ten seconds.
4982+
4983+> The resulting package is also part of the examples directory in the
4984+> [snapcraft sources](https://github.com/ubuntu-core/snapcraft/tree/master/examples/webcam-webui)
4985+
4986+### The Web Server
4987+
4988+Go has a simple web server in its standard libraries. So let's just use that.
4989+
4990+It's trivial to write a complete (but basic) web server in a few lines:
4991+
4992+ package main
4993+ import "net/http"
4994+ func main() {
4995+ panic(http.ListenAndServe(":8080", http.FileServer(http.Dir("."))))
4996+ }
4997+
4998+This will serve the current directory on port `:8080`. If there is an
4999+`index.html` in the current directory, it will be served. Otherwise a
5000+directory listing will be shown.
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches