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
=== modified file 'Makefile'
--- Makefile 2016-01-12 14:24:40 +0000
+++ Makefile 2016-01-19 00:21:38 +0000
@@ -46,7 +46,7 @@
4646
47syncdb:47syncdb:
48 @echo "Syncing database"48 @echo "Syncing database"
49 @python manage.py syncdb --noinput --migrate --settings charm_settings49 @python manage.py migrate --noinput --settings charm_settings
5050
51collectstatic: collectstatic.done51collectstatic: collectstatic.done
52collectstatic.done:52collectstatic.done:
@@ -61,18 +61,18 @@
61update-pip-cache:61update-pip-cache:
62 @echo "Updating pip-cache"62 @echo "Updating pip-cache"
63 rm -rf pip-cache63 rm -rf pip-cache
64 bzr branch lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies pip-cache64 bzr branch lp:developer-ubuntu-com/dependencies pip-cache
65 pip install --exists-action=w --download pip-cache/ -r requirements.txt65 pip install --exists-action=w --download pip-cache/ -r requirements.txt
66 bzr add pip-cache/* 66 bzr add pip-cache/*
67 bzr commit pip-cache/ -m 'automatically updated devportal requirements'67 bzr commit pip-cache/ -m 'automatically updated devportal requirements'
68 bzr push --directory pip-cache lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies68 bzr push --directory pip-cache lp:developer-ubuntu-com/dependencies
69 bzr revno pip-cache > pip-cache-revno.txt69 bzr revno pip-cache > pip-cache-revno.txt
70 rm -rf pip-cache70 rm -rf pip-cache
71 @echo "** Remember to commit pip-cache-revno.txt"71 @echo "** Remember to commit pip-cache-revno.txt"
7272
73pip-cache:73pip-cache:
74 @echo "Downloading pip-cache"74 @echo "Downloading pip-cache"
75 @bzr branch -r `cat pip-cache-revno.txt` lp:~developer-ubuntu-com-dev/developer-ubuntu-com/dependencies pip-cache75 @bzr branch -r `cat pip-cache-revno.txt` lp:developer-ubuntu-com/dependencies pip-cache
7676
77env: pip-cache77env: pip-cache
78 @echo "Creating virtualenv"78 @echo "Creating virtualenv"
@@ -82,7 +82,7 @@
8282
83db.sqlite3: env83db.sqlite3: env
84 @echo "Initializing database"84 @echo "Initializing database"
85 @./env/bin/python manage.py syncdb --noinput --migrate85 @./env/bin/python manage.py migrate --noinput
86 @./env/bin/python manage.py initdb86 @./env/bin/python manage.py initdb
87 @./env/bin/python manage.py init_apidocs87 @./env/bin/python manage.py init_apidocs
8888
8989
=== modified file 'README.md'
--- README.md 2015-12-08 10:25:29 +0000
+++ README.md 2016-01-19 00:21:38 +0000
@@ -27,7 +27,7 @@
27 ./env/bin/python manage.py flush --noinput27 ./env/bin/python manage.py flush --noinput
28 echo "delete from auth_permission;" | ./env/bin/python manage.py dbshell28 echo "delete from auth_permission;" | ./env/bin/python manage.py dbshell
29 ./env/bin/python manage.py loaddata ../dbbackup/dbdump.json29 ./env/bin/python manage.py loaddata ../dbbackup/dbdump.json
30 ./env/bin/python manage.py syncdb --noinput --all30 ./env/bin/python manage.py migrate --noinput
31 ./env/bin/python manage.py initdb31 ./env/bin/python manage.py initdb
3232
33# Managing translations33# Managing translations
3434
=== modified file 'api_docs/migrations/0001_initial.py'
--- api_docs/migrations/0001_initial.py 2015-12-08 10:25:29 +0000
+++ api_docs/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
@@ -1,186 +1,139 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'Topic'
12 db.create_table(u'api_docs_topic', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
15 ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
16 ))
17 db.send_create_signal(u'api_docs', ['Topic'])
18
19 # Adding model 'Language'
20 db.create_table(u'api_docs_language', (
21 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
22 ('topic', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Topic'])),
23 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
24 ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
25 ('current_version', self.gf('django.db.models.fields.related.ForeignKey')(related_name='current_for_lang', null=True, to=orm['api_docs.Version'])),
26 ('development_version', self.gf('django.db.models.fields.related.ForeignKey')(related_name='development_for_lang', null=True, to=orm['api_docs.Version'])),
27 ))
28 db.send_create_signal(u'api_docs', ['Language'])
29
30 # Adding model 'Version'
31 db.create_table(u'api_docs_version', (
32 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
33 ('language', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Language'], null=True)),
34 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
35 ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
36 ))
37 db.send_create_signal(u'api_docs', ['Version'])
38
39 # Adding model 'Section'
40 db.create_table(u'api_docs_section', (
41 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
42 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
43 ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
44 ('topic_version', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Version'])),
45 ))
46 db.send_create_signal(u'api_docs', ['Section'])
47
48 # Adding model 'Namespace'
49 db.create_table(u'api_docs_namespace', (
50 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
51 ('platform_section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
52 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
53 ('display_name', self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True)),
54 ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
55 ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
56 ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
57 ))
58 db.send_create_signal(u'api_docs', ['Namespace'])
59
60 # Adding model 'Element'
61 db.create_table(u'api_docs_element', (
62 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
63 ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
64 ('description', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
65 ('namespace', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Namespace'], null=True, blank=True)),
66 ('section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
67 ('fullname', self.gf('django.db.models.fields.CharField')(max_length=128)),
68 ('keywords', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
69 ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
70 ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
71 ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
72 ))
73 db.send_create_signal(u'api_docs', ['Element'])
74
75 # Adding model 'Page'
76 db.create_table(u'api_docs_page', (
77 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
78 ('slug', self.gf('django.db.models.fields.CharField')(max_length=64)),
79 ('title', self.gf('django.db.models.fields.CharField')(max_length=64)),
80 ('description', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
81 ('namespace', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Namespace'], null=True, blank=True)),
82 ('section', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api_docs.Section'])),
83 ('fullname', self.gf('django.db.models.fields.CharField')(max_length=128)),
84 ('keywords', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
85 ('data', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
86 ('source_file', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
87 ('source_format', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
88 ('order_index', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, blank=True)),
89 ))
90 db.send_create_signal(u'api_docs', ['Page'])
91
92
93 def backwards(self, orm):
94 # Deleting model 'Topic'
95 db.delete_table(u'api_docs_topic')
96
97 # Deleting model 'Language'
98 db.delete_table(u'api_docs_language')
99
100 # Deleting model 'Version'
101 db.delete_table(u'api_docs_version')
102
103 # Deleting model 'Section'
104 db.delete_table(u'api_docs_section')
105
106 # Deleting model 'Namespace'
107 db.delete_table(u'api_docs_namespace')
108
109 # Deleting model 'Element'
110 db.delete_table(u'api_docs_element')
111
112 # Deleting model 'Page'
113 db.delete_table(u'api_docs_page')
114
115
116 models = {
117 u'api_docs.element': {
118 'Meta': {'ordering': "('name',)", 'object_name': 'Element'},
119 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
120 'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
121 'fullname': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
122 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123 'keywords': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
124 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
125 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Namespace']", 'null': 'True', 'blank': 'True'}),
126 'section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
127 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
128 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'})
129 },
130 u'api_docs.language': {
131 'Meta': {'object_name': 'Language'},
132 'current_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'current_for_lang'", 'null': 'True', 'to': u"orm['api_docs.Version']"}),
133 'development_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'development_for_lang'", 'null': 'True', 'to': u"orm['api_docs.Version']"}),
134 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
136 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
137 'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Topic']"})
138 },
139 u'api_docs.namespace': {
140 'Meta': {'ordering': "('name',)", 'object_name': 'Namespace'},
141 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
142 'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
143 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
145 'platform_section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
146 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
147 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'})
148 },
149 u'api_docs.page': {
150 'Meta': {'ordering': "('order_index',)", 'object_name': 'Page'},
151 'data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
152 'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
153 'fullname': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
154 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
155 'keywords': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
156 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Namespace']", 'null': 'True', 'blank': 'True'}),
157 'order_index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
158 'section': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Section']"}),
159 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
160 'source_file': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
161 'source_format': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
162 'title': ('django.db.models.fields.CharField', [], {'max_length': '64'})
163 },
164 u'api_docs.section': {
165 'Meta': {'object_name': 'Section'},
166 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
167 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
168 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
169 'topic_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Version']"})
170 },
171 u'api_docs.topic': {
172 'Meta': {'object_name': 'Topic'},
173 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
174 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
175 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'})
176 },
177 u'api_docs.version': {
178 'Meta': {'object_name': 'Version'},
179 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
180 'language': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api_docs.Language']", 'null': 'True'}),
181 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
182 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64'})
183 }
184 }
185
186 complete_apps = ['api_docs']
187\ No newline at end of file2\ No newline at end of file
3from __future__ import unicode_literals
4
5from django.db import migrations, models
6
7
8class Migration(migrations.Migration):
9
10 dependencies = [
11 ]
12
13 operations = [
14 migrations.CreateModel(
15 name='Element',
16 fields=[
17 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 ('name', models.CharField(max_length=64)),
19 ('description', models.CharField(default=b'', max_length=256, blank=True)),
20 ('fullname', models.CharField(max_length=128)),
21 ('keywords', models.CharField(default=b'', max_length=256, blank=True)),
22 ('data', models.TextField(default=b'', blank=True)),
23 ('source_file', models.CharField(max_length=128, null=True, blank=True)),
24 ('source_format', models.CharField(max_length=32, null=True, blank=True)),
25 ],
26 options={
27 'ordering': ('name',),
28 'verbose_name': 'Rendered Element',
29 'verbose_name_plural': 'Rendered Elements',
30 },
31 ),
32 migrations.CreateModel(
33 name='Language',
34 fields=[
35 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
36 ('name', models.CharField(max_length=64)),
37 ('slug', models.CharField(max_length=64)),
38 ],
39 ),
40 migrations.CreateModel(
41 name='Namespace',
42 fields=[
43 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
44 ('name', models.CharField(max_length=64)),
45 ('display_name', models.CharField(default=b'', max_length=64, blank=True)),
46 ('data', models.TextField(default=b'', blank=True)),
47 ('source_file', models.CharField(max_length=128, null=True, blank=True)),
48 ('source_format', models.CharField(max_length=32, null=True, blank=True)),
49 ],
50 options={
51 'ordering': ('name',),
52 },
53 ),
54 migrations.CreateModel(
55 name='Page',
56 fields=[
57 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
58 ('slug', models.CharField(max_length=64)),
59 ('title', models.CharField(max_length=64)),
60 ('description', models.CharField(default=b'', max_length=256, blank=True)),
61 ('fullname', models.CharField(max_length=128)),
62 ('keywords', models.CharField(default=b'', max_length=256, blank=True)),
63 ('data', models.TextField(default=b'', blank=True)),
64 ('source_file', models.CharField(max_length=128, null=True, blank=True)),
65 ('source_format', models.CharField(max_length=32, null=True, blank=True)),
66 ('order_index', models.PositiveIntegerField(default=0, blank=True)),
67 ('namespace', models.ForeignKey(blank=True, to='api_docs.Namespace', null=True)),
68 ],
69 options={
70 'ordering': ('order_index',),
71 'verbose_name': 'Rendered Page',
72 'verbose_name_plural': 'Rendered Pages',
73 },
74 ),
75 migrations.CreateModel(
76 name='Section',
77 fields=[
78 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
79 ('name', models.CharField(max_length=64)),
80 ('description', models.TextField(null=True, blank=True)),
81 ],
82 ),
83 migrations.CreateModel(
84 name='Topic',
85 fields=[
86 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
87 ('name', models.CharField(max_length=64)),
88 ('slug', models.CharField(max_length=64)),
89 ],
90 ),
91 migrations.CreateModel(
92 name='Version',
93 fields=[
94 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
95 ('name', models.CharField(max_length=64)),
96 ('slug', models.CharField(max_length=64)),
97 ('language', models.ForeignKey(to='api_docs.Language', null=True)),
98 ],
99 ),
100 migrations.AddField(
101 model_name='section',
102 name='topic_version',
103 field=models.ForeignKey(to='api_docs.Version'),
104 ),
105 migrations.AddField(
106 model_name='page',
107 name='section',
108 field=models.ForeignKey(to='api_docs.Section'),
109 ),
110 migrations.AddField(
111 model_name='namespace',
112 name='platform_section',
113 field=models.ForeignKey(to='api_docs.Section'),
114 ),
115 migrations.AddField(
116 model_name='language',
117 name='current_version',
118 field=models.ForeignKey(related_name='current_for_lang', blank=True, to='api_docs.Version', null=True),
119 ),
120 migrations.AddField(
121 model_name='language',
122 name='development_version',
123 field=models.ForeignKey(related_name='development_for_lang', blank=True, to='api_docs.Version', null=True),
124 ),
125 migrations.AddField(
126 model_name='language',
127 name='topic',
128 field=models.ForeignKey(to='api_docs.Topic'),
129 ),
130 migrations.AddField(
131 model_name='element',
132 name='namespace',
133 field=models.ForeignKey(blank=True, to='api_docs.Namespace', null=True),
134 ),
135 migrations.AddField(
136 model_name='element',
137 name='section',
138 field=models.ForeignKey(to='api_docs.Section'),
139 ),
140 ]
188141
=== modified file 'developer_portal/admin.py'
--- developer_portal/admin.py 2015-12-08 10:25:29 +0000
+++ developer_portal/admin.py 2016-01-19 00:21:38 +0000
@@ -4,20 +4,12 @@
4from reversion.admin import VersionAdmin4from reversion.admin import VersionAdmin
55
6from cms.extensions import TitleExtensionAdmin6from cms.extensions import TitleExtensionAdmin
7from .models import ExternalDocsBranch, SEOExtension7from .models import SEOExtension
8from django.core.management import call_command
98
10__all__ = (9__all__ = (
11)10)
1211
1312
14def import_selected_external_docs_branches(modeladmin, request, queryset):
15 for branch in queryset:
16 call_command('import-external-docs-branches', branch.docs_namespace)
17 import_selected_external_docs_branches.short_description = \
18 "Import selected branches"
19
20
21class RevisionAdmin(admin.ModelAdmin):13class RevisionAdmin(admin.ModelAdmin):
22 list_display = ('date_created', 'user', 'comment')14 list_display = ('date_created', 'user', 'comment')
23 list_display_links = ('date_created', )15 list_display_links = ('date_created', )
@@ -35,13 +27,6 @@
35admin.site.register(Version, VersionAdmin)27admin.site.register(Version, VersionAdmin)
3628
3729
38class ExternalDocsBranchAdmin(admin.ModelAdmin):
39 list_display = ('lp_origin', 'docs_namespace')
40 list_filter = ('lp_origin', 'docs_namespace')
41 actions = [import_selected_external_docs_branches]
42
43admin.site.register(ExternalDocsBranch, ExternalDocsBranchAdmin)
44
45class SEOExtensionAdmin(TitleExtensionAdmin):30class SEOExtensionAdmin(TitleExtensionAdmin):
46 pass31 pass
4732
4833
=== modified file 'developer_portal/blog/views.py'
--- developer_portal/blog/views.py 2015-12-08 10:25:29 +0000
+++ developer_portal/blog/views.py 2016-01-19 00:21:38 +0000
@@ -35,7 +35,9 @@
35 return super(MultiLangEntryIndex, self).get(request, *args, **kwargs)35 return super(MultiLangEntryIndex, self).get(request, *args, **kwargs)
3636
37 def get_dated_queryset(self, ordering=None, **lookup):37 def get_dated_queryset(self, ordering=None, **lookup):
38 return super(MultiLangEntryIndex, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)38 if ordering:
39 return super(MultiLangEntryIndex, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
40 return super(MultiLangEntryIndex, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
3941
40class MultiLangEntryYear(MultiLangMixin, EntryYear):42class MultiLangEntryYear(MultiLangMixin, EntryYear):
41 def get(self, request, *args, **kwargs):43 def get(self, request, *args, **kwargs):
@@ -43,7 +45,9 @@
43 return super(MultiLangEntryYear, self).get(request, *args, **kwargs)45 return super(MultiLangEntryYear, self).get(request, *args, **kwargs)
4446
45 def get_dated_queryset(self, ordering=None, **lookup):47 def get_dated_queryset(self, ordering=None, **lookup):
46 return super(MultiLangEntryYear, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)48 if ordering:
49 return super(MultiLangEntryYear, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
50 return super(MultiLangEntryYear, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
4751
48class MultiLangEntryMonth(MultiLangMixin, EntryMonth):52class MultiLangEntryMonth(MultiLangMixin, EntryMonth):
49 def get(self, request, *args, **kwargs):53 def get(self, request, *args, **kwargs):
@@ -51,7 +55,9 @@
51 return super(MultiLangEntryMonth, self).get(request, *args, **kwargs)55 return super(MultiLangEntryMonth, self).get(request, *args, **kwargs)
5256
53 def get_dated_queryset(self, ordering=None, **lookup):57 def get_dated_queryset(self, ordering=None, **lookup):
54 return super(MultiLangEntryMonth, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)58 if ordering:
59 return super(MultiLangEntryMonth, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
60 return super(MultiLangEntryMonth, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
5561
56class MultiLangEntryWeek(MultiLangMixin, EntryWeek):62class MultiLangEntryWeek(MultiLangMixin, EntryWeek):
57 def get(self, request, *args, **kwargs):63 def get(self, request, *args, **kwargs):
@@ -59,7 +65,9 @@
59 return super(MultiLangEntryWeek, self).get(request, *args, **kwargs)65 return super(MultiLangEntryWeek, self).get(request, *args, **kwargs)
6066
61 def get_dated_queryset(self, ordering=None, **lookup):67 def get_dated_queryset(self, ordering=None, **lookup):
62 return super(MultiLangEntryWeek, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)68 if ordering:
69 return super(MultiLangEntryWeek, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
70 return super(MultiLangEntryWeek, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
6371
64class MultiLangEntryDay(MultiLangMixin, EntryDay):72class MultiLangEntryDay(MultiLangMixin, EntryDay):
65 def get(self, request, *args, **kwargs):73 def get(self, request, *args, **kwargs):
@@ -67,7 +75,9 @@
67 return super(MultiLangEntryDay, self).get(request, *args, **kwargs)75 return super(MultiLangEntryDay, self).get(request, *args, **kwargs)
6876
69 def get_dated_queryset(self, ordering=None, **lookup):77 def get_dated_queryset(self, ordering=None, **lookup):
70 return super(MultiLangEntryDay, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)78 if ordering:
79 return super(MultiLangEntryDay, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
80 return super(MultiLangEntryDay, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
7181
72class MultiLangEntryToday(MultiLangMixin, EntryToday):82class MultiLangEntryToday(MultiLangMixin, EntryToday):
73 def get(self, request, *args, **kwargs):83 def get(self, request, *args, **kwargs):
@@ -75,4 +85,6 @@
75 return super(MultiLangEntryToday, self).get(request, *args, **kwargs)85 return super(MultiLangEntryToday, self).get(request, *args, **kwargs)
7686
77 def get_dated_queryset(self, ordering=None, **lookup):87 def get_dated_queryset(self, ordering=None, **lookup):
78 return super(MultiLangEntryToday, self).get_dated_queryset(ordering, **lookup).filter(categories__slug=self.language)88 if ordering:
89 return super(MultiLangEntryToday, self).get_dated_queryset(**lookup).filter(categories__slug=self.language).order_by(ordering)
90 return super(MultiLangEntryToday, self).get_dated_queryset(**lookup).filter(categories__slug=self.language)
7991
=== modified file 'developer_portal/management/commands/initdb.py'
--- developer_portal/management/commands/initdb.py 2015-12-08 10:25:29 +0000
+++ developer_portal/management/commands/initdb.py 2016-01-19 00:21:38 +0000
@@ -1,17 +1,12 @@
1#!/usr/bin/python1#!/usr/bin/python
22
3from django.core.management.base import BaseCommand3from django.core.management.base import BaseCommand
4from optparse import make_option
5
6from django.conf import settings4from django.conf import settings
75
8import subprocess6from django.contrib.auth.models import User, Permission
9import os
10import sys
11
12from django.contrib.auth.models import User, Group, Permission
13from django.contrib.contenttypes.models import ContentType
14from cms.models.permissionmodels import PageUserGroup, GlobalPagePermission7from cms.models.permissionmodels import PageUserGroup, GlobalPagePermission
8from zinnia.models import Category
9
1510
16class Command(BaseCommand):11class Command(BaseCommand):
17 help = "Make sure the Developer Portal database is set up properly."12 help = "Make sure the Developer Portal database is set up properly."
@@ -20,24 +15,25 @@
2015
21 all_perms = Permission.objects.filter()16 all_perms = Permission.objects.filter()
2217
23 print "Creating admin user."18 print("Creating admin user.")
24 admin, created = User.objects.get_or_create(username='system')19 admin, created = User.objects.get_or_create(username='system')
25 admin.is_staff = True20 admin.is_staff = True
26 admin.is_superuser = True21 admin.is_superuser = True
27 admin.save()22 admin.save()
2823
29 if hasattr(settings, 'ADMIN_GROUP') and settings.ADMIN_GROUP != "":24 if hasattr(settings, 'ADMIN_GROUP') and settings.ADMIN_GROUP != "":
30 print "Configuring "+settings.ADMIN_GROUP+" group."25 print("Configuring {} group.".format(settings.ADMIN_GROUP))
31 admins, created = PageUserGroup.objects.get_or_create(name=settings.ADMIN_GROUP, defaults={'created_by': admin})26 admins, created = PageUserGroup.objects.get_or_create(
27 name=settings.ADMIN_GROUP, defaults={'created_by': admin})
32 admins.permissions.add(*list(all_perms))28 admins.permissions.add(*list(all_perms))
3329
34 print "Configuring global permissions for group."30 print("Configuring global permissions for group.")
35 adminperms, created = GlobalPagePermission.objects.get_or_create(31 adminperms, created = GlobalPagePermission.objects.get_or_create(
36 # who:32 # who:
37 group = admins,33 group=admins,
3834
39 # what:35 # what:
40 defaults = {36 defaults={
41 'can_change': True,37 'can_change': True,
42 'can_add': True,38 'can_add': True,
43 'can_delete': True,39 'can_delete': True,
@@ -51,18 +47,20 @@
51 adminperms.sites.add(settings.SITE_ID)47 adminperms.sites.add(settings.SITE_ID)
5248
53 if hasattr(settings, 'EDITOR_GROUP') and settings.EDITOR_GROUP != "":49 if hasattr(settings, 'EDITOR_GROUP') and settings.EDITOR_GROUP != "":
54 print "Configuring "+settings.EDITOR_GROUP+" group."50 print("Configuring {} group.".format(settings.EDITOR_GROUP))
55 editors, created = PageUserGroup.objects.get_or_create(name=settings.EDITOR_GROUP, defaults={'created_by': admin})51 editors, created = PageUserGroup.objects.get_or_create(
56 page_perms = Permission.objects.filter(content_type__app_label='cms', content_type__name='page')52 name=settings.EDITOR_GROUP, defaults={'created_by': admin})
53 page_perms = Permission.objects.filter(
54 content_type__app_label='cms', content_type__model='page')
57 editors.permissions.add(*list(page_perms))55 editors.permissions.add(*list(page_perms))
5856
59 print "Configuring global permissions for group."57 print("Configuring global permissions for group.")
60 editorsperms, created = GlobalPagePermission.objects.get_or_create(58 editorsperms, created = GlobalPagePermission.objects.get_or_create(
61 # who:59 # who:
62 group = editors,60 group=editors,
6361
64 # what:62 # what:
65 defaults = {63 defaults={
66 'can_change': True,64 'can_change': True,
67 'can_add': True,65 'can_add': True,
68 'can_delete': True,66 'can_delete': True,
@@ -74,3 +72,11 @@
74 }72 }
75 )73 )
76 editorsperms.sites.add(settings.SITE_ID)74 editorsperms.sites.add(settings.SITE_ID)
75
76 print('Adding zinnia categories for the following: {}.'.format(
77 ', '.join([a[0] for a in settings.LANGUAGES])))
78 for lang in settings.LANGUAGES:
79 if lang[1] == 'Simplified Chinese':
80 Category.objects.get_or_create(title='Chinese', slug=lang[0])
81 else:
82 Category.objects.get_or_create(title=lang[1], slug=lang[0])
7783
=== modified file 'developer_portal/management/commands/update-template.py'
--- developer_portal/management/commands/update-template.py 2015-07-09 07:23:54 +0000
+++ developer_portal/management/commands/update-template.py 2016-01-19 00:21:38 +0000
@@ -1,38 +1,35 @@
1#!/usr/bin/python1#!/usr/bin/python
22
3from django.core.management.base import NoArgsCommand3from django.core.management.base import NoArgsCommand
4from django.core.management import call_command
45
5import subprocess
6import os6import os
7import sys
87
9from django.conf import settings8from django.conf import settings
109
11APP_NAME = "developer_portal"10APP_NAME = 'developer_portal'
1211
13DUMMY_LOCALE = "xx"12DUMMY_LOCALE = 'xx'
1413
1514
16def update_template():15def update_template():
17 pwd = os.getcwd()16 call_command(
18 os.chdir(settings.PROJECT_PATH)17 'makemessages',
19 subprocess.call([sys.executable, "manage.py", "makemessages",18 '--keep-pot', '-i', 'env/*', '-i', 'urls.py',
20 "--keep-pot", "--all", "-i", "env/*", "-i", "urls.py",19 '-l', DUMMY_LOCALE)
21 "-l", DUMMY_LOCALE])20 project_locale_path = os.path.join(settings.PROJECT_PATH, 'locale')
22 project_locale_path = os.path.join(settings.PROJECT_PATH, "locale")
23 os.rename(os.path.join(project_locale_path,21 os.rename(os.path.join(project_locale_path,
24 "%s/LC_MESSAGES/django.po" % DUMMY_LOCALE),22 '%s/LC_MESSAGES/django.po' % DUMMY_LOCALE),
25 os.path.join(project_locale_path, "%s.pot" % APP_NAME))23 os.path.join(project_locale_path, '%s.pot' % APP_NAME))
26 os.removedirs(os.path.join(project_locale_path,24 os.removedirs(os.path.join(project_locale_path,
27 "%s/LC_MESSAGES" % DUMMY_LOCALE))25 '%s/LC_MESSAGES' % DUMMY_LOCALE))
28 old_pot_fn = os.path.join(project_locale_path, "django.pot")26 old_pot_fn = os.path.join(project_locale_path, 'django.pot')
29 if os.path.exists(old_pot_fn):27 if os.path.exists(old_pot_fn):
30 os.remove(old_pot_fn)28 os.remove(old_pot_fn)
31 os.chdir(pwd)
3229
3330
34class Command(NoArgsCommand):31class Command(NoArgsCommand):
35 help = "Update translations template."32 help = 'Update translations template.'
3633
37 def handle_noargs(self, **options):34 def handle_noargs(self, **options):
38 update_template()35 update_template()
3936
=== added file 'developer_portal/migrations/0001_initial.py'
--- developer_portal/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
+++ developer_portal/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,36 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import migrations, models
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ]
11
12 operations = [
13 migrations.CreateModel(
14 name='RawHtml',
15 fields=[
16 ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
17 ('body', models.TextField(verbose_name='body')),
18 ],
19 options={
20 'abstract': False,
21 },
22 bases=('cms.cmsplugin',),
23 ),
24 migrations.CreateModel(
25 name='SEOExtension',
26 fields=[
27 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
28 ('keywords', models.CharField(max_length=256)),
29 ('extended_object', models.OneToOneField(editable=False, to='cms.Title')),
30 ('public_extension', models.OneToOneField(related_name='draft_extension', null=True, editable=False, to='developer_portal.SEOExtension')),
31 ],
32 options={
33 'abstract': False,
34 },
35 ),
36 ]
037
=== removed file 'developer_portal/migrations/0001_initial.py'
--- developer_portal/migrations/0001_initial.py 2015-12-08 10:25:29 +0000
+++ developer_portal/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
@@ -1,20 +0,0 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 pass
12
13 def backwards(self, orm):
14 pass
15
16 models = {
17
18 }
19
20 complete_apps = ['developer_portal']
21\ No newline at end of file0\ No newline at end of file
221
=== removed file 'developer_portal/migrations/0002_add_rawhtml_plugin.py'
--- developer_portal/migrations/0002_add_rawhtml_plugin.py 2015-12-08 10:25:29 +0000
+++ developer_portal/migrations/0002_add_rawhtml_plugin.py 1970-01-01 00:00:00 +0000
@@ -1,53 +0,0 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'RawHtml'
12 db.create_table(u'developer_portal_rawhtml', (
13 (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)),
14 ('body', self.gf('django.db.models.fields.TextField')()),
15 ))
16 db.send_create_signal(u'developer_portal', ['RawHtml'])
17
18
19 def backwards(self, orm):
20 # Deleting model 'RawHtml'
21 db.delete_table(u'developer_portal_rawhtml')
22
23
24 models = {
25 'cms.cmsplugin': {
26 'Meta': {'object_name': 'CMSPlugin'},
27 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
28 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
29 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
31 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
32 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
33 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
34 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
35 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
36 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
37 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
38 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
39 },
40 'cms.placeholder': {
41 'Meta': {'object_name': 'Placeholder'},
42 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
43 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
44 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
45 },
46 u'developer_portal.rawhtml': {
47 'Meta': {'object_name': 'RawHtml'},
48 'body': ('django.db.models.fields.TextField', [], {}),
49 u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
50 }
51 }
52
53 complete_apps = ['developer_portal']
54\ No newline at end of file0\ No newline at end of file
551
=== removed file 'developer_portal/migrations/0003_add_external_docs_branches.py'
--- developer_portal/migrations/0003_add_external_docs_branches.py 2015-12-08 10:25:29 +0000
+++ developer_portal/migrations/0003_add_external_docs_branches.py 1970-01-01 00:00:00 +0000
@@ -1,62 +0,0 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'ExternalDocsBranch'
12 db.create_table(u'developer_portal_externaldocsbranch', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('lp_origin', self.gf('django.db.models.fields.CharField')(max_length=200)),
15 ('docs_namespace', self.gf('django.db.models.fields.CharField')(max_length=120)),
16 ('index_doc', self.gf('django.db.models.fields.CharField')(max_length=120, blank=True)),
17 ))
18 db.send_create_signal(u'developer_portal', ['ExternalDocsBranch'])
19
20
21 def backwards(self, orm):
22 # Deleting model 'ExternalDocsBranch'
23 db.delete_table(u'developer_portal_externaldocsbranch')
24
25
26 models = {
27 'cms.cmsplugin': {
28 'Meta': {'object_name': 'CMSPlugin'},
29 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
30 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
31 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
33 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
34 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
35 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
36 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
37 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
38 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
39 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
40 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
41 },
42 'cms.placeholder': {
43 'Meta': {'object_name': 'Placeholder'},
44 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
45 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
46 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
47 },
48 u'developer_portal.externaldocsbranch': {
49 'Meta': {'object_name': 'ExternalDocsBranch'},
50 'docs_namespace': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
51 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
52 'index_doc': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}),
53 'lp_origin': ('django.db.models.fields.CharField', [], {'max_length': '200'})
54 },
55 u'developer_portal.rawhtml': {
56 'Meta': {'object_name': 'RawHtml'},
57 'body': ('django.db.models.fields.TextField', [], {}),
58 u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
59 }
60 }
61
62 complete_apps = ['developer_portal']
63\ No newline at end of file0\ No newline at end of file
641
=== removed file 'developer_portal/migrations/0004_auto__add_seoextension.py'
--- developer_portal/migrations/0004_auto__add_seoextension.py 2015-12-08 10:25:29 +0000
+++ developer_portal/migrations/0004_auto__add_seoextension.py 1970-01-01 00:00:00 +0000
@@ -1,126 +0,0 @@
1# -*- coding: utf-8 -*-
2from south.utils import datetime_utils as datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'SEOExtension'
12 db.create_table(u'developer_portal_seoextension', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('public_extension', self.gf('django.db.models.fields.related.OneToOneField')(related_name='draft_extension', unique=True, null=True, to=orm['developer_portal.SEOExtension'])),
15 ('extended_object', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.Title'], unique=True)),
16 ('keywords', self.gf('django.db.models.fields.CharField')(max_length=256)),
17 ))
18 db.send_create_signal(u'developer_portal', ['SEOExtension'])
19
20
21 def backwards(self, orm):
22 # Deleting model 'SEOExtension'
23 db.delete_table(u'developer_portal_seoextension')
24
25
26 models = {
27 'cms.cmsplugin': {
28 'Meta': {'object_name': 'CMSPlugin'},
29 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
30 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
31 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
33 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
34 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
35 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
36 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
37 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
38 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
39 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
40 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
41 },
42 'cms.page': {
43 'Meta': {'ordering': "('tree_id', 'lft')", 'unique_together': "(('publisher_is_draft', 'application_namespace'), ('reverse_id', 'site', 'publisher_is_draft'))", 'object_name': 'Page'},
44 'application_namespace': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
45 'application_urls': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
46 'changed_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
47 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
48 'created_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
49 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
50 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
51 'in_navigation': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
52 'is_home': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
53 'languages': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
54 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
55 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
56 'limit_visibility_in_menu': ('django.db.models.fields.SmallIntegerField', [], {'default': 'None', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
57 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
58 'navigation_extenders': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}),
59 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['cms.Page']"}),
60 'placeholders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cms.Placeholder']", 'symmetrical': 'False'}),
61 'publication_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
62 'publication_end_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
63 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
64 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Page']"}),
65 'reverse_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
66 'revision_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
67 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
68 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_pages'", 'to': u"orm['sites.Site']"}),
69 'soft_root': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
70 'template': ('django.db.models.fields.CharField', [], {'default': "'INHERIT'", 'max_length': '100'}),
71 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
72 'xframe_options': ('django.db.models.fields.IntegerField', [], {'default': '0'})
73 },
74 'cms.placeholder': {
75 'Meta': {'object_name': 'Placeholder'},
76 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
77 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
78 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
79 },
80 'cms.title': {
81 'Meta': {'unique_together': "(('language', 'page'),)", 'object_name': 'Title'},
82 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
83 'has_url_overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
84 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
85 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
86 'menu_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
87 'meta_description': ('django.db.models.fields.TextField', [], {'max_length': '155', 'null': 'True', 'blank': 'True'}),
88 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'title_set'", 'to': "orm['cms.Page']"}),
89 'page_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
90 'path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
91 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
92 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
93 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Title']"}),
94 'publisher_state': ('django.db.models.fields.SmallIntegerField', [], {'default': '0', 'db_index': 'True'}),
95 'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
96 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
97 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
98 },
99 u'developer_portal.externaldocsbranch': {
100 'Meta': {'object_name': 'ExternalDocsBranch'},
101 'docs_namespace': ('django.db.models.fields.CharField', [], {'max_length': '120'}),
102 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
103 'index_doc': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}),
104 'lp_origin': ('django.db.models.fields.CharField', [], {'max_length': '200'})
105 },
106 u'developer_portal.rawhtml': {
107 'Meta': {'object_name': 'RawHtml'},
108 'body': ('django.db.models.fields.TextField', [], {}),
109 u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
110 },
111 u'developer_portal.seoextension': {
112 'Meta': {'object_name': 'SEOExtension'},
113 'extended_object': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.Title']", 'unique': 'True'}),
114 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
115 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
116 'public_extension': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'draft_extension'", 'unique': 'True', 'null': 'True', 'to': u"orm['developer_portal.SEOExtension']"})
117 },
118 u'sites.site': {
119 'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"},
120 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
121 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
123 }
124 }
125
126 complete_apps = ['developer_portal']
127\ No newline at end of file0\ No newline at end of file
1281
=== modified file 'developer_portal/models.py'
--- developer_portal/models.py 2015-11-02 16:47:26 +0000
+++ developer_portal/models.py 2016-01-19 00:21:38 +0000
@@ -1,7 +1,5 @@
1from django.db import models1from django.db import models
2from django.utils.translation import ugettext_lazy as _
32
4from cms.models import CMSPlugin
5from cms.extensions import TitleExtension3from cms.extensions import TitleExtension
6from cms.extensions.extension_pool import extension_pool4from cms.extensions.extension_pool import extension_pool
7from djangocms_text_ckeditor.html import extract_images5from djangocms_text_ckeditor.html import extract_images
@@ -20,25 +18,6 @@
20 AbstractText.save(self, *args, **kwargs)18 AbstractText.save(self, *args, **kwargs)
2119
2220
23class ExternalDocsBranch(models.Model):
24 # We originally assumed that branches would also live in LP,
25 # well, we were wrong, but let's keep the name around. It's
26 # no use having a schema/data migration just for this.
27 lp_origin = models.CharField(
28 max_length=200,
29 help_text=_('External branch location, ie: lp:snappy/15.04 or '
30 'https://github.com/ubuntu-core/snappy.git'))
31 docs_namespace = models.CharField(
32 max_length=120,
33 help_text=_('Path alias we want to use for the docs, '
34 'ie "snappy/guides/15.04" or '
35 '"snappy/guides/latest", etc.'))
36 index_doc = models.CharField(
37 max_length=120,
38 help_text=_('File name of doc to be used as index document, '
39 'ie "intro.md"'),
40 blank=True)
41
42class SEOExtension(TitleExtension):21class SEOExtension(TitleExtension):
43 keywords = models.CharField(max_length=256)22 keywords = models.CharField(max_length=256)
4423
4524
=== modified file 'developer_portal/settings.py'
--- developer_portal/settings.py 2015-12-08 10:25:29 +0000
+++ developer_portal/settings.py 2016-01-19 00:21:38 +0000
@@ -30,8 +30,6 @@
30# SECURITY WARNING: don't run with debug turned on in production!30# SECURITY WARNING: don't run with debug turned on in production!
31DEBUG = True31DEBUG = True
3232
33TEMPLATE_DEBUG = True
34
35ALLOWED_HOSTS = ['127.0.0.1', 'developer.ubuntu.com']33ALLOWED_HOSTS = ['127.0.0.1', 'developer.ubuntu.com']
3634
3735
@@ -49,14 +47,13 @@
49 # Allow login from Ubuntu SSO47 # Allow login from Ubuntu SSO
50 'django_openid_auth',48 'django_openid_auth',
5149
52 'mptt', #utilities for implementing a modified pre-order traversal tree
53 'menus', #helper for model independent hierarchical website navigation50 'menus', #helper for model independent hierarchical website navigation
54 'south', #intelligent schema and data migrations
55 'sekizai', #for javascript and css management51 'sekizai', #for javascript and css management
56 'reversion', #content versioning52 'reversion', #content versioning
57 'django_pygments',53 'django_pygments',
58 'django_comments',54 'django_comments',
59 'tagging',55 'tagging',
56 'template_debug',
6057
61 'ckeditor',58 'ckeditor',
62 'djangocms_text_ckeditor',59 'djangocms_text_ckeditor',
@@ -66,6 +63,7 @@
66 'djangocms_picture',63 'djangocms_picture',
67 'djangocms_video',64 'djangocms_video',
68 'djangocms_snippet',65 'djangocms_snippet',
66 'treebeard',
6967
70 'cmsplugin_zinnia',68 'cmsplugin_zinnia',
71 'zinnia',69 'zinnia',
@@ -78,6 +76,8 @@
78 'store_data',76 'store_data',
7977
80 'api_docs',78 'api_docs',
79
80 'md_importer',
81]81]
8282
83MIDDLEWARE_CLASSES = (83MIDDLEWARE_CLASSES = (
@@ -107,24 +107,29 @@
107#CACHE_MIDDLEWARE_SECONDS = 3600107#CACHE_MIDDLEWARE_SECONDS = 3600
108#CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True108#CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True
109109
110TEMPLATE_CONTEXT_PROCESSORS = (110TEMPLATES = [
111 'django.contrib.auth.context_processors.auth',111 {
112 'django.core.context_processors.i18n',112 'BACKEND': 'django.template.backends.django.DjangoTemplates',
113 'django.core.context_processors.request',113 'DIRS': [
114 'django.core.context_processors.media',114 os.path.join(PROJECT_PATH, "templates"),
115 'django.core.context_processors.static',115 ],
116116 'APP_DIRS': True,
117 'sekizai.context_processors.sekizai',117 'OPTIONS': {
118 'cms.context_processors.cms_settings',118 'context_processors': [
119 'django.contrib.messages.context_processors.messages',119 'django.core.context_processors.request',
120)120 'django.contrib.auth.context_processors.auth',
121121 'django.core.context_processors.i18n',
122TEMPLATE_DIRS = (122 'django.core.context_processors.media',
123 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".123 'django.core.context_processors.static',
124 # Always use forward slashes, even on Windows.124
125 # Don't forget to use absolute paths, not relative paths.125 'sekizai.context_processors.sekizai',
126 os.path.join(PROJECT_PATH, "templates"),126 'cms.context_processors.cms_settings',
127)127 'django.contrib.messages.context_processors.messages',
128 ]
129 }
130 }
131]
132
128133
129ROOT_URLCONF = 'developer_portal.urls'134ROOT_URLCONF = 'developer_portal.urls'
130135
@@ -318,6 +323,23 @@
318 #'PAGINATE_BY': 10,323 #'PAGINATE_BY': 10,
319}324}
320325
326MIGRATION_MODULES = {
327 'cms': 'cms.migrations',
328 'cmsplugin_zinnia': 'cmsplugin_zinnia.migrations',
329 'djangocms_link': 'djangocms_link.migrations',
330 'djangocms_picture': 'djangocms_picture.migrations',
331 'djangocms_snippet': 'djangocms_snippet.migrations',
332 'djangocms_text_ckeditor': 'djangocms_text_ckeditor.migrations',
333 'djangocms_video': 'djangocms_video.migrations',
334 'django_comments': 'django_comments.migrations',
335 'menus': 'menus.migrations',
336 'rest_framework.authtoken': 'rest_framework.authtoken.migrations',
337 'reversion': 'reversion.migrations',
338 'tagging': 'tagging.migrations',
339 'taggit': 'taggit.migrations',
340 'zinnia': 'zinnia.migrations',
341}
342
321LOGGING = {343LOGGING = {
322 'version': 1,344 'version': 1,
323 'disable_existing_loggers': False,345 'disable_existing_loggers': False,
324346
=== modified file 'developer_portal/urls.py'
--- developer_portal/urls.py 2015-12-08 10:25:29 +0000
+++ developer_portal/urls.py 2016-01-19 00:21:38 +0000
@@ -32,7 +32,7 @@
3232
33urlpatterns += i18n_patterns('',33urlpatterns += i18n_patterns('',
34 url(r'^search/', 'developer_portal.views.search', name='search'),34 url(r'^search/', 'developer_portal.views.search', name='search'),
35 url(r'^ckeditor/', include('ckeditor.urls')),35 url(r'^ckeditor/', include('ckeditor_uploader.urls')),
36 url(r'^', include('cms.urls')),36 url(r'^', include('cms.urls')),
37) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)37) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
3838
3939
=== modified file 'locale/developer_portal.pot'
--- locale/developer_portal.pot 2015-12-14 13:22:16 +0000
+++ locale/developer_portal.pot 2016-01-19 00:21:38 +0000
@@ -8,7 +8,7 @@
8msgstr ""8msgstr ""
9"Project-Id-Version: PACKAGE VERSION\n"9"Project-Id-Version: PACKAGE VERSION\n"
10"Report-Msgid-Bugs-To: \n"10"Report-Msgid-Bugs-To: \n"
11"POT-Creation-Date: 2015-12-14 13:22+0000\n"11"POT-Creation-Date: 2016-01-19 00:07+0000\n"
12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14"Language-Team: LANGUAGE <LL@li.org>\n"14"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -21,44 +21,28 @@
21msgid "Raw HTML"21msgid "Raw HTML"
22msgstr ""22msgstr ""
2323
24#: developer_portal/models.py:2924#: developer_portal/settings.py:194 developer_portal/settings.py:202
25msgid ""
26"External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-"
27"core/snappy.git"
28msgstr ""
29
30#: developer_portal/models.py:33
31msgid ""
32"Path alias we want to use for the docs, ie \"snappy/guides/15.04\" or "
33"\"snappy/guides/latest\", etc."
34msgstr ""
35
36#: developer_portal/models.py:38
37msgid "File name of doc to be used as index document, ie \"intro.md\""
38msgstr ""
39
40#: developer_portal/settings.py:189 developer_portal/settings.py:197
41#: templates/translations_dashboard.html:1225#: templates/translations_dashboard.html:12
42msgid "English"26msgid "English"
43msgstr ""27msgstr ""
4428
45#: developer_portal/settings.py:190 developer_portal/settings.py:20429#: developer_portal/settings.py:195 developer_portal/settings.py:209
46#: templates/translations_dashboard.html:1330#: templates/translations_dashboard.html:13
47msgid "Simplified Chinese"31msgid "Simplified Chinese"
48msgstr ""32msgstr ""
4933
50#: developer_portal/settings.py:21234#: developer_portal/settings.py:217
51msgid "Spanish"35msgid "Spanish"
52msgstr ""36msgstr ""
5337
54#: developer_portal/settings.py:25238#: developer_portal/settings.py:257
55msgid "Page content"39msgid "Page content"
56msgstr ""40msgstr ""
5741
58#. Translators: this is the default text that will be shown42#. Translators: this is the default text that will be shown
59#. to editors when editing a page. You can use some HTML,43#. to editors when editing a page. You can use some HTML,
60#. but don't go wild :)44#. but don't go wild :)
61#: developer_portal/settings.py:26045#: developer_portal/settings.py:265
62msgid "<p>Add content here...</p>"46msgid "<p>Add content here...</p>"
63msgstr ""47msgstr ""
6448
@@ -68,6 +52,40 @@
68"profile</a> to use the developer site"52"profile</a> to use the developer site"
69msgstr ""53msgstr ""
7054
55#: md_importer/models.py:10
56msgid ""
57"External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-"
58"core/snappy.git"
59msgstr ""
60
61#: md_importer/models.py:14
62msgid "For use with git branches, ie: \"master\" or \"15.04\" or \"1.x\"."
63msgstr ""
64
65#: md_importer/models.py:19
66msgid "Command to run after checkout of the branch."
67msgstr ""
68
69#: md_importer/models.py:37
70msgid ""
71"File or directory to import from the branch. Ie: \"docs/intro.md\" (file) or "
72"\"docs\" (complete directory), etc."
73msgstr ""
74
75#: md_importer/models.py:43
76msgid ""
77"Article URL (for a specific file) or article namespace for a directory or a "
78"set of files."
79msgstr ""
80
81#: md_importer/models.py:56
82msgid "Datetime"
83msgstr ""
84
85#: md_importer/models.py:56
86msgid "Datetime of last import."
87msgstr ""
88
71#: store_data/cms_plugins.py:1189#: store_data/cms_plugins.py:11
72msgid "Snap list - Gadget"90msgid "Snap list - Gadget"
73msgstr ""91msgstr ""
@@ -101,9 +119,8 @@
101119
102#: templates/base.html:34 templates/base.html.py:51120#: templates/base.html:34 templates/base.html.py:51
103#: templates/menu/menu_footer_first_level.html:8121#: templates/menu/menu_footer_first_level.html:8
104#: templates/menu/menu_header_first_level.html:8122#: templates/menu/menu_header_first_level.html:8 templates/menu/sub_menu.html:8
105#: templates/menu/sub_menu.html:8 templates/menu/sub_menu.html.py:22123#: templates/menu/sub_menu.html.py:22 templates/menu/sub_menu.html:25
106#: templates/menu/sub_menu.html:25
107msgid "Overview"124msgid "Overview"
108msgstr ""125msgstr ""
109126
@@ -175,15 +192,23 @@
175msgid "Go to the top of the page"192msgid "Go to the top of the page"
176msgstr ""193msgstr ""
177194
195#: templates/comments/zinnia/entry/form.html:25
196msgid "Comment as"
197msgstr ""
198
199#: templates/comments/zinnia/entry/form.html:25 templates/error_base.html:63
200#: templates/website_base.html:84
201msgid "Log out"
202msgstr ""
203
204#: templates/comments/zinnia/entry/form.html:28
205msgid "Post"
206msgstr ""
207
178#: templates/error_base.html:54 templates/website_base.html:74208#: templates/error_base.html:54 templates/website_base.html:74
179msgid "Jump to content"209msgid "Jump to content"
180msgstr ""210msgstr ""
181211
182#: templates/error_base.html:63 templates/website_base.html:84
183#: templates/comments/zinnia/entry/form.html:25
184msgid "Log out"
185msgstr ""
186
187#: templates/error_base.html:69 templates/website_base.html:90212#: templates/error_base.html:69 templates/website_base.html:90
188msgid "Log in"213msgid "Log in"
189msgstr ""214msgstr ""
@@ -204,6 +229,14 @@
204"href='https://bugs.launchpad.net/ubuntudeveloperportal'>report it"229"href='https://bugs.launchpad.net/ubuntudeveloperportal'>report it"
205msgstr ""230msgstr ""
206231
232#: templates/menu/menu_language_chooser.html:6
233msgid "View in:"
234msgstr ""
235
236#: templates/menu/menu_language_chooser.html:10
237msgid "Change to language:"
238msgstr ""
239
207#: templates/search_results.html:4 templates/search_results.html.py:11240#: templates/search_results.html:4 templates/search_results.html.py:11
208msgid "Search results for:"241msgid "Search results for:"
209msgstr ""242msgstr ""
@@ -224,8 +257,7 @@
224msgid "A simple dashboard to keep track of updates between languages."257msgid "A simple dashboard to keep track of updates between languages."
225msgstr ""258msgstr ""
226259
227#: templates/translations_dashboard.html:11260#: templates/translations_dashboard.html:11 templates/zinnia/entry_list.html:23
228#: templates/zinnia/entry_list.html:23
229msgid "Page"261msgid "Page"
230msgstr ""262msgstr ""
231263
@@ -277,22 +309,6 @@
277msgid "Back to top"309msgid "Back to top"
278msgstr ""310msgstr ""
279311
280#: templates/comments/zinnia/entry/form.html:25
281msgid "Comment as"
282msgstr ""
283
284#: templates/comments/zinnia/entry/form.html:28
285msgid "Post"
286msgstr ""
287
288#: templates/menu/menu_language_chooser.html:6
289msgid "View in:"
290msgstr ""
291
292#: templates/menu/menu_language_chooser.html:10
293msgid "Change to language:"
294msgstr ""
295
296#: templates/zinnia/_entry_detail_base.html:33312#: templates/zinnia/_entry_detail_base.html:33
297#, python-format313#, python-format
298msgid "%(percent)s%% of %(object)s still remains to read."314msgid "%(percent)s%% of %(object)s still remains to read."
@@ -334,7 +350,7 @@
334msgid "Authors"350msgid "Authors"
335msgstr ""351msgstr ""
336352
337#: templates/zinnia/author_list.html:21 templates/zinnia/category_list.html:19353#: templates/zinnia/author_list.html:21 templates/zinnia/category_list.html:20
338#: templates/zinnia/tag_list.html:20354#: templates/zinnia/tag_list.html:20
339#, python-format355#, python-format
340msgid "%(entry_count)s entry"356msgid "%(entry_count)s entry"
@@ -354,21 +370,20 @@
354msgid "RSS Feed of latest discussions"370msgid "RSS Feed of latest discussions"
355msgstr ""371msgstr ""
356372
357#: templates/zinnia/category_list.html:4373#: templates/zinnia/category_list.html:5 templates/zinnia/category_list.html:13
358#: templates/zinnia/category_list.html:12
359msgid "Category list"374msgid "Category list"
360msgstr ""375msgstr ""
361376
362#: templates/zinnia/category_list.html:6377#: templates/zinnia/category_list.html:7
363msgid "Categories"378msgid "Categories"
364msgstr ""379msgstr ""
365380
366#: templates/zinnia/category_list.html:18381#: templates/zinnia/category_list.html:19
367#, python-format382#, python-format
368msgid "Show all entries in %(category)s"383msgid "Show all entries in %(category)s"
369msgstr ""384msgstr ""
370385
371#: templates/zinnia/category_list.html:24386#: templates/zinnia/category_list.html:25
372msgid "No categories yet."387msgid "No categories yet."
373msgstr ""388msgstr ""
374389
375390
=== added directory 'md_importer'
=== added file 'md_importer/TODO'
--- md_importer/TODO 1970-01-01 00:00:00 +0000
+++ md_importer/TODO 2016-01-19 00:21:38 +0000
@@ -0,0 +1,7 @@
1- fix branch indicator (devel vs. stable, 1.x vs devel, etc.)
2- unpublished docs: some articles show up with a blue circle, so
3 unpublished although they're accessible (not a real issue - content is
4 always updated correctly)
5- add comment at top of RawHTML plugins, so users know not to edit them,
6 currently blocked on LP: #1523925 (RawHTML plugin strips comments)
7- investigate what the api importer does (upload to swift)
08
=== added file 'md_importer/__init__.py'
=== added file 'md_importer/admin.py'
--- md_importer/admin.py 1970-01-01 00:00:00 +0000
+++ md_importer/admin.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,36 @@
1from django.contrib import admin
2
3from .models import (
4 ExternalDocsBranch, ExternalDocsBranchImportDirective,
5 ImportedArticle,
6)
7from django.core.management import call_command
8
9__all__ = (
10)
11
12
13def import_selected_external_docs_branches(modeladmin, request, queryset):
14 branches = []
15 for branch in queryset:
16 branches.append(branch.origin)
17 call_command('import_md', *branches)
18 import_selected_external_docs_branches.short_description = \
19 "Import selected branches"
20
21
22@admin.register(ExternalDocsBranch)
23class ExternalDocsBranchAdmin(admin.ModelAdmin):
24 list_display = ('origin', 'post_checkout_command', 'branch_name',)
25 list_filter = ('origin', 'post_checkout_command', 'branch_name',)
26 actions = [import_selected_external_docs_branches]
27
28
29@admin.register(ExternalDocsBranchImportDirective)
30class ExternalDocsBranchImportDirectiveAdmin(admin.ModelAdmin):
31 pass
32
33
34@admin.register(ImportedArticle)
35class ImportedArticleAdmin(admin.ModelAdmin):
36 pass
037
=== added directory 'md_importer/importer'
=== added file 'md_importer/importer/__init__.py'
--- md_importer/importer/__init__.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/__init__.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,5 @@
1from developer_portal.settings import LANGUAGE_CODE
2
3DEFAULT_LANG = LANGUAGE_CODE
4HOME_PAGE_URL = '/{}/'.format(DEFAULT_LANG)
5SUPPORTED_ARTICLE_TYPES = ['.md', '.html']
06
=== added file 'md_importer/importer/article.py'
--- md_importer/importer/article.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/article.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,163 @@
1from bs4 import BeautifulSoup
2import codecs
3import logging
4import markdown
5import os
6import re
7import sys
8
9from . import (
10 DEFAULT_LANG,
11 SUPPORTED_ARTICLE_TYPES,
12)
13from .publish import get_or_create_page, slugify
14
15if sys.version_info.major == 2:
16 from urlparse import urlparse
17else:
18 from urllib.parse import urlparse
19
20
21class Article:
22 def __init__(self, fn, write_to):
23 self.html = None
24 self.page = None
25 self.title = ""
26 self.fn = fn
27 self.write_to = slugify(self.fn)
28 self.full_url = write_to
29 self.slug = os.path.basename(self.full_url)
30
31 def _find_local_images(self):
32 '''Local images are currently not supported.'''
33 soup = BeautifulSoup(self.html, 'html5lib')
34 local_images = []
35 for img in soup.find_all('img'):
36 if img.has_attr('src'):
37 (scheme, netloc, path, params, query, fragment) = \
38 urlparse(img.attrs['src'])
39 if scheme not in ['http', 'https']:
40 local_images.extend([img.attrs['src']])
41 return local_images
42
43 def read(self):
44 if os.path.splitext(self.fn)[1] not in SUPPORTED_ARTICLE_TYPES:
45 logging.error("Don't know how to interpret '{}'.".format(
46 self.fn))
47 return False
48 with codecs.open(self.fn, 'r', encoding='utf-8') as f:
49 if self.fn.endswith('.md'):
50 self.html = markdown.markdown(
51 f.read(),
52 output_format='html5',
53 extensions=['pymdownx.github'])
54 elif self.fn.endswith('.html'):
55 self.html = f.read()
56 local_images = self._find_local_images()
57 if local_images:
58 logging.error('Found the following local image(s): {}'.format(
59 ', '.join(local_images)
60 ))
61 return False
62 self.title = self._read_title()
63 self._remove_body_and_html_tags()
64 self._use_developer_site_style()
65 return True
66
67 def _read_title(self):
68 soup = BeautifulSoup(self.html, 'html5lib')
69 if soup.title:
70 return soup.title.text
71 if soup.h1:
72 return soup.h1.text
73 return slugify(self.fn).replace('-', ' ').title()
74
75 def _remove_body_and_html_tags(self):
76 self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
77 flags=re.MULTILINE)
78 self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
79 flags=re.MULTILINE)
80
81 def _use_developer_site_style(self):
82 begin = (u"<div class=\"row no-border\">"
83 "\n<div class=\"eight-col\">\n")
84 end = u"</div>\n</div>"
85 self.html = begin + self.html + end
86 self.html = self.html.replace(
87 "<pre><code>",
88 "</div><div class=\"twelve-col\"><pre><code>")
89 self.html = self.html.replace(
90 "</code></pre>",
91 "</code></pre></div><div class=\"eight-col\">")
92
93 def replace_links(self, titles, url_map):
94 soup = BeautifulSoup(self.html, 'html5lib')
95 change = False
96 for link in soup.find_all('a'):
97 if not link.has_attr('class') or \
98 'headeranchor-link' not in link.attrs['class']:
99 for title in titles:
100 if title.endswith(link.attrs['href']) and \
101 link.attrs['href'] != url_map[title].full_url:
102 link.attrs['href'] = url_map[title].full_url
103 change = True
104 if change:
105 self.html = soup.prettify()
106 return change
107
108 def add_to_db(self):
109 '''Publishes pages in their branch alias namespace.'''
110 self.page = get_or_create_page(
111 title=self.title, full_url=self.full_url, menu_title=self.title,
112 html=self.html)
113 if not self.page:
114 return False
115 self.full_url = self.page.get_absolute_url()
116 return True
117
118 def publish(self):
119 if self.page.is_dirty(DEFAULT_LANG):
120 self.page.publish(DEFAULT_LANG)
121 self.page = self.page.get_public_object()
122 return self.page
123
124
125class SnappyArticle(Article):
126 release_alias = None
127
128 def read(self):
129 if not Article.read(self):
130 return False
131 self.release_alias = re.findall(r'snappy/guides/(\S+?)/\S+?',
132 self.full_url)[0]
133 self._make_snappy_mods()
134 return True
135
136 def _make_snappy_mods(self):
137 # Make sure the reader knows which documentation she is browsing
138 if self.release_alias != 'current':
139 before = (u"<div class=\"row no-border\">\n"
140 "<div class=\"eight-col\">\n")
141 after = (u"<div class=\"row no-border\">\n"
142 "<div class=\"box pull-three three-col\">"
143 "<p>You are browsing the Snappy <code>%s</code> "
144 "documentation.</p>"
145 "<p><a href=\"/snappy/guides/current/%s\">"
146 "Back to the latest stable release &rsaquo;"
147 "</a></p></div>\n"
148 "<div class=\"eight-col\">\n") % (self.release_alias,
149 self.slug, )
150 self.html = self.html.replace(before, after)
151
152 def add_to_db(self):
153 if self.release_alias == "current":
154 # Add a guides/<page> redirect to guides/current/<page>
155 page = get_or_create_page(
156 title=self.title,
157 full_url=self.full_url.replace('/current', ''),
158 redirect="/snappy/guides/current/{}".format(self.slug))
159 if not page:
160 return False
161 else:
162 self.title += " (%s)" % (self.release_alias,)
163 return Article.add_to_db(self)
0164
=== added file 'md_importer/importer/process.py'
--- md_importer/importer/process.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/process.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,60 @@
1from django.core.management import call_command
2
3import datetime
4import pytz
5import shutil
6import tempfile
7
8from md_importer.importer.repo import create_repo
9
10from md_importer.models import (
11 ExternalDocsBranchImportDirective,
12 ImportedArticle,
13)
14
15
16def process_branch(branch):
17 tempdir = tempfile.mkdtemp()
18 repo = create_repo(tempdir, branch.origin, branch.branch_name,
19 branch.post_checkout_command)
20 if repo.get() != 0:
21 return False
22 for directive in ExternalDocsBranchImportDirective.objects.filter(
23 external_docs_branch=branch):
24 repo.add_directive(directive.import_from,
25 directive.write_to)
26 if not repo.execute_import_directives():
27 return False
28 if not repo.publish():
29 return False
30 timestamp = datetime.datetime.now(pytz.utc)
31
32 # Update data in ImportedArticle table
33 for page in repo.pages:
34 if ImportedArticle.objects.filter(branch=branch, page=page).count():
35 imported_article = ImportedArticle.objects.filter(
36 branch=branch, page=page)[0]
37 imported_article.last_import = datetime.datetime.now(pytz.utc)
38 imported_article.save()
39 else:
40 ImportedArticle.objects.get_or_create(
41 branch=branch,
42 page=page,
43 last_import=datetime.datetime.now(pytz.utc))
44
45 # Remove old entries
46 for imported_article in ImportedArticle.objects.filter(
47 branch=branch, last_import__lt=timestamp):
48 imported_article.page.delete()
49 imported_article.delete()
50
51 # The import is done, now let's clean up.
52 imported_page_ids = [p.id for p in repo.pages
53 if p.changed_by in ['python-api', 'script']]
54 ImportedArticle.objects.filter(
55 branch=branch).filter(id__in=imported_page_ids).delete()
56 shutil.rmtree(tempdir)
57
58 # https://stackoverflow.com/questions/33284171/
59 call_command('cms', 'fix-tree')
60 return True
061
=== added file 'md_importer/importer/publish.py'
--- md_importer/importer/publish.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/publish.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,75 @@
1from md_importer.importer import DEFAULT_LANG, HOME_PAGE_URL
2
3from cms.api import create_page, add_plugin
4from cms.models import Title
5from djangocms_text_ckeditor.html import clean_html
6
7import logging
8import re
9import os
10
11
12def slugify(filename):
13 return os.path.basename(filename).replace('.md', '').replace('.html', '')
14
15
16def _find_parent(full_url):
17 if full_url == HOME_PAGE_URL:
18 # If we set up the homepage, we don't need a parent.
19 return None
20 parent_url = re.sub(
21 r'^\/{}\/'.format(DEFAULT_LANG),
22 '',
23 os.path.dirname(full_url))
24
25 parent_pages = Title.objects.select_related('page').filter(
26 path__regex=parent_url, language=DEFAULT_LANG).filter(
27 publisher_is_draft=True)
28 if not parent_pages:
29 logging.error('Parent {} not found.'.format(
30 parent_url))
31 return None
32 return parent_pages[0].page
33
34
35def get_or_create_page(title, full_url, menu_title=None,
36 in_navigation=True, redirect=None, html=None):
37 # First check if pages already exist.
38 pages = Title.objects.select_related('page').filter(
39 path__regex=full_url).filter(publisher_is_draft=True)
40 if pages:
41 page = pages[0].page
42 if page.get_title() != title:
43 page.title = title
44 if page.get_menu_title() != menu_title:
45 page.menu_title = menu_title
46 if page.in_navigation != in_navigation:
47 page.in_navigation = in_navigation
48 if page.get_redirect() != redirect:
49 page.redirect = redirect
50 if html:
51 # We create the page, so we know there's just one placeholder
52 placeholder = page.placeholders.all()[0]
53 if placeholder.get_plugins():
54 plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
55 if plugin.body != clean_html(html, full=False):
56 plugin.body = html
57 plugin.save()
58 else:
59 add_plugin(
60 placeholder, 'RawHtmlPlugin',
61 DEFAULT_LANG, body=html)
62 else:
63 parent = _find_parent(full_url)
64 if not parent:
65 return None
66 slug = os.path.basename(full_url)
67 page = create_page(
68 title, 'default.html', DEFAULT_LANG, slug=slug, parent=parent,
69 menu_title=menu_title, in_navigation=in_navigation,
70 position='last-child', redirect=redirect)
71 placeholder = page.placeholders.get()
72 add_plugin(placeholder, 'RawHtmlPlugin', DEFAULT_LANG, body=html)
73 placeholder = page.placeholders.all()[0]
74 plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
75 return page
076
=== added file 'md_importer/importer/repo.py'
--- md_importer/importer/repo.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/repo.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,174 @@
1from . import (
2 DEFAULT_LANG,
3 SUPPORTED_ARTICLE_TYPES,
4)
5from .article import Article, SnappyArticle
6from .publish import get_or_create_page, slugify
7from .source import SourceCode
8
9import glob
10import logging
11import os
12
13
14def create_repo(tempdir, origin, branch_name, post_checkout_command):
15 if os.path.exists(origin):
16 if 'snappy' in origin:
17 repo_class = SnappyRepo
18 else:
19 repo_class = Repo
20 else:
21 if origin.startswith('lp:snappy') or \
22 'snappy' in origin.split(':')[1].split('.git')[0].split('/'):
23 repo_class = SnappyRepo
24 else:
25 repo_class = Repo
26 return repo_class(tempdir, origin, branch_name, post_checkout_command)
27
28
29class Repo:
30 def __init__(self, tempdir, origin, branch_name, post_checkout_command):
31 self.directives = []
32 self.imported_articles = []
33 self.url_map = {}
34 self.titles = {}
35 self.index_doc_url = None
36 self.index_page = None
37 self.release_alias = None
38 # On top of the pages in imported_articles this also
39 # includes index_page
40 self.pages = []
41 self.origin = origin
42 self.branch_name = branch_name
43 self.post_checkout_command = post_checkout_command
44 branch_nick = os.path.basename(self.origin.replace('.git', ''))
45 self.checkout_location = os.path.join(
46 tempdir, branch_nick)
47 self.index_doc_title = branch_nick
48 self.article_class = Article
49
50 def get(self):
51 sourcecode = SourceCode(self.origin, self.checkout_location,
52 self.branch_name, self.post_checkout_command)
53 if sourcecode.get() != 0:
54 logging.error(
55 'Could not check out branch "{}".'.format(self.origin))
56 return 1
57 return 0
58
59 def add_directive(self, import_from, write_to):
60 self.directives += [
61 {
62 'import_from': os.path.join(self.checkout_location,
63 import_from),
64 'write_to': write_to
65 }
66 ]
67
68 def execute_import_directives(self):
69 import_list = []
70 # Import single files first
71 for directive in [d for d in self.directives
72 if os.path.isfile(d['import_from'])]:
73 import_list += [
74 (directive['import_from'], directive['write_to'])
75 ]
76 # Import directories next
77 for directive in [d for d in self.directives
78 if os.path.isdir(d['import_from'])]:
79 for fn in glob.glob('{}/*'.format(directive['import_from'])):
80 if fn not in [a[0] for a in import_list]:
81 import_list += [
82 (fn, os.path.join(directive['write_to'], slugify(fn)))
83 ]
84 # If we import into a namespace and don't have an index doc,
85 # we need to write one.
86 if directive['write_to'] not in [x[1] for x in import_list]:
87 self.index_doc_url = directive['write_to']
88 if self.index_doc_url:
89 if not self._create_fake_index_page():
90 logging.error('Importing of {} aborted.'.format(self.origin))
91 return False
92 # The actual import
93 for entry in import_list:
94 article = self._read_article(entry[0], entry[1])
95 if article:
96 self.imported_articles += [article]
97 self.titles[article.fn] = article.title
98 self.url_map[article.fn] = article
99 elif os.path.splitext(entry[0])[1] in SUPPORTED_ARTICLE_TYPES:
100 # In this case the article was supported but still reading
101 # it failed, importing should be stopped here to avoid
102 # problems.
103 logging.error('Importing of {} aborted.'.format(self.origin))
104 return False
105 if self.index_doc_url:
106 self._write_fake_index_doc()
107 return True
108
109 def _read_article(self, fn, write_to):
110 article = self.article_class(fn, write_to)
111 if article.read():
112 return article
113 return None
114
115 def publish(self):
116 for article in self.imported_articles:
117 if not article.add_to_db():
118 logging.error('Publishing of {} aborted.'.format(self.origin))
119 return False
120 article.replace_links(self.titles, self.url_map)
121 self.pages = []
122 for article in self.imported_articles:
123 self.pages.extend([article.publish()])
124 if self.index_page:
125 self.index_page.publish(DEFAULT_LANG)
126 self.pages.extend([self.index_page])
127 return True
128
129 def _create_fake_index_page(self):
130 '''Creates a fake index page at the top of the branches
131 docs namespace.'''
132
133 if self.index_doc_url.endswith('current'):
134 redirect = '/snappy/guides'
135 else:
136 redirect = None
137 self.index_page = get_or_create_page(
138 title=self.index_doc_title, full_url=self.index_doc_url,
139 in_navigation=False, redirect=redirect, html='',
140 menu_title=None)
141 if not self.index_page:
142 return False
143 return True
144
145 def _write_fake_index_doc(self):
146 list_pages = ''
147 for article in [a for a
148 in self.imported_articles
149 if a.full_url.startswith(self.index_doc_url)]:
150 list_pages += '<li><a href=\"{}\">{}</a></li>'.format(
151 os.path.basename(article.full_url), article.title)
152 self.index_page.html = (
153 u'<div class=\"row\"><div class=\"eight-col\">\n'
154 '<p>This section contains documentation for the '
155 '<code>{}</code> Snappy branch.</p>'
156 '<p><ul class=\"list-ubuntu\">{}</ul></p>\n'
157 '<p>Auto-imported from <a '
158 'href=\"{}\">{}</a>.</p>\n'
159 '</div></div>'.format(self.release_alias, list_pages,
160 self.origin, self.origin))
161
162
163class SnappyRepo(Repo):
164 def __init__(self, tempdir, origin, branch_name, post_checkout_command):
165 Repo.__init__(self, tempdir, origin, branch_name,
166 post_checkout_command)
167 self.article_class = SnappyArticle
168 self.index_doc_title = 'Snappy documentation'
169
170 def _create_fake_index_page(self):
171 self.release_alias = os.path.basename(self.index_doc_url)
172 if not self.index_doc_url.endswith('current'):
173 self.index_doc_title += ' ({})'.format(self.release_alias)
174 return Repo._create_fake_index_page(self)
0175
=== added file 'md_importer/importer/source.py'
--- md_importer/importer/source.py 1970-01-01 00:00:00 +0000
+++ md_importer/importer/source.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,60 @@
1import logging
2import os
3import shutil
4import subprocess
5
6
7class SourceCode():
8 def __init__(self, origin, checkout_location, branch_name,
9 post_checkout_command):
10 self.origin = origin
11 self.checkout_location = checkout_location
12 self.branch_name = branch_name
13 self.post_checkout_command = post_checkout_command
14
15 def get(self):
16 res = self._get_repo()
17 if res == 0 and self.post_checkout_command:
18 res = self._post_checkout()
19 return res
20 return res
21
22 def _get_repo(self):
23 if os.path.exists(self.origin):
24 shutil.copytree(
25 self.origin,
26 self.checkout_location)
27 return 0
28 if self.origin.startswith('lp:') and \
29 os.path.exists('/usr/bin/bzr'):
30 return subprocess.call([
31 'bzr', 'checkout', '--lightweight', self.origin,
32 self.checkout_location])
33 if self.origin.startswith('https://github.com') and \
34 self.origin.endswith('.git') and \
35 os.path.exists('/usr/bin/git'):
36 retcode = subprocess.call([
37 'git', 'clone', '--quiet', self.origin,
38 self.checkout_location])
39 if retcode == 0 and self.branch_name:
40 pwd = os.getcwd()
41 os.chdir(self.checkout_location)
42 retcode = subprocess.call(['git', 'checkout', '--quiet',
43 self.branch_name])
44 os.chdir(pwd)
45 return retcode
46 logging.error(
47 'Repo format "{}" not understood.'.format(self.origin))
48 return 1
49
50 def _post_checkout(self):
51 pwd = os.getcwd()
52 os.chdir(self.checkout_location)
53 process = subprocess.Popen(self.post_checkout_command.split(),
54 stdout=subprocess.PIPE)
55 (out, err) = process.communicate()
56 retcode = process.wait()
57 os.chdir(pwd)
58 if retcode != 0:
59 logging.error(out)
60 return retcode
061
=== added directory 'md_importer/management'
=== added file 'md_importer/management/__init__.py'
=== added directory 'md_importer/management/commands'
=== added file 'md_importer/management/commands/__init__.py'
=== renamed file 'developer_portal/management/commands/import-external-docs-branches.py' => 'md_importer/management/commands/import_md.py'
--- developer_portal/management/commands/import-external-docs-branches.py 2015-12-08 10:25:29 +0000
+++ md_importer/management/commands/import_md.py 2016-01-19 00:21:38 +0000
@@ -1,300 +1,9 @@
1import logging
2
1from django.core.management.base import BaseCommand3from django.core.management.base import BaseCommand
2from django.core.management import call_command4
3from django.db import transaction5from md_importer.importer.process import process_branch
46from md_importer.models import ExternalDocsBranch
5from cms.api import create_page, add_plugin
6from cms.models import Page, Title
7from cms.utils import page_resolver
8
9from bs4 import BeautifulSoup
10import codecs
11import glob
12import logging
13import markdown
14import os
15import re
16import shutil
17import subprocess
18import sys
19import tempfile
20
21from developer_portal.models import ExternalDocsBranch
22
23DOCS_DIRNAME = 'docs'
24
25
26class DBActions:
27 added_pages = []
28 removed_pages = []
29
30 def add_page(self, **kwargs):
31 self.added_pages += [kwargs]
32
33 def remove_page(self, page_id):
34 self.removed_pages += [page_id]
35
36 @transaction.commit_on_success()
37 def run(self):
38 for added_page in self.added_pages:
39 page = get_or_create_page(**added_page)
40 page.publish('en')
41
42 # Only remove pages created by a script!
43 Page.objects.filter(id__in=self.removed_pages,
44 created_by="script").delete()
45
46 # https://stackoverflow.com/questions/33284171/
47 call_command('cms', 'fix-mptt')
48
49
50class MarkdownFile:
51 html = None
52
53 def __init__(self, fn, docs_namespace, db_actions, slug_override=None):
54 self.fn = fn
55 self.docs_namespace = docs_namespace
56 self.db_actions = db_actions
57 if slug_override:
58 self.slug = slug_override
59 else:
60 self.slug = slugify(self.fn)
61 self.full_url = os.path.join(self.docs_namespace, self.slug)
62 with codecs.open(self.fn, 'r', encoding='utf-8') as f:
63 self.html = markdown.markdown(
64 f.read(),
65 output_format="html5",
66 extensions=['markdown.extensions.tables'])
67 self.release_alias = self._get_release_alias()
68 self.title = self._read_title()
69 self._remove_body_and_html_tags()
70 self._use_developer_site_style()
71
72 def _get_release_alias(self):
73 alias = re.findall(r'/tmp/tmp\S+?/(\S+?)/%s/\S+?' % DOCS_DIRNAME,
74 self.fn)
75 return alias[0]
76
77 def _read_title(self):
78 soup = BeautifulSoup(self.html, 'html5lib')
79 if soup.title:
80 return soup.title.text
81 if soup.h1:
82 return soup.h1.text
83 return slugify(self.fn).replace('-', ' ').title()
84
85 def _remove_body_and_html_tags(self):
86 self.html = re.sub(r"<html>\n\s<body>\n", "", self.html,
87 flags=re.MULTILINE)
88 self.html = re.sub(r"\s<\/body>\n<\/html>", "", self.html,
89 flags=re.MULTILINE)
90
91 def _use_developer_site_style(self):
92 begin = (u"<div class=\"row no-border\">"
93 "\n<div class=\"eight-col\">\n")
94 end = u"</div>\n</div>"
95 self.html = begin + self.html + end
96 self.html = self.html.replace(
97 "<pre><code>",
98 "</div><div class=\"twelve-col\"><pre><code>")
99 self.html = self.html.replace(
100 "</code></pre>",
101 "</code></pre></div><div class=\"eight-col\">")
102
103 def replace_links(self, titles, url_map):
104 for title in titles:
105 local_md_fn = os.path.basename(title)
106 url = u'/'+url_map[title]
107 # Replace links of the form <a href="/path/somefile.md"> first
108 href = u"<a href=\"{}\">".format(url)
109 md_href = u"<a href=\"{}\">".format(local_md_fn)
110 self.html = self.html.replace(md_href, href)
111
112 # Now we can replace free-standing "somefile.md" references in
113 # the HTML
114 link = href + u"{}</a>".format(titles[title])
115 self.html = self.html.replace(local_md_fn, link)
116
117 def publish(self):
118 '''Publishes pages in their branch alias namespace.'''
119 self.db_actions.add_page(
120 title=self.title, full_url=self.full_url, menu_title=self.title,
121 html=self.html)
122
123
124class SnappyMarkdownFile(MarkdownFile):
125 def __init__(self, fn, docs_namespace, db_actions):
126 MarkdownFile.__init__(self, fn, docs_namespace, db_actions)
127 self._make_snappy_mods()
128
129 def _make_snappy_mods(self):
130 # Make sure the reader knows which documentation she is browsing
131 if self.release_alias != 'current':
132 before = (u"<div class=\"row no-border\">\n"
133 "<div class=\"eight-col\">\n")
134 after = (u"<div class=\"row no-border\">\n"
135 "<div class=\"box pull-three three-col\">"
136 "<p>You are browsing the Snappy <code>%s</code> "
137 "documentation.</p>"
138 "<p><a href=\"/snappy/guides/current/%s\">"
139 "Back to the latest stable release &rsaquo;"
140 "</a></p></div>\n"
141 "<div class=\"eight-col\">\n") % (self.release_alias,
142 self.slug, )
143 self.html = self.html.replace(before, after)
144
145 def publish(self):
146 if self.release_alias == "current":
147 # Add a guides/<page> redirect to guides/current/<page>
148 self.db_actions.add_page(
149 title=self.title,
150 full_url=self.full_url.replace('/current', ''),
151 redirect="/snappy/guides/current/%s" % (self.slug))
152 else:
153 self.title += " (%s)" % (self.release_alias,)
154 MarkdownFile.publish(self)
155
156
157def slugify(filename):
158 return os.path.basename(filename).replace('.md', '')
159
160
161class LocalBranch:
162 titles = {}
163 url_map = {}
164
165 def __init__(self, dirname, external_branch, db_actions):
166 self.dirname = dirname
167 self.docs_path = os.path.join(self.dirname, DOCS_DIRNAME)
168 self.doc_fns = glob.glob(self.docs_path+'/*.md')
169 self.md_files = []
170 self.external_branch = external_branch
171 self.docs_namespace = self.external_branch.docs_namespace
172 self.release_alias = os.path.basename(self.docs_namespace)
173 self.index_doc_title = self.release_alias.title()
174 self.index_doc = self.external_branch.index_doc
175 self.db_actions = db_actions
176 self.markdown_class = MarkdownFile
177
178 def import_markdown(self):
179 for doc_fn in self.doc_fns:
180 if self.index_doc and os.path.basename(doc_fn) == self.index_doc:
181 md_file = self.markdown_class(
182 doc_fn,
183 os.path.dirname(self.docs_namespace),
184 self.db_actions,
185 slug_override=os.path.basename(self.docs_namespace))
186 self.md_files.insert(0, md_file)
187 else:
188 md_file = self.markdown_class(doc_fn, self.docs_namespace,
189 self.db_actions)
190 self.md_files += [md_file]
191 self.titles[md_file.fn] = md_file.title
192 self.url_map[md_file.fn] = md_file.full_url
193 if not self.index_doc:
194 self._create_fake_index_doc()
195 for md_file in self.md_files:
196 md_file.replace_links(self.titles, self.url_map)
197
198 def remove_old_pages(self):
199 imported_page_urls = set([md_file.full_url
200 for md_file in self.md_files])
201 index_doc = page_resolver.get_page_queryset_from_path(
202 self.docs_namespace)
203 db_pages = []
204 if len(index_doc):
205 # All pages in this namespace currently in the database
206 db_pages = index_doc[0].get_descendants().all()
207 for db_page in db_pages:
208 still_relevant = False
209 for url in imported_page_urls:
210 if url in db_page.get_absolute_url():
211 still_relevant = True
212 break
213 # At this point we know that there's no match and the page
214 # can be deleted.
215 if not still_relevant:
216 self.db_actions.remove_page(db_page.id)
217
218 def publish(self):
219 for md_file in self.md_files:
220 md_file.publish()
221
222 def _create_fake_index_doc(self):
223 '''Creates a fake index page at the top of the branches
224 docs namespace.'''
225
226 if self.docs_namespace == "current":
227 redirect = "/snappy/guides"
228 else:
229 redirect = None
230
231 in_navigation = False
232 menu_title = None
233 list_pages = ""
234 for page in self.md_files:
235 list_pages += "<li><a href=\"%s\">%s</a></li>" \
236 % (os.path.basename(page.full_url), page.title)
237 landing = (
238 u"<div class=\"row\"><div class=\"eight-col\">\n"
239 "<p>This section contains documentation for the "
240 "<code>%s</code> Snappy branch.</p>"
241 "<p><ul class=\"list-ubuntu\">%s</ul></p>\n"
242 "<p>Auto-imported from <a "
243 "href=\"https://github.com/ubuntu-core/snappy\">%s</a>.</p>\n"
244 "</div></div>") % (self.release_alias, list_pages,
245 self.external_branch.lp_origin)
246 self.db_actions.add_page(
247 title=self.index_doc_title, full_url=self.docs_namespace,
248 in_navigation=in_navigation, redirect=redirect, html=landing,
249 menu_title=menu_title)
250
251
252class SnappyLocalBranch(LocalBranch):
253 def __init__(self, dirname, external_branch, db_actions):
254 LocalBranch.__init__(self, dirname, external_branch, db_actions)
255 self.markdown_class = SnappyMarkdownFile
256 self.index_doc_title = 'Snappy documentation'
257 if self.release_alias != 'current':
258 self.index_doc_title += ' (%s)' % self.release_alias
259
260
261def get_or_create_page(title, full_url, menu_title=None,
262 in_navigation=True, redirect=None, html=None):
263 # First check if pages already exist.
264 pages = Title.objects.select_related('page').filter(path__regex=full_url)
265 if pages:
266 page = pages[0].page
267 page.title = title
268 page.publisher_is_draft = True
269 page.menu_title = menu_title
270 page.in_navigation = in_navigation
271 page.redirect = redirect
272 if html:
273 # We create the page, so we know there's just one placeholder
274 placeholder = page.placeholders.all()[0]
275 if placeholder.get_plugins():
276 plugin = placeholder.get_plugins()[0].get_plugin_instance()[0]
277 plugin.body = html
278 plugin.save()
279 else:
280 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
281 else:
282 parent_pages = Title.objects.select_related('page').filter(
283 path__regex=os.path.dirname(full_url))
284 if not parent_pages:
285 print('Parent %s not found.' % os.path.dirname(full_url))
286 sys.exit(1)
287 parent = parent_pages[0].page
288
289 slug = os.path.basename(full_url)
290 page = create_page(
291 title, "default.html", "en", slug=slug, parent=parent,
292 menu_title=menu_title, in_navigation=in_navigation,
293 position="last-child", redirect=redirect)
294 if html:
295 placeholder = page.placeholders.get()
296 add_plugin(placeholder, 'RawHtmlPlugin', 'en', body=html)
297 return page
2987
2998
300def import_branches(selection):9def import_branches(selection):
@@ -302,64 +11,26 @@
302 logging.error('No branches registered in the '11 logging.error('No branches registered in the '
303 'ExternalDocsBranch table yet.')12 'ExternalDocsBranch table yet.')
304 return13 return
305 tempdir = tempfile.mkdtemp()
306 db_actions = DBActions()
307 for branch in ExternalDocsBranch.objects.filter(14 for branch in ExternalDocsBranch.objects.filter(
308 docs_namespace__regex=selection):15 origin__regex=selection, active=True):
309 checkout_location = os.path.join(16 if not process_branch(branch):
310 tempdir, os.path.basename(branch.docs_namespace))
311 sourcecode = SourceCode(branch.lp_origin, checkout_location)
312 if sourcecode.get() != 0:
313 logging.error(
314 'Could not check out branch "%s".' % branch.lp_origin)
315 if os.path.exists(checkout_location):
316 shutil.rmtree(checkout_location)
317 break17 break
318 if branch.lp_origin.startswith('lp:snappy') or \
319 'snappy' in branch.lp_origin.split(':')[1].split('.git')[0].split('/'):
320 local_branch = SnappyLocalBranch(checkout_location, branch,
321 db_actions)
322 else:
323 local_branch = LocalBranch(checkout_location, branch, db_actions)
324 local_branch.import_markdown()
325 local_branch.publish()
326 local_branch.remove_old_pages()
327 shutil.rmtree(tempdir)
328 db_actions.run()
329
330
331class SourceCode():
332 def __init__(self, branch_origin, checkout_location):
333 self.branch_origin = branch_origin
334 self.checkout_location = checkout_location
335
336 def get(self):
337 if self.branch_origin.startswith('lp:') and \
338 os.path.exists('/usr/bin/bzr'):
339 return subprocess.call([
340 'bzr', 'checkout', '--lightweight', self.branch_origin,
341 self.checkout_location])
342 if self.branch_origin.startswith('https://github.com') and \
343 self.branch_origin.endswith('.git') and \
344 os.path.exists('/usr/bin/git'):
345 return subprocess.call([
346 'git', 'clone', '-q', self.branch_origin,
347 self.checkout_location])
348 logging.error(
349 'Branch format "{}" not understood.'.format(self.branch_origin))
350 return 1
35118
35219
353class Command(BaseCommand):20class Command(BaseCommand):
354 help = "Import external branches for documentation."21 help = "Import external branches for documentation."
35522
23 def add_arguments(self, parser):
24 parser.add_argument('branches', nargs='*')
25
356 def handle(*args, **options):26 def handle(*args, **options):
357 logging.basicConfig(27 logging.basicConfig(
358 level=logging.ERROR,28 level=logging.ERROR,
359 format='%(asctime)s %(levelname)-8s %(message)s',29 format='%(asctime)s %(levelname)-8s %(message)s',
360 datefmt='%F %T')30 datefmt='%F %T')
361 if len(args) < 2 or args[1] == "all":31 branches = options['branches']
362 selection = '.*'32 if not branches:
33 import_branches('.*')
363 else:34 else:
364 selection = args[1]35 for b in branches:
365 import_branches(selection)36 import_branches(b)
36637
=== added directory 'md_importer/migrations'
=== added file 'md_importer/migrations/0001_initial.py'
--- md_importer/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
+++ md_importer/migrations/0001_initial.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,46 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import migrations, models
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('cms', '0013_urlconfrevision'),
11 ]
12
13 operations = [
14 migrations.CreateModel(
15 name='ExternalDocsBranch',
16 fields=[
17 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 ('origin', models.CharField(help_text='External branch location, ie: lp:snappy/15.04 or https://github.com/ubuntu-core/snappy.git', max_length=200)),
19 ('branch_name', models.CharField(help_text='For use with git branches, ie: "master" or "15.04" or "1.x".', max_length=200, blank=True)),
20 ('post_checkout_command', models.CharField(help_text='Command to run after checkout of the branch.', max_length=100, blank=True)),
21 ('active', models.BooleanField(default=True)),
22 ],
23 options={
24 'verbose_name': 'external docs branch',
25 'verbose_name_plural': 'external docs branches',
26 },
27 ),
28 migrations.CreateModel(
29 name='ExternalDocsBranchImportDirective',
30 fields=[
31 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
32 ('import_from', models.CharField(help_text='File or directory to import from the branch. Ie: "docs/intro.md" (file) or "docs" (complete directory), etc.', max_length=150, blank=True)),
33 ('write_to', models.CharField(help_text='Article URL (for a specific file) or article namespace for a directory or a set of files.', max_length=150, blank=True)),
34 ('external_docs_branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
35 ],
36 ),
37 migrations.CreateModel(
38 name='ImportedArticle',
39 fields=[
40 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
41 ('last_import', models.DateTimeField(help_text='Datetime of last import.', verbose_name='Datetime')),
42 ('branch', models.ForeignKey(to='md_importer.ExternalDocsBranch')),
43 ('page', models.ForeignKey(to='cms.Page')),
44 ],
45 ),
46 ]
047
=== added file 'md_importer/migrations/__init__.py'
=== added file 'md_importer/models.py'
--- md_importer/models.py 1970-01-01 00:00:00 +0000
+++ md_importer/models.py 2016-01-19 00:21:38 +0000
@@ -0,0 +1,56 @@
1from django.db import models
2from django.utils.translation import ugettext_lazy as _
3
4from cms.models import Page
5
6
7class ExternalDocsBranch(models.Model):
8 origin = models.CharField(
9 max_length=200,
10 help_text=_('External branch location, ie: lp:snappy/15.04 or '
11 'https://github.com/ubuntu-core/snappy.git'))
12 branch_name = models.CharField(
13 max_length=200,
14 help_text=_('For use with git branches, ie: "master" or "15.04" '
15 'or "1.x".'),
16 blank=True)
17 post_checkout_command = models.CharField(
18 max_length=100,
19 help_text=_('Command to run after checkout of the branch.'),
20 blank=True)
21 active = models.BooleanField(default=True)
22
23 def __str__(self):
24 if self.branch_name:
25 return "{} - {}".format(self.origin, self.branch_name)
26 return "{}".format(self.origin)
27
28 class Meta:
29 verbose_name = "external docs branch"
30 verbose_name_plural = "external docs branches"
31
32
33class ExternalDocsBranchImportDirective(models.Model):
34 external_docs_branch = models.ForeignKey(ExternalDocsBranch)
35 import_from = models.CharField(
36 max_length=150,
37 help_text=_('File or directory to import from the branch. '
38 'Ie: "docs/intro.md" (file) or '
39 '"docs" (complete directory), etc.'),
40 blank=True)
41 write_to = models.CharField(
42 max_length=150,
43 help_text=_('Article URL (for a specific file) or article namespace '
44 'for a directory or a set of files.'),
45 blank=True)
46
47 def __str__(self):
48 return "{} -- {}".format(self.external_docs_branch,
49 self.import_from)
50
51
52class ImportedArticle(models.Model):
53 page = models.ForeignKey(Page)
54 branch = models.ForeignKey(ExternalDocsBranch)
55 last_import = models.DateTimeField(
56 _('Datetime'), help_text=_('Datetime of last import.'))
057
=== added directory 'md_importer/tests'
=== added file 'md_importer/tests/__init__.py'
=== added directory 'md_importer/tests/data'
=== added directory 'md_importer/tests/data/link-broken-test'
=== added file 'md_importer/tests/data/link-broken-test/file1.md'
--- md_importer/tests/data/link-broken-test/file1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-broken-test/file1.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,5 @@
1# Test
2
3This is a link to [the file3 file](file3.md).
4
5That's it.
06
=== added file 'md_importer/tests/data/link-broken-test/file2.md'
--- md_importer/tests/data/link-broken-test/file2.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-broken-test/file2.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,3 @@
1# File 2
2
3Here's just some text.
04
=== added directory 'md_importer/tests/data/link-test'
=== added file 'md_importer/tests/data/link-test/file1.md'
--- md_importer/tests/data/link-test/file1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-test/file1.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,5 @@
1# Test
2
3This is a link to [the file2 file](file2.md).
4
5That's it.
06
=== added file 'md_importer/tests/data/link-test/file2.md'
--- md_importer/tests/data/link-test/file2.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link-test/file2.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,3 @@
1# File 2
2
3Here's just some text.
04
=== added directory 'md_importer/tests/data/link2-test'
=== added file 'md_importer/tests/data/link2-test/file1.md'
--- md_importer/tests/data/link2-test/file1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link2-test/file1.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,5 @@
1# Test
2
3This is a link to [the file3 file](file3.md).
4
5That's it.
06
=== added file 'md_importer/tests/data/link2-test/file3.md'
--- md_importer/tests/data/link2-test/file3.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/link2-test/file3.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,3 @@
1# File 3
2
3Here's just some text.
04
=== added directory 'md_importer/tests/data/local-image-test'
=== added file 'md_importer/tests/data/local-image-test/test1.md'
--- md_importer/tests/data/local-image-test/test1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/local-image-test/test1.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,6 @@
1# Local image test
2
3Here is a local image:
4
5![Alt text for a remote image](img.jpg)
6
07
=== added directory 'md_importer/tests/data/remote-image-test'
=== added file 'md_importer/tests/data/remote-image-test/test1.md'
--- md_importer/tests/data/remote-image-test/test1.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/remote-image-test/test1.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,6 @@
1# Local image test
2
3Here is a local image:
4
5![Alt text for a remote image](http://design.ubuntu.com/wp-content/uploads/ubuntu-logo112.png)
6
07
=== added directory 'md_importer/tests/data/snapcraft-test'
=== added file 'md_importer/tests/data/snapcraft-test/CONTRIBUTING.md'
--- md_importer/tests/data/snapcraft-test/CONTRIBUTING.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/CONTRIBUTING.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,32 @@
1# Snapcraft Contribution Guide
2
3Welcome to Snapcraft! We're a pretty friendly community and we're thrilled that
4you want to make Snapcraft even better. However, we do ask that you follow some
5general guidelines while doing so, just so we can keep things organized around
6here.
7
81. Sign the [contributor license agreement][1].
9
10 This is how you give us permission to use your contributions.
11
122. We use a forking, feature-based workflow.
13
14 Make a fork of Snapcraft, and create a branch named specifically for the
15 feature on which you'd like to work. Make your changes there, adding new
16 tests as needed, and make sure the existing tests continue to pass when your
17 changes are complete (for information about running the tests, see the
18 [HACKING][2] document).
19
203. Squash commits into one, well-formatted commit.
21
22 If you really feel like there should be more than one commit in your branch,
23 then you're probably trying to introduce more than one feature and you should
24 make another branch for it.
25
264. Submit a pull request to get changes from your branch into master.
27
28 If you want to get the change into 1.x as well, make a note of it on the pull
29 request and it can be cherry-picked after the merge.
30
31[1]: http://www.ubuntu.com/legal/contributors/
32[2]: HACKING.md
033
=== added file 'md_importer/tests/data/snapcraft-test/HACKING.md'
--- md_importer/tests/data/snapcraft-test/HACKING.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/HACKING.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,48 @@
1# Snapcraft
2
3## Running
4
5To see all the commands and options, run `snapcraft --help`.
6
7## Testing
8
9Simply run the top level testing script:
10
11 ./runtests.sh
12
13- If you want to get a test coverage report, install python3-coverage before running the tests:
14
15 sudo apt-get install python3-coverage
16
17
18- 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.
19
20- If you are on 15.04 or earlier, you will need to run:
21
22 sudo add-apt-repository ppa:hardware-certification/public
23
24### PPA
25
26You can install the daily build PPA by running:
27
28 sudo add-apt-repository ppa:snappy-dev/snapcraft-daily
29
30## Hacking
31
32We'd love the help!
33
34- Submit pull requests against [snapcraft](https://github.com/ubuntu-core/snapcraft/pulls)
35- Our mailing list is snappy-devel@lists.ubuntu.com
36- We can also be found on the #snappy IRC channel on Freenode
37
38### Project Layout
39
40- **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.
41
42- **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.
43
44- **plugins:** Holds yaml metadata for the current snapcraft plugins.
45
46- **tests:** Tests, obviously. `unit` holds Python unit tests and `plainbox` holds plainbox integration tests.
47
48- **snapcraft:** The Python module that houses the core snapcraft logic. The `plugins` subdirectory holds the code for each plugin.
049
=== added file 'md_importer/tests/data/snapcraft-test/README.md'
--- md_importer/tests/data/snapcraft-test/README.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/README.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,33 @@
1[![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]
2
3# Snapcraft
4
5Snapcraft is a delightful packaging tool
6
7Snapcraft helps you assemble a whole project in a single tree out of
8many pieces. It can drive a very wide range of build and packaging systems,
9so that you can simply list all the upstream projects you want and have
10them built and installed together as a single tree.
11
12![Snapcraft Overview][overview-image]
13
14For example, say you want to make a product that includes PyPI packages,
15Node.js packages from NPM, Java, and a bunch of daemons written in C and
16C++ that are built with autotools, snapcraft would make assembling the
17final tree very easy.
18
19Snapcraft allows easy crafting of snap packages for the [snappy Ubuntu Core](http://ubuntu.com/snappy)
20transactional update system.
21
22## More Information
23
24* [Introduction](docs/intro.md) to all the details about the concepts behind snapcraft.
25* [Hacking guide](HACKING.md) to contribute if you're interested in developing Snapcraft.
26
27[travis-image]: https://travis-ci.org/ubuntu-core/snapcraft.svg?branch=master
28[travis-url]: https://travis-ci.org/ubuntu-core/snapcraft
29
30[coveralls-image]: https://coveralls.io/repos/ubuntu-core/snapcraft/badge.svg?branch=master&service=github
31[coveralls-url]: https://coveralls.io/github/ubuntu-core/snapcraft?branch=master
32
33[overview-image]: https://rawgit.com/ted-gould/snapcraft/snapcraft-overview-diagram/docs/snapcraft%20overview.svg
034
=== added directory 'md_importer/tests/data/snapcraft-test/docs'
=== added file 'md_importer/tests/data/snapcraft-test/docs/get-started.md'
--- md_importer/tests/data/snapcraft-test/docs/get-started.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/get-started.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,47 @@
1# Setting up your Ubuntu development host
2
3Ubuntu is a great and convenient OS for developers. Snappy developer tools are
4readily available to enable app developers familiar with Ubuntu to port and
5write new software for a snappy-based system easily.
6
7For app developers that want the latest stable tools to work on Snappy
8technology, we recommend to use the latest classic Ubuntu Long-Term Support
9(LTS) release as the host. At the time of writing this is Ubuntu 14.04 LTS. For
10those not using an Ubuntu machine (and you should), you can use a VM
11(VirtualBox, VMware, Vagrant) to execute your Ubuntu development host.
12
13This version of snapcraft only works on Ubuntu 16.04 (Xenial Xerus), for
14previous versions of snapcraft, refer to the
15[1.x documentation](https://github.com/ubuntu-core/snapcraft/blob/1.x/docs/get-started.md).
16
17Once your Ubuntu host system is up and running, you can then enable the
18snappy-tools PPA to get the latest tools to develop for Snappy. A PPA is a
19Personal Package Archive that developers can subscribe to install and get
20frequent updates of the software the archive contains. Open up a terminal with
21`Ctrl+Alt+T` and type the following command to add the snappy-tools PPA to
22your system:
23
24 $ sudo apt-add-repository ppa:snappy-dev/tools
25 $ sudo apt update
26
27After that, running the following command will install the `snappy-tools`
28package, which will in turn install the optimal selection of Snappy development
29software to your system.
30
31 $ sudo apt install snappy-tools
32
33The snappy-tools PPA is officially supported by the Snappy Core team for
34Ubuntu LTS releases. In addition to it, we try to keep snappy-tools also
35conveniently available for the latest Ubuntu stable release as well as the
36current development release for those who prefer those as host. For a
37production environment however, we recommend using an Ubuntu LTS-based host.
38
39This is the most important selection of tools you will get after installation:
40
41 snappy try - try snaps from a .snap, the [stage] or [snap] dir
42 snappy-remote - run snappy operations on remote snappy target by IP
43 snapcraft - the snap build tool for all snaps
44
45# Next
46
47How about putting together [your first snap](your-first-snap.md) now?
048
=== added file 'md_importer/tests/data/snapcraft-test/docs/intro.md'
--- md_importer/tests/data/snapcraft-test/docs/intro.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/intro.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,104 @@
1# Intro
2
3Snapcraft is a build and packaging tool which helps you package your software
4as a snap. It makes it easy to incorporate components from different sources
5and build technologies or solutions.
6
7# Key concepts
8
9A `.snap` package for the Ubuntu Core system contains all its
10dependencies. This has a couple of advantages over traditional `deb` or
11`rpm` based dependency handling, the most important being that a
12developer can always be assured that there are no regressions triggered by
13changes to the system underneath their app.
14
15Snapcraft makes bundling these dependencies easy by allowing you to
16specify them as "parts" in the `snapcraft.yaml` file.
17
18# Snappy
19
20Snappy Ubuntu Core is a new rendition of Ubuntu with transactional updates - a
21minimal server image with the same libraries as today's Ubuntu, but
22applications are provided through a simpler mechanism.
23
24Snappy apps and Ubuntu Core itself can be upgraded atomically and rolled back
25if needed. Apps are also strictly confined and sandboxed to safeguard your
26data and system.
27
28## Parts
29
30A central aspect of a snapcraft recipe is a "part". A part is a piece
31of software or data that the snap package requires to work or to
32build other parts. Each part is managed by a snapcraft plugin and parts
33are usually independent of each other.
34
35## Plugin
36
37Each part has a `plugin` associated to it, this `plugin` provides the mechanism
38to handle it. Parts are driven through plugins, there are a variety of plugins
39already included for python 2 and 3, go, java, and cmake or autotools based
40projects.
41
42## Lifecycle
43
44Each part goes through the following steps:
45
46### Pull
47
48The first is that each part is pulled. This step will download
49content, e.g. checkout a git repository or download a binary component
50like the Java SDK. Snapcraft will create a `parts/` directory with
51sub-directories like `parts/part-name/src` for each part that contains
52the downloaded content.
53
54### Build
55
56The next step is that each part is built in its `parts/part-name/build`
57directory and installs itself into `parts/part-name/install`.
58
59### Stage
60
61After the build of each part the parts are combined into a single
62directory tree that is called the "staging area". It can be found
63under the `./stage` directory.
64
65This `./stage` directory is useful for building outside code that isn't in the
66`snapcraft.yaml` recipe against the snap contents. For example, you might
67build a local project against the libraries in `./stage` by running `snapcraft
68shell make`. Though in general, you are encouraged to add even local
69projects to snapcraft.yaml with a local `source:` path.
70
71For rapid iteration one can run `snappy try` against this directory to have it
72mounted in a `snappy` capable system.
73
74### Strip
75
76The strip step moves the data into a `./snap` directory. It contains only
77the content that will be put into the final snap package, unlike the staging
78area which may include some development files not destined for your package.
79
80The Snappy metadata information about your project will also now be placed in
81`./snap/meta`. Snapcraft takes care of generating all the meta-data Snappy
82expects. For a breakdown of what this is, have a look at our [Snappy developer
83reference](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
84
85This `./snap` directory is useful for inspecting what is going into your snap
86and to make any final post-processing on snapcraft's output.
87
88For rapid iteration one can run `snappy try` against this directory to have it
89mounted in a `snappy` capable system.
90
91### Snap
92
93The final step builds a snap package out of the `snap` directory. This `.snap`
94file can be uploaded to the Ubuntu Store and published directly to Snappy
95users.
96
97This command can also be used with a directory argument for projects that
98are not following the snapcraft lifecycle but which follow the internal
99snap format.
100
101# Next
102
103After introducing the key concept of snapcraft it is probably a good
104time [get set up](get-started.md) to create your first snap with snapcraft.
0105
=== added file 'md_importer/tests/data/snapcraft-test/docs/ros-snap.md'
--- md_importer/tests/data/snapcraft-test/docs/ros-snap.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/ros-snap.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,366 @@
1# Using Snappy with ROS
2
3The [Robot Operating System][1] (ROS) is an open-source collection of tools and
4libraries meant to aid in the development of complex robotic systems. ROS is
5excellent at what it does, but there are a few things it doesn't do:
6
7- Security: How does one prevent a rogue application from interfering with one's
8 finely-honed ROS ecosystem? Confinement and access control isn't one of the
9 many problems solved by ROS.
10
11- Deployment: How does one deploy an entire ROS project in one step, without
12 worrying about dependencies?
13
14- Updating: How does one allow end-users to update the entire ROS project in one
15 step, while retaining for the possibility of rolling back the change if
16 the update goes sideways?
17
18These are all problems solved by Snappy; the combination of the two is a perfect
19match for consumer robotic systems.
20
21
22## Package your current ROS project as a .snap
23
24Let's assume you already have a ROS project. It can be as simple or as
25complicated as you like, but for this example, our project will be made up of:
26
27- A ROS package containing a C++ "talker."
28- A ROS package containing a Python "listener," as well as a launch file to
29 bring both the talker and listener up at the same time with roscore.
30
31Our objective will be to create a .snap containing these pieces and their
32dependencies. The easiest way to do that is with Snapcraft, using its Catkin
33plugin. But before we get to that, let's create our project. Prerequisites for
34this walkthrough:
35
36- Installed Snapcraft (see [Getting Started][2]).
37- Installed/configured ROS Indigo (see the [Indigo installation tutorial][3]).
38- An empty ROS workspace (see the Catkin [workspace tutorial][4]).
39- General ROS experience (at least go through the [C++ pub/sub tutorial][5]).
40- General Snapcraft experience (read [Your first snap][6]).
41
42
43### C++ "talker"
44
45First, create the package:
46
47 $ catkin_create_pkg talker roscpp std_msgs
48
49Now make sure the `package.xml` is setup correctly (comments removed for
50brevity):
51
52```xml
53<?xml version="1.0"?>
54<package>
55 <name>talker</name>
56 <version>0.0.0</version>
57 <description>The talker package</description>
58 <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
59 <license>TODO</license>
60 <buildtool_depend>catkin</buildtool_depend>
61 <build_depend>roscpp</build_depend>
62 <build_depend>std_msgs</build_depend>
63 <run_depend>roscpp</run_depend>
64 <run_depend>std_msgs</run_depend>
65</package>
66```
67
68Also make sure the `CMakeLists.txt` is setup correctly. Importantly, make sure
69that you've specified install rules, since Snapcraft uses `make install`.
70Anything that doesn't have an install rule won't end up in the final .snap:
71
72```cmake
73cmake_minimum_required(VERSION 2.8.3)
74project(talker)
75
76find_package(catkin REQUIRED COMPONENTS
77 roscpp
78 std_msgs
79)
80
81catkin_package()
82
83include_directories(${catkin_INCLUDE_DIRS})
84
85add_executable(talker_node src/talker_node.cpp)
86
87target_link_libraries(talker_node ${catkin_LIBRARIES})
88
89install(TARGETS talker_node
90 ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
91 LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
92 RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
93)
94```
95
96Finally, for `src/talker_node.cpp`, we have this:
97
98```cpp
99#include <sstream>
100
101#include <ros/ros.h>
102#include <std_msgs/String.h>
103
104int main(int argc, char **argv)
105{
106 ros::init(argc, argv, "talker");
107
108 ros::NodeHandle nodeHandle;
109
110 ros::Publisher publisher = nodeHandle.advertise<std_msgs::String>("chatter", 1);
111
112 ros::Rate loopRate(10);
113
114 int count = 0;
115 while (ros::ok())
116 {
117 std_msgs::String message;
118
119 std::stringstream stream;
120 stream << "Hello world " << count++;
121 message.data = stream.str();
122
123 ROS_INFO("%s", message.data.c_str());
124
125 publisher.publish(message);
126
127 ros::spinOnce();
128
129 loopRate.sleep();
130 }
131
132 return 0;
133}
134```
135
136The "talker" is now complete.
137
138
139### The Python "listener"
140
141First, create the package:
142
143 $ catkin_create_pkg listener rospy std_msgs
144
145Now make sure the `package.xml` is setup correctly (comments removed for
146brevity):
147
148```xml
149<?xml version="1.0"?>
150<package>
151 <name>listener</name>
152 <version>0.0.0</version>
153 <description>The listener package</description>
154 <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
155 <license>TODO</license>
156 <buildtool_depend>catkin</buildtool_depend>
157 <build_depend>rospy</build_depend>
158 <build_depend>std_msgs</build_depend>
159 <run_depend>rospy</run_depend>
160 <run_depend>std_msgs</run_depend>
161</package>
162```
163
164Also make sure the `CMakeLists.txt` is setup correctly. Again, it's important to remember the install rules:
165
166```cmake
167cmake_minimum_required(VERSION 2.8.3)
168project(listener)
169
170find_package(catkin REQUIRED COMPONENTS
171 rospy
172 std_msgs
173)
174
175catkin_package()
176
177install(PROGRAMS
178 scripts/listener_node
179 DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
180)
181
182install(FILES
183 talk_and_listen.launch
184 DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
185)
186```
187
188Now for `scripts/listener_node`, we have this:
189
190```python
191#!/usr/bin/env python
192
193import rospy
194from std_msgs.msg import String
195
196def callback(data):
197 rospy.loginfo('I heard %s', data.data)
198
199def listener():
200 rospy.init_node('listener')
201
202 rospy.Subscriber('babble', String, callback)
203
204 rospy.spin()
205
206if __name__ == '__main__':
207 listener()
208```
209
210Make sure this script is executable:
211
212 $ chmod +x scripts/listener_node
213
214Note that the listener subscribes to the `babble` topic, but the talker
215publishes on `chatter`. We need to make sure we account for that in
216`talk_and_listen.launch`:
217
218```xml
219<launch>
220 <node name = "talker" pkg = "talker" type = "talker_node"
221 output = "screen" />
222
223 <node name = "listener" pkg = "listener" type = "listener_node"
224 output = "screen">
225 <remap from = "babble" to = "chatter" />
226 </node>
227</launch>
228```
229
230The "listener" is now complete.
231
232
233### Verify functionality
234
235You can verify that everything works by getting in the workspace root and
236running:
237
238 $ catkin_make
239 $ catkin_make install
240 $ roslaunch listener talk_and_listen.launch
241
242You should see them communicating, looking something like this:
243
244 ...
245 [INFO] [WallTime: 1449512660.316140] I heard Hello world 2
246 [ INFO] [1449512660.415941529]: Hello world 3
247 [INFO] [WallTime: 1449512660.416330] I heard Hello world 3
248 [ INFO] [1449512660.515954882]: Hello world 4
249 [INFO] [WallTime: 1449512660.516307] I heard Hello world 4
250 [ INFO] [1449512660.615954473]: Hello world 5
251 [INFO] [WallTime: 1449512660.616306] I heard Hello world 5
252 ...
253
254### Put it all in a .snap
255
256Let's get back to our stated objective, which was to create a .snap containing
257these pieces and their dependencies. As mentioned, the easiest way to create a
258.snap with complex dependencies is to use Snapcraft. Snapcraft contains a plugin
259especially for Catkin, which makes creating a ROS .snap particularly easy.
260
261We tell Snapcraft how to create the .snap via a file named `snapcraft.yaml`;
262let's create that file in the workspace root, containing the following:
263
264```yaml
265name: ros-talker-and-listener
266version: 1.0
267summary: ROS Example
268description: Contains talker/listener ROS packages and a .launch file.
269
270binaries:
271 launch-project:
272 exec: roslaunch listener talk_and_listen.launch
273
274parts:
275 foo:
276 plugin: catkin
277 source: .
278 catkin-packages:
279 - talker
280 - listener
281 stage-packages:
282 - ros-indigo-ros-core
283```
284
285Most of this file should look familiar to you if you've met the prerequisites,
286but let's focus on a few specific pieces.
287
288```yaml
289# ...
290binaries:
291 launch-project:
292 exec: roslaunch listener talk_and_listen.launch
293# ...
294```
295
296Even though the `talker` and `listener` packages will be installed, they'll be
297within a confined ROS installation, so the user won't be able to simply call
298`rosrun` or `roslaunch`. Instead, you have control over how your .snap is used,
299and here we specify that we only want a single binary, called "launch-project",
300which results in the `roslaunch` call you see. If this seems confusing now, it
301will make more sense when we actually use it.
302
303```yaml
304# ...
305parts:
306 foo:
307 plugin: catkin
308 source: .
309 catkin-packages:
310 - talker
311 - listener
312 stage-packages:
313 - ros-indigo-ros-core
314# ...
315```
316
317This is specifying that the .snap is made up of a single part, called "foo,"
318which utilizes the Catkin plugin. It states that the workspace is in the same
319path as the `snapcraft.yaml`, and it specifies which ROS packages should be
320included in the .snap (`talker` and `listener`). Finally, and this is important,
321it specifies that the Ubuntu package containing `roscore` (ros-indigo-ros-core)
322should be installed into the .snap.
323
324That last point is worth discussing. Currently, since .snaps cannot depend upon
325each other, any .snap that uses roscore must distribute roscore within it. We're
326working on some new features that will enable sharing roscore between snaps, but
327until then there are some limitations to keep in mind:
328
329- `roscore` must be bundled into each .snap that requires it.
330- Snappy's port negotiation feature is still a work-in-progress, which means
331 that only a single .snap that runs `roscore` can be installed at a time or
332 they will fight for the same port.
333
334Now that we understand the `snapcraft.yaml`, let's create the .snap!
335
336 $ snapcraft snap
337
338This will take some time to pull down the dependencies etc., but in the end
339you'll have a .snap.
340
341
342### Take the .snap for a test drive
343
344You can transfer your newly-minted .snap to your Ubuntu Core machine and install
345it at the same time via `snappy-remote`, for example:
346
347 $ snappy-remote --url=ssh://<host>:<port> install \
348 ros-talker-and-listener_1.0_amd64.snap
349
350Now on the Ubuntu Core machine, take a look in `/apps/bin/`, and you'll see the
351binary you requested, called `ros-talker-and-listener.launch-project`. Test it
352out:
353
354 $ ros-talker-and-listener.launch-project
355
356And you should see the talker and listener communicating like before. As usual,
357ctrl+c will stop it. Note also that, since ROS is running in a confined
358environment, its log isn't in `$HOME/.ros` as usual, but in
359`$HOME/apps/ros-talker-and-listener.sideload/1.0/ros`.
360
361[1]: http://www.ros.org/
362[2]: get-started.md
363[3]: http://wiki.ros.org/indigo/Installation/Ubuntu
364[4]: http://wiki.ros.org/catkin/Tutorials/create_a_workspace
365[5]: http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber%28c%2B%2B%29
366[6]: your-first-snap.md
0367
=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg'
--- md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/snapcraft overview.svg 2016-01-19 00:21:38 +0000
@@ -0,0 +1,1199 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 xmlns:xlink="http://www.w3.org/1999/xlink"
11 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
12 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13 width="629.42749"
14 height="583.61816"
15 viewBox="0 0 629.4275 583.61817"
16 id="svg2"
17 version="1.1"
18 inkscape:version="0.91 r13725"
19 sodipodi:docname="snapcraft overview.svg">
20 <defs
21 id="defs4">
22 <marker
23 inkscape:isstock="true"
24 style="overflow:visible"
25 id="marker9243"
26 refX="0"
27 refY="0"
28 orient="auto"
29 inkscape:stockid="Arrow2Mend">
30 <path
31 inkscape:connector-curvature="0"
32 transform="scale(-0.6,-0.6)"
33 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"
34 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
35 id="path9245" />
36 </marker>
37 <marker
38 inkscape:isstock="true"
39 style="overflow:visible"
40 id="marker9024"
41 refX="0"
42 refY="0"
43 orient="auto"
44 inkscape:stockid="Arrow2Mend">
45 <path
46 inkscape:connector-curvature="0"
47 transform="scale(-0.6,-0.6)"
48 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"
49 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
50 id="path9026" />
51 </marker>
52 <marker
53 inkscape:isstock="true"
54 style="overflow:visible"
55 id="marker8885"
56 refX="0"
57 refY="0"
58 orient="auto"
59 inkscape:stockid="Arrow2Mend">
60 <path
61 inkscape:connector-curvature="0"
62 transform="scale(-0.6,-0.6)"
63 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"
64 style="fill:#ffffff;fill-opacity:0;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
65 id="path8887" />
66 </marker>
67 <linearGradient
68 inkscape:collect="always"
69 id="linearGradient7897">
70 <stop
71 style="stop-color:#ffffff;stop-opacity:0"
72 offset="0"
73 id="stop7899" />
74 <stop
75 style="stop-color:#ffffff;stop-opacity:1"
76 offset="1"
77 id="stop7901" />
78 </linearGradient>
79 <marker
80 inkscape:isstock="true"
81 style="overflow:visible"
82 id="marker7803"
83 refX="0"
84 refY="0"
85 orient="auto"
86 inkscape:stockid="Arrow2Mend">
87 <path
88 inkscape:connector-curvature="0"
89 transform="scale(-0.6,-0.6)"
90 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"
91 style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
92 id="path7805" />
93 </marker>
94 <marker
95 inkscape:isstock="true"
96 style="overflow:visible"
97 id="marker5350"
98 refX="0"
99 refY="0"
100 orient="auto"
101 inkscape:stockid="Arrow2Mend">
102 <path
103 transform="scale(-0.6,-0.6)"
104 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"
105 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
106 id="path5352"
107 inkscape:connector-curvature="0" />
108 </marker>
109 <marker
110 inkscape:isstock="true"
111 style="overflow:visible"
112 id="marker5271"
113 refX="0"
114 refY="0"
115 orient="auto"
116 inkscape:stockid="Arrow2Mend">
117 <path
118 transform="scale(-0.6,-0.6)"
119 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"
120 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
121 id="path5273"
122 inkscape:connector-curvature="0" />
123 </marker>
124 <marker
125 inkscape:isstock="true"
126 style="overflow:visible"
127 id="marker5198"
128 refX="0"
129 refY="0"
130 orient="auto"
131 inkscape:stockid="Arrow2Mend">
132 <path
133 transform="scale(-0.6,-0.6)"
134 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"
135 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
136 id="path5200"
137 inkscape:connector-curvature="0" />
138 </marker>
139 <marker
140 inkscape:isstock="true"
141 style="overflow:visible"
142 id="marker4983"
143 refX="0"
144 refY="0"
145 orient="auto"
146 inkscape:stockid="Arrow2Mend">
147 <path
148 transform="scale(-0.6,-0.6)"
149 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"
150 style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
151 id="path4985"
152 inkscape:connector-curvature="0" />
153 </marker>
154 <marker
155 inkscape:isstock="true"
156 style="overflow:visible"
157 id="marker4880"
158 refX="0"
159 refY="0"
160 orient="auto"
161 inkscape:stockid="Arrow2Mend">
162 <path
163 transform="scale(-0.6,-0.6)"
164 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"
165 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
166 id="path4882"
167 inkscape:connector-curvature="0" />
168 </marker>
169 <marker
170 inkscape:stockid="Arrow2Mend"
171 orient="auto"
172 refY="0"
173 refX="0"
174 id="marker4828"
175 style="overflow:visible"
176 inkscape:isstock="true"
177 inkscape:collect="always">
178 <path
179 id="path4830"
180 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
181 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"
182 transform="scale(-0.6,-0.6)"
183 inkscape:connector-curvature="0" />
184 </marker>
185 <marker
186 inkscape:isstock="true"
187 style="overflow:visible"
188 id="marker4782"
189 refX="0"
190 refY="0"
191 orient="auto"
192 inkscape:stockid="Arrow2Mend"
193 inkscape:collect="always">
194 <path
195 transform="scale(-0.6,-0.6)"
196 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"
197 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
198 id="path4784"
199 inkscape:connector-curvature="0" />
200 </marker>
201 <marker
202 inkscape:stockid="Arrow2Mend"
203 orient="auto"
204 refY="0"
205 refX="0"
206 id="Arrow2Mend"
207 style="overflow:visible"
208 inkscape:isstock="true"
209 inkscape:collect="always">
210 <path
211 id="path4401"
212 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
213 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"
214 transform="scale(-0.6,-0.6)"
215 inkscape:connector-curvature="0" />
216 </marker>
217 <marker
218 inkscape:stockid="Arrow1Lend"
219 orient="auto"
220 refY="0"
221 refX="0"
222 id="Arrow1Lend"
223 style="overflow:visible"
224 inkscape:isstock="true">
225 <path
226 id="path4377"
227 d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"
228 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
229 transform="matrix(-0.8,0,0,-0.8,-10,0)"
230 inkscape:connector-curvature="0" />
231 </marker>
232 <marker
233 inkscape:stockid="Arrow2Mend"
234 orient="auto"
235 refY="0"
236 refX="0"
237 id="Arrow2Mend-9"
238 style="overflow:visible"
239 inkscape:isstock="true">
240 <path
241 inkscape:connector-curvature="0"
242 id="path4401-8"
243 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
244 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"
245 transform="scale(-0.6,-0.6)" />
246 </marker>
247 <linearGradient
248 id="linearGradient2795">
249 <stop
250 id="stop2797"
251 offset="0"
252 style="stop-color:#b8b8b8;stop-opacity:0.49803922;" />
253 <stop
254 id="stop2799"
255 offset="1"
256 style="stop-color:#7f7f7f;stop-opacity:0;" />
257 </linearGradient>
258 <linearGradient
259 id="linearGradient2787">
260 <stop
261 id="stop2789"
262 offset="0"
263 style="stop-color:#7f7f7f;stop-opacity:0.5;" />
264 <stop
265 id="stop2791"
266 offset="1"
267 style="stop-color:#7f7f7f;stop-opacity:0;" />
268 </linearGradient>
269 <linearGradient
270 id="linearGradient3676">
271 <stop
272 id="stop3678"
273 offset="0"
274 style="stop-color:#b2b2b2;stop-opacity:0.5;" />
275 <stop
276 id="stop3680"
277 offset="1"
278 style="stop-color:#b3b3b3;stop-opacity:0;" />
279 </linearGradient>
280 <linearGradient
281 id="linearGradient3236">
282 <stop
283 id="stop3244"
284 offset="0"
285 style="stop-color:#f4f4f4;stop-opacity:1" />
286 <stop
287 id="stop3240"
288 offset="1"
289 style="stop-color:white;stop-opacity:1" />
290 </linearGradient>
291 <linearGradient
292 id="linearGradient4671">
293 <stop
294 id="stop4673"
295 offset="0"
296 style="stop-color:#ffd43b;stop-opacity:1;" />
297 <stop
298 id="stop4675"
299 offset="1"
300 style="stop-color:#ffe873;stop-opacity:1" />
301 </linearGradient>
302 <linearGradient
303 id="linearGradient4689">
304 <stop
305 id="stop4691"
306 offset="0"
307 style="stop-color:#5a9fd4;stop-opacity:1;" />
308 <stop
309 id="stop4693"
310 offset="1"
311 style="stop-color:#306998;stop-opacity:1;" />
312 </linearGradient>
313 <linearGradient
314 gradientTransform="translate(100.2702,99.61116)"
315 gradientUnits="userSpaceOnUse"
316 xlink:href="#linearGradient4671"
317 id="linearGradient2987"
318 y2="144.75717"
319 x2="-65.308502"
320 y1="144.75717"
321 x1="224.23996" />
322 <linearGradient
323 gradientTransform="translate(100.2702,99.61116)"
324 gradientUnits="userSpaceOnUse"
325 xlink:href="#linearGradient4689"
326 id="linearGradient2990"
327 y2="76.313133"
328 x2="26.670298"
329 y1="77.475983"
330 x1="172.94208" />
331 <linearGradient
332 y2="144.75717"
333 x2="-65.308502"
334 y1="144.75717"
335 x1="224.23996"
336 gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
337 gradientUnits="userSpaceOnUse"
338 id="linearGradient2255"
339 xlink:href="#linearGradient4671"
340 inkscape:collect="always" />
341 <linearGradient
342 y2="76.313133"
343 x2="26.670298"
344 y1="76.176224"
345 x1="172.94208"
346 gradientTransform="matrix(0.562541,0,0,0.567972,-11.5974,-7.60954)"
347 gradientUnits="userSpaceOnUse"
348 id="linearGradient2258"
349 xlink:href="#linearGradient4689"
350 inkscape:collect="always" />
351 <radialGradient
352 gradientUnits="userSpaceOnUse"
353 gradientTransform="matrix(1,0,0,0.177966,0,108.7434)"
354 r="29.036913"
355 fy="132.28575"
356 fx="61.518883"
357 cy="132.28575"
358 cx="61.518883"
359 id="radialGradient2801"
360 xlink:href="#linearGradient2795"
361 inkscape:collect="always" />
362 <linearGradient
363 y2="137.27299"
364 x2="112.03144"
365 y1="192.35176"
366 x1="150.96111"
367 gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
368 gradientUnits="userSpaceOnUse"
369 id="linearGradient1475"
370 xlink:href="#linearGradient4671"
371 inkscape:collect="always" />
372 <linearGradient
373 y2="114.39767"
374 x2="135.66525"
375 y1="20.603781"
376 x1="26.648937"
377 gradientTransform="matrix(0.562541,0,0,0.567972,-9.399749,-5.305317)"
378 gradientUnits="userSpaceOnUse"
379 id="linearGradient1478"
380 xlink:href="#linearGradient4689"
381 inkscape:collect="always" />
382 <radialGradient
383 r="29.036913"
384 fy="132.28575"
385 fx="61.518883"
386 cy="132.28575"
387 cx="61.518883"
388 gradientTransform="matrix(2.382716e-8,-0.296405,1.43676,4.683673e-7,-128.544,150.5202)"
389 gradientUnits="userSpaceOnUse"
390 id="radialGradient1480"
391 xlink:href="#linearGradient2795"
392 inkscape:collect="always" />
393 <linearGradient
394 inkscape:collect="always"
395 xlink:href="#linearGradient7897"
396 id="linearGradient7903"
397 x1="574.78748"
398 y1="518.93152"
399 x2="627.95801"
400 y2="543.29303"
401 gradientUnits="userSpaceOnUse"
402 gradientTransform="translate(0,0.39292731)" />
403 <marker
404 inkscape:isstock="true"
405 style="overflow:visible"
406 id="marker9243-9"
407 refX="0"
408 refY="0"
409 orient="auto"
410 inkscape:stockid="Arrow2Mend">
411 <path
412 inkscape:connector-curvature="0"
413 transform="scale(-0.6,-0.6)"
414 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"
415 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
416 id="path9245-6" />
417 </marker>
418 </defs>
419 <sodipodi:namedview
420 id="base"
421 pagecolor="#ffffff"
422 bordercolor="#666666"
423 borderopacity="1.0"
424 inkscape:pageopacity="1"
425 inkscape:pageshadow="2"
426 inkscape:zoom="1.5438176"
427 inkscape:cx="314.71375"
428 inkscape:cy="291.80908"
429 inkscape:document-units="px"
430 inkscape:current-layer="layer1"
431 showgrid="false"
432 units="px"
433 inkscape:showpageshadow="false"
434 showguides="false"
435 inkscape:guide-bbox="true"
436 inkscape:window-width="1486"
437 inkscape:window-height="1036"
438 inkscape:window-x="940"
439 inkscape:window-y="212"
440 inkscape:window-maximized="0"
441 fit-margin-top="20"
442 fit-margin-left="20"
443 fit-margin-right="20"
444 fit-margin-bottom="20"
445 showborder="true">
446 <sodipodi:guide
447 position="470.86538,187.82982"
448 orientation="1,0"
449 id="guide4356" />
450 <sodipodi:guide
451 position="586.76952,189.57274"
452 orientation="0,1"
453 id="guide4358" />
454 <sodipodi:guide
455 position="381.97649,255.80368"
456 orientation="1,0"
457 id="guide4360" />
458 <sodipodi:guide
459 position="548.4253,272.36142"
460 orientation="1,0"
461 id="guide4362" />
462 <sodipodi:guide
463 position="508.33815,349.92133"
464 orientation="0,1"
465 id="guide4364" />
466 <sodipodi:guide
467 position="612.91332,113.75575"
468 orientation="0,1"
469 id="guide4366" />
470 <sodipodi:guide
471 position="548.4253,36.195832"
472 orientation="0,1"
473 id="guide4688" />
474 <sodipodi:guide
475 position="186.76952,113.75575"
476 orientation="1,0"
477 id="guide4690" />
478 <sodipodi:guide
479 position="68.250992,243.60325"
480 orientation="1,0"
481 id="guide4694" />
482 <sodipodi:guide
483 position="420.02074,458.45319"
484 orientation="0,1"
485 id="guide5148" />
486 <sodipodi:guide
487 position="170.49847,514.08991"
488 orientation="1,0"
489 id="guide5150" />
490 <sodipodi:guide
491 position="222.20129,491.61043"
492 orientation="1,0"
493 id="guide5152" />
494 <sodipodi:guide
495 position="160.94469,342.68385"
496 orientation="0,1"
497 id="guide5154" />
498 <sodipodi:guide
499 position="400.91318,430.63483"
500 orientation="0,1"
501 id="guide5156" />
502 <sodipodi:guide
503 position="303.40842,400.56852"
504 orientation="1,0"
505 id="guide5158" />
506 </sodipodi:namedview>
507 <metadata
508 id="metadata7">
509 <rdf:RDF>
510 <cc:Work
511 rdf:about="">
512 <dc:format>image/svg+xml</dc:format>
513 <dc:type
514 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
515 <dc:title></dc:title>
516 </cc:Work>
517 </rdf:RDF>
518 </metadata>
519 <g
520 inkscape:label="Layer 1"
521 inkscape:groupmode="layer"
522 id="layer1"
523 transform="translate(-139.15641,-459.62397)">
524 <g
525 id="g4174"
526 transform="translate(89.103862,103.1549)">
527 <rect
528 y="534.89746"
529 x="70.052551"
530 height="124.65837"
531 width="92.022942"
532 id="rect4136"
533 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" />
534 <rect
535 y="538.51373"
536 x="73.530533"
537 height="55"
538 width="85"
539 id="rect4138"
540 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" />
541 <text
542 sodipodi:linespacing="125%"
543 id="text4142"
544 y="561.84375"
545 x="116.18498"
546 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"
547 xml:space="preserve"><tspan
548 y="561.84375"
549 x="116.18498"
550 id="tspan4144"
551 sodipodi:role="line">Source</tspan><tspan
552 id="tspan4146"
553 y="580.59375"
554 x="116.18498"
555 sodipodi:role="line">Code</tspan></text>
556 <rect
557 y="600.51373"
558 x="73.530533"
559 height="55"
560 width="85"
561 id="rect4138-1"
562 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" />
563 <text
564 sodipodi:linespacing="125%"
565 id="text4148"
566 y="623.04871"
567 x="115.86811"
568 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"
569 xml:space="preserve"><tspan
570 y="623.04871"
571 x="115.86811"
572 id="tspan4150"
573 sodipodi:role="line">snapcraft</tspan><tspan
574 id="tspan4152"
575 y="641.79871"
576 x="115.86811"
577 sodipodi:role="line">yaml</tspan></text>
578 </g>
579 <path
580 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)"
581 d="m 521.13289,693.32077 c 126.36166,0 166.44881,84.53159 166.44881,160.34858"
582 id="path4368"
583 inkscape:connector-curvature="0"
584 sodipodi:nodetypes="cc" />
585 <g
586 id="g4174-4"
587 transform="translate(524.45885,136.18127)">
588 <rect
589 y="534.89746"
590 x="70.052551"
591 height="124.65837"
592 width="92.022942"
593 id="rect4136-5"
594 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" />
595 <rect
596 y="538.51373"
597 x="73.530533"
598 height="55"
599 width="85"
600 id="rect4138-8"
601 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" />
602 <text
603 sodipodi:linespacing="125%"
604 id="text4142-2"
605 y="571.44373"
606 x="116.18498"
607 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"
608 xml:space="preserve"><tspan
609 id="tspan4146-4"
610 y="571.44373"
611 x="116.18498"
612 sodipodi:role="line">Binaries</tspan></text>
613 <rect
614 y="600.51373"
615 x="73.530533"
616 height="55"
617 width="85"
618 id="rect4138-1-6"
619 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" />
620 <text
621 sodipodi:linespacing="125%"
622 id="text4148-2"
623 y="623.04871"
624 x="115.86811"
625 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"
626 xml:space="preserve"><tspan
627 y="623.04871"
628 x="115.86811"
629 id="tspan4150-4"
630 sodipodi:role="line">package</tspan><tspan
631 id="tspan4152-7"
632 y="641.79871"
633 x="115.86811"
634 sodipodi:role="line">metadata</tspan></text>
635 </g>
636 <g
637 id="g4235"
638 transform="translate(287.58171,67.973856)">
639 <rect
640 y="801.38177"
641 x="338.99783"
642 height="122.00436"
643 width="122.00436"
644 id="rect4227"
645 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" />
646 <text
647 sodipodi:linespacing="125%"
648 id="text4229"
649 y="855.71198"
650 x="399.80914"
651 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"
652 xml:space="preserve"><tspan
653 y="855.71198"
654 x="399.80914"
655 id="tspan4231"
656 sodipodi:role="line">Snappy</tspan><tspan
657 id="tspan4233"
658 y="885.71198"
659 x="399.80914"
660 sodipodi:role="line">Store</tspan></text>
661 </g>
662 <g
663 id="g4235-9"
664 transform="translate(42.701512,-168.19172)">
665 <rect
666 y="801.38177"
667 x="338.99783"
668 height="122.00436"
669 width="122.00436"
670 id="rect4227-0"
671 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" />
672 <text
673 sodipodi:linespacing="125%"
674 id="text4229-8"
675 y="869.47595"
676 x="399.82413"
677 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"
678 xml:space="preserve"><tspan
679 id="tspan4233-2"
680 y="869.47595"
681 x="399.82413"
682 sodipodi:role="line">Snapcraft</tspan></text>
683 </g>
684 <path
685 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"
686 d="m 610.02178,929.48634 c -284.09585,0 -170.80609,-75.81699 -284.09585,-75.81699"
687 id="path4692"
688 inkscape:connector-curvature="0" />
689 <path
690 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"
691 d="m 610.02178,929.40975 c -284.09585,0 -170.80609,75.81695 -284.09585,75.81695"
692 id="path4692-3"
693 inkscape:connector-curvature="0" />
694 <path
695 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"
696 d="m 610.02178,929.48634 -284.09585,0"
697 id="path4711"
698 inkscape:connector-curvature="0"
699 sodipodi:nodetypes="cc" />
700 <g
701 id="g4174-4-4"
702 transform="translate(374.16692,301.34465)">
703 <rect
704 y="534.89746"
705 x="70.052551"
706 height="187"
707 width="92.022942"
708 id="rect4136-5-5"
709 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" />
710 <g
711 id="g4347"
712 transform="translate(0,-2.8292561e-6)">
713 <rect
714 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"
715 id="rect4138-8-8"
716 width="85"
717 height="55"
718 x="73.530533"
719 y="538.51373" />
720 <text
721 xml:space="preserve"
722 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"
723 x="116.18498"
724 y="571.44373"
725 id="text4142-2-1"
726 sodipodi:linespacing="125%"><tspan
727 sodipodi:role="line"
728 x="116.18498"
729 y="571.44373"
730 id="tspan4146-4-4">Binaries</tspan></text>
731 </g>
732 <g
733 id="g4341"
734 transform="translate(0,-0.25683877)">
735 <rect
736 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"
737 id="rect4138-1-6-3"
738 width="85"
739 height="55"
740 x="73.530533"
741 y="600.51373" />
742 <text
743 xml:space="preserve"
744 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"
745 x="115.86811"
746 y="623.04871"
747 id="text4148-2-9"
748 sodipodi:linespacing="125%"><tspan
749 sodipodi:role="line"
750 id="tspan4150-4-6"
751 x="115.86811"
752 y="623.04871">package</tspan><tspan
753 sodipodi:role="line"
754 x="115.86811"
755 y="641.79871"
756 id="tspan4152-7-0">metadata</tspan></text>
757 </g>
758 <g
759 id="g4335">
760 <rect
761 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"
762 id="rect4138-1-6-3-3"
763 width="85"
764 height="55"
765 x="74.001198"
766 y="662.00006" />
767 <text
768 xml:space="preserve"
769 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"
770 x="116.33875"
771 y="684.53503"
772 id="text4148-2-9-4"
773 sodipodi:linespacing="125%"><tspan
774 sodipodi:role="line"
775 x="116.33875"
776 y="684.53503"
777 id="tspan4152-7-0-7">digital</tspan><tspan
778 sodipodi:role="line"
779 x="116.33875"
780 y="703.28503"
781 id="tspan4354">signature</tspan></text>
782 </g>
783 </g>
784 <g
785 id="g5113"
786 transform="translate(-6.743845,-3.9339095)">
787 <rect
788 y="502.17679"
789 x="256.82809"
790 height="69.124413"
791 width="119.70325"
792 id="rect4981"
793 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" />
794 <text
795 sodipodi:linespacing="125%"
796 id="text5107"
797 y="533.09399"
798 x="316.85104"
799 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"
800 xml:space="preserve"><tspan
801 y="533.09399"
802 x="316.85104"
803 id="tspan5109"
804 sodipodi:role="line">Snapcraft</tspan><tspan
805 id="tspan5111"
806 y="551.84399"
807 x="316.85104"
808 sodipodi:role="line">Cloud Parts</tspan></text>
809 </g>
810 <g
811 id="g5113-2"
812 transform="translate(146.95962,-3.6528961)">
813 <rect
814 y="502.17679"
815 x="256.82809"
816 height="69.124413"
817 width="250"
818 id="rect4981-9"
819 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" />
820 <text
821 sodipodi:linespacing="125%"
822 id="text5107-3"
823 y="494.31686"
824 x="383.92838"
825 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"
826 xml:space="preserve"><tspan
827 id="tspan5111-3"
828 y="494.31686"
829 x="383.92838"
830 sodipodi:role="line">Common Repositories</tspan></text>
831 </g>
832 <path
833 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"
834 d="m 442.56482,584.78891 0,27.81836"
835 id="path5160"
836 inkscape:connector-curvature="0" />
837 <path
838 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"
839 d="m 266.10088,700.55825 95.25682,0"
840 id="path5162"
841 inkscape:connector-curvature="0" />
842 <path
843 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"
844 d="m 309.65488,584.78891 c 0,44.67797 6.46286,84.29806 51.70282,84.57906"
845 id="path5164"
846 inkscape:connector-curvature="0" />
847 <path
848 inkscape:connector-curvature="0"
849 id="path5463-4"
850 style="fill:#1b1817;fill-opacity:1;fill-rule:evenodd;stroke:none"
851 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" />
852 <g
853 id="g5903"
854 transform="matrix(0.33153681,0,0,0.33153681,478.85446,509.13681)">
855 <path
856 id="path1948"
857 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"
858 style="fill:url(#linearGradient1478);fill-opacity:1"
859 inkscape:connector-curvature="0" />
860 <path
861 id="path1950"
862 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"
863 style="fill:url(#linearGradient1475);fill-opacity:1"
864 inkscape:connector-curvature="0" />
865 <ellipse
866 transform="matrix(0.73406,0,0,0.809524,16.24958,27.00935)"
867 id="path1894"
868 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"
869 cx="61.518883"
870 cy="132.28575"
871 rx="48.948284"
872 ry="8.6066771" />
873 </g>
874 <image
875 y="510.65253"
876 x="539.22363"
877 id="image7798"
878 xlink:href="data:image/png;base64,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
1033Gt0EzcEAAAAASUVORK5CYII=
1034"
1035 preserveAspectRatio="none"
1036 height="38.276688"
1037 width="38.276688" />
1038 <text
1039 xml:space="preserve"
1040 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"
1041 x="591.80872"
1042 y="531.6861"
1043 id="text8871"
1044 sodipodi:linespacing="125%"><tspan
1045 sodipodi:role="line"
1046 id="tspan8873"
1047 x="591.80872"
1048 y="531.6861">……</tspan></text>
1049 <rect
1050 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"
1051 id="rect7801"
1052 width="97.053047"
1053 height="81.728882"
1054 x="574.12695"
1055 y="494.32266" />
1056 <g
1057 id="g8879"
1058 transform="translate(-0.27784156,15.003444)">
1059 <path
1060 sodipodi:nodetypes="czsssc"
1061 inkscape:connector-curvature="0"
1062 id="path8875"
1063 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"
1064 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
1065 <path
1066 sodipodi:nodetypes="cc"
1067 inkscape:connector-curvature="0"
1068 id="path8877"
1069 d="m 243.62484,909.3225 c 0.55568,-15.55913 26.95063,-23.61653 39.4535,-3.3341"
1070 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
1071 </g>
1072 <g
1073 id="g4347-5"
1074 transform="translate(143.43127,303.02174)">
1075 <g
1076 id="g9018"
1077 transform="translate(51.678531,-9.1687717)">
1078 <rect
1079 ry="5"
1080 rx="5"
1081 y="533.23474"
1082 x="70.613205"
1083 height="54.72216"
1084 width="28.32032"
1085 id="rect4138-8-8-9-9"
1086 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" />
1087 <rect
1088 y="538.65265"
1089 x="73.53054"
1090 height="43.886337"
1091 width="22.485647"
1092 id="rect4138-8-8-9"
1093 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" />
1094 </g>
1095 </g>
1096 <g
1097 id="g9789"
1098 transform="translate(7.125194,1.2954898)">
1099 <rect
1100 y="1003.0229"
1101 x="253.4267"
1102 height="7.7959223"
1103 width="18.228931"
1104 id="rect9022"
1105 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" />
1106 <g
1107 id="g9756">
1108 <rect
1109 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"
1110 id="rect9022-7"
1111 width="5.1752357"
1112 height="5.5057745"
1113 x="284.96884"
1114 y="1004.7013" />
1115 <path
1116 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
1117 d="m 263.23546,1007.3397 23.17593,0.229"
1118 id="path9237"
1119 inkscape:connector-curvature="0"
1120 sodipodi:nodetypes="cc" />
1121 <path
1122 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
1123 d="m 287.55645,1005.5162 0,-7.78642"
1124 id="path9239"
1125 inkscape:connector-curvature="0" />
1126 <circle
1127 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"
1128 id="path9241"
1129 cx="287.55646"
1130 cy="996.92834"
1131 r="1.2954898" />
1132 <path
1133 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"
1134 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"
1135 id="path9595"
1136 inkscape:connector-curvature="0"
1137 sodipodi:nodetypes="czc" />
1138 <path
1139 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"
1140 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"
1141 id="path9595-5"
1142 inkscape:connector-curvature="0"
1143 sodipodi:nodetypes="czc" />
1144 </g>
1145 <g
1146 transform="translate(0,0.51071554)"
1147 id="g9764">
1148 <rect
1149 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"
1150 id="rect9022-7-5"
1151 width="5.1752357"
1152 height="5.5057745"
1153 x="-240.11348"
1154 y="1004.1906"
1155 transform="scale(-1,1)" />
1156 <path
1157 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
1158 d="m 261.84687,1006.8289 -23.17593,0.229"
1159 id="path9237-4"
1160 inkscape:connector-curvature="0"
1161 sodipodi:nodetypes="cc" />
1162 <path
1163 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
1164 d="m 237.52588,1005.0054 0,-7.78637"
1165 id="path9239-4"
1166 inkscape:connector-curvature="0" />
1167 <circle
1168 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"
1169 id="path9241-9"
1170 cx="-237.52586"
1171 cy="996.41754"
1172 r="1.2954898"
1173 transform="scale(-1,1)" />
1174 <path
1175 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"
1176 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"
1177 id="path9595-59"
1178 inkscape:connector-curvature="0"
1179 sodipodi:nodetypes="czc" />
1180 <path
1181 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"
1182 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"
1183 id="path9595-5-0"
1184 inkscape:connector-curvature="0"
1185 sodipodi:nodetypes="czc" />
1186 </g>
1187 <path
1188 inkscape:connector-curvature="0"
1189 id="path9772"
1190 d="m 257.53179,1008.2639 -3.72453,8.5826"
1191 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
1192 <path
1193 inkscape:connector-curvature="0"
1194 id="path9772-8"
1195 d="m 267.80726,1008.2639 3.72453,8.5826"
1196 style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
1197 </g>
1198 </g>
1199</svg>
01200
=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md'
--- md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-advanced-features.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,233 @@
1# Snapcraft: Advanced features
2
3Once you have built [your first snap](your-first-snap.md), you will probably
4want to learn more about snapcraft's more advanced features. Having a look at
5our selection of examples is a good idea, as we want it to be a good showcase
6of what is possible and generally relevant.
7
8## Examples
9
10Our showcase can be found in the actual source of `snapcraft` itself. Check
11it out by simply running:
12
13 git clone https://github.com/ubuntu-core/snapcraft
14 cd snapcraft/examples
15
16Inspecting the source locally will make easier to build the examples and
17play around with them. (You can
18[view them online](https://github.com/ubuntu-core/snapcraft/tree/master/examples)
19as well.)
20
21### Playing around with the examples
22
23If you just cloned the `snapcraft` source and inspect the examples, you
24can start off your explorations by reading the accompanying `snapcraft.yaml`
25file and running:
26
27 ../../bin/snapcraft snap
28
29This will inform you of all the steps taken during the creation of the snap.
30
31## Defining your parts
32
33Once you have noted down all the general information about your snap
34(like description, summary information and everything else), naming
35the individual parts will define the stucture of your `snapcraft.yaml` file.
36Think of parts as individual components of your snap: Where do you pull them
37from? How are they built?
38
39### Prerequisites during the build
40
41The example named `downloader-with-wiki-parts` shows how very easy you can
42make sure that the relevant build dependencies are installed:
43
44 build-packages: [libssl-dev]
45
46The above will install the `libssl-dev` package from the Ubuntu archive before
47an attempted build. If you need a specific version of libssl-dev or a custom
48build, you will need to specify a separate part.
49
50Also note that the above will not define which libraries are shipped with the
51app. It merely makes sure you have all the relevant build tools installed.
52
53
54### Pulling and building
55
56If you just intend to pull and build the source, take a look at the `gopaste`
57example. In its `snapcraft.yaml` file you can find just this one `parts`
58paragraph:
59
60 parts:
61 gopaste:
62 plugin: go
63 source: git://github.com/wisnij/gopaste/gopasted
64
65It starts off with the name of the specific part (`gopaste` here), the origin
66of the part (it's a `git` URL) and how to build it (plugin: `go`).
67Other possible scenarios would be Bazaar or Mercurial branches, or local
68directories.
69
70### Mixing and matching plugins
71
72An interesting example is `py2-project` because it defines two parts
73`spongeshaker` using the `python2` plugin, and `make-project` using the
74`make` plugin.
75
76 parts:
77 spongeshaker:
78 plugin: python2
79 source: git://github.com/markokr/spongeshaker.git
80 make-project:
81 plugin: make
82 source: .
83
84The example above mixes and matches parts of different origin. Locally it
85provides a binary we intend to ship (the `sha3sum.py` script) and a
86`Makefile` (to install our script in the right place).
87
88`spongeshaker` is a python library we will need to pull from git, build as
89a python project and bundle along with our script.
90
91A possible use-case for the above would be if you just intend to ship a small
92binary which you maintain, but require a library you need to build from git
93as opposed to simply including it from the Ubuntu archives.
94
95What's happening during the `snapcraft` run is:
96
971. Get `spongeshaker` from `git`.
981. Build it as a python project (which will include installing the python
99 library in the right place).
1001. Running `make` (from the local `Makefile`) and thus installing our
101 `sha3sum.py` script in the right place.
102
103### Putting your parts in order
104
105If your app is comprised of multiple parts, it might be necessary to build
106and stage parts in a particular order. This can be done by using the `after`
107keyword:
108
109 parts:
110 pipelinetest:
111 plugin: make
112 source: lp:~mterry/+junk/pipelinetest
113 after:
114 - libpipeline
115 libpipeline:
116 plugin: autotools
117 source: lp:~mterry/libpipeline/printf
118
119
120In the case of the `libpipeline` example above, the part named `pipelinetest`
121will be built after `libpipeline`. Especially if you need specific
122functionality during a build or as part of checks during the `stage` phase,
123this will be handy.
124
125### Re-using parts
126
127With snapcraft we want to make it easy to learn from other app vendors and
128re-use parts which have worked well for them.
129
130In the `downloader-with-wiki-parts` example, you can see that the `main`
131part is built:
132
133 after:
134 - curl
135
136As we never define the `curl` part in the above example, `snapcraft` will
137check the Ubuntu Wiki, which is where we currently host examples of
138successful snapcraft parts. The build order in this case would be `curl`,
139then `main`.
140
141
142## Finishing steps
143
144### Individual files
145
146If you are planning to provide binaries and services to the users of your
147apps, you need to specify them in your definition first. It's just a matter
148of enumerating them.
149
150The `godd` example has one binary:
151
152 binaries:
153 godd:
154 exec: ./bin/godd
155
156The above will take care of making the script executable, adding it to the
157user's path and install it in the right place.
158
159For a simple service we can take a look at `gopaste`:
160
161 services:
162 gopaste:
163 description: "gopaste"
164 start: bin/gopasted
165
166You define a name for the service, describe it (so log messages are more
167descriptive), declare how to run the service and that's it. For more
168thoughts on services and their security, visit the
169[snappy policy](https://developer.ubuntu.com/en/snappy/guides/security-policy/).
170
171
172### Limiting the number of installed files
173
174To check the list of files included in your snap, you can use `unsquashfs -l`
175on the resulting `.snap` file. If you find that certain files should not be
176shipped to the user (download size being just one factor), you can
177explicitly tell `snapcraft` which files to snap:
178
179 snap:
180 - usr/lib/x86_64-linux-gnu/libgudev-1.0.so*
181 - usr/lib/x86_64-linux-gnu/libobject-2.0.so*
182 - usr/lib/x86_64-linux-gnu/libglib-2.0.so*
183 - bin/godd*
184
185Here `godd` further defines the list of files to be placed in the app
186during the `snap` phase. As you can see above, globs (using asterisks as
187wildcard characters) are a good way of handling complexities within the
188directory structure.
189
190In the `webcam-webui` example you can see the following part called `cam`:
191
192 cam:
193 plugin: go
194 go-packages:
195 - github.com/mikix/golang-static-http
196 stage-packages:
197 - fswebcam
198 filesets:
199 fswebcam:
200 - usr/bin/fswebcam
201 - lib
202 - usr/lib
203 go-server:
204 - bin/golang-*
205 stage:
206 - $fswebcam
207 - $go-server
208 snap:
209 - $fswebcam
210 - $go-server
211 - -usr/share/doc
212
213In the `stage` definition you can see how named filesets are re-used
214(`$fswebcam` and `$go-server`).
215
216Another feature used in the `snap` definition is an exclude (`-usr/share/doc`
217in this case), meaning that files in these directories will not be installed.
218
219
220### node.js
221
222Snapping node.js apps has never been this easy. Take a look at the `shout`
223example and see how short and sweet it is. To bundle node packages, you simply
224do something like:
225
226 parts:
227 shout:
228 plugin: nodejs
229 node-packages:
230 - shout
231
232`node-packages` simply lists which packages (including their dependencies) to
233add to the snap.
0234
=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md'
--- md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-parts.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,130 @@
1# Snapcraft parts
2
3Parts are the main building block to create snaps using Snapcraft. Parts have
4their own private space and lifecycle. Each part uses a `plugin`, which tells
5the part how to behave and what to do with the information inside it.
6
7As seen in the [article about snapcraft.yaml syntax](snapcraft-syntax.md)
8parts have general keywords that apply to all of them. In one case, you may
9want to enhance your part's functionality using `stage-packages` which end up
10bringing Ubuntu deb-based packages into your part, `filesets` to declare
11inclusion and exclusion sets, `organize` to make the artifact output for your
12part neater, `stage` and `snap` to make certain only the right set of files is
13seen at each step (making use of `filesets` or not). An example integrating
14these concepts for a part called `example-part` using a hypothetical plugin
15called `sample` would look like:
16
17 parts:
18 example-part:
19 type: sample
20 stage-packages:
21 - gpg
22 - wget
23 organize:
24 opt/bin: bin
25 filesets:
26 binaries:
27 - bin/*
28 - usr/bin/*
29 headers:
30 - *.h
31 - -include
32 stage:
33 - $binaries
34 - test/bin/test_app
35 - $headers
36 snap:
37 - $binaries
38
39In this example, imagine that the `sample` plugin actually builds something in
40its private *build* location using its private *source* directory as a base,
41and that it *installs* the usual set of files from its private install
42directory.
43
44This `sample` plugin makes use of `stage-packages`, these packages will be
45fetched from the Ubuntu deb archive using the *series* (release, i.e.; trusty,
46vivid, wily, ...) that is being used on the host. In this case, the part will
47be enhanced by the *gpg* and *wget* deb packages and its necessary
48dependencies to work isolated inside the part.
49
50When reaching the *stage* phase, the components in the private part's
51*install* directory will be exposed there, but since we used the organize
52keyword the contents in the install directory will be exposed to other parts
53in a cleaner form if desired or required; it is important to notice that in
54the event of using `filesets` they will follow the organized files and not
55the internal layout.
56
57The concept of `filesets` basically allows the creation of sets named after
58the keywords defined within, in this case *binaries* and *headers*, these are
59not necessarily needed but allow for variable expansion in the common
60targets: `stage` and `snap`. An inclusion is defined by just listing the
61target file, it can be globbed with `*` and a file can be explicitly
62excluded by prepending a `-` (when using `*` at the beginning of a path it
63needs to be quoted).
64
65The `stage` keyword will replace *$binaries* with all the *binaries* defined
66in `filesets`, but it also adds *test/bin/test_app* to the `stage` file set;
67*$headers* will basically *include* all the header files except those that
68live in *include* as it has a `-` in front of it. These are the files that
69will make it to the *stage* directory.
70
71The behavior for `snap` is identical to `stage` with the exception of applying
72this in the snap directory, which is the final layout for the snap, this is
73where everything should look clean and crisp for a good quality snap.
74
75
76## Snapcraft for Python with PIP
77
78Snapcraft includes support for Python 2.x and Python 3.x parts; here's how a
79`snapcraft.yaml` parts section will look like:
80
81 parts:
82 spongeshaker:
83 plugin: python3
84 source: git://github.com/markokr/spongeshaker.git
85
86A Python part will typically make sure required Python packages are installed
87on the build host and embed the following pieces in your snap:
88
89 * latest Python runtime from the latest Ubuntu packages of your current
90 Ubuntu release
91 * latest PIP for this Python version as downloaded from PyPy
92 * latest versions of your PIP requirements
93
94The proper `PYTHONPATH` environment variable will also be set in the wrapper
95scripts generated by snapcraft or when running your app locally.
96
97Python parts support standard snapcraft options and the requirements option
98to point PIP at its requirements file.
99
100Why embed a Python runtime? While Snappy does currently include a Python
101runtime, this might not be the one you need, and it might be updated to a
102different version or removed in a Snappy update. This is why applications
103using Python should embed their copy of the Python runtime.
104
105
106## Snapcraft for Java, Maven or Ant
107
108Snapcraft includes support for building parts with Apache Maven or Ant;
109here's how a snapcraft.yaml parts section will look like:
110
111 parts:
112 webapp:
113 plugin: maven
114 source: git://github.com/lool/snappy-mvn-demo.git
115
116A Maven part will typically:
117
118 * make sure the tool is installed on the build host
119 * embed a Java runtime in your snap
120 * run `mvn package` and copy the resulting `*.jar` and `*.war` files in
121 your snaps `jar/` and `war/` directories
122
123An Ant part works similarly, except it runs ant and sets the proper
124`CLASSPATH` environment variable in the wrapper scripts generated by
125snapcraft or when running the app locally.
126
127If you only need to embed a Java runtime, add a part with the jdk type. This
128will pull a relocatable OpenJDK via the default-jdk Ubuntu package and will
129set the proper `JAVA_HOME` and `PATH` environment variables in wrapper
130scripts generated by snapcraft or when running the app locally.
0131
=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md'
--- md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-syntax.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,83 @@
1# Syntax of the snapcraft.yaml file
2
3The `snapcraft.yaml` file is the main entry point to create a snap through
4Snapcraft. The main building blocks are parts and each defined part is
5independent from each other. In addition to parts, there are attributes
6that define the metadata for the snap package.
7
8What follows is a list of all the attributes the `snapcraft.yaml` file can
9contain.
10
11* `name` (string)
12 The name of the resulting snap.
13* `version` (string)
14 The version of the resulting snap.
15* `summary` (string)
16 A 78 character long summary for the snap.
17* `description` (string)
18 The description for the snap, this can and is expected to be a longer
19 explanation for the snap.
20* `config` (string)
21 Path to the runnable in snap that will be used as the [Snappy config
22 interface](https://developer.ubuntu.com/snappy/guides/config-command/).
23* `services` (yaml subsection)
24 A set of keys representing service names with values as defined by the
25 [Snappy packaging spec](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
26* `binaries` (yaml subsection)
27 A set of keys representing binary names with values as defined by the
28 [Snappy packaging spec](https://developer.ubuntu.com/snappy/guides/packaging-format-apps/).
29* `icon` (string)
30 Path to the icon that will be used for the snap.
31* `license` (yaml subsection)
32 License the snap will carry.
33 * `text` (string)
34 The license text for the snap itself.
35 * `accept-required` (boolean)
36 If true, license acceptance is required for the package to be activated.
37 A good example for this one is the Sun JRE/JDK being bundled in a snap.
38* `framework-policy` (string)
39 A relative path to a directory containing additional policies, used if
40 creating a framework and want to extend permissions to snap apps.
41* `parts` (yaml subsection)
42 A map of part names to their own part configuration. Order in the file is
43 not relevant (to aid copy-and-pasting).
44 * `plugin` (string)
45 Specifies the plugin name that will manage this part. Snapcraft will pass
46 to it all the other user-specified part options. If plugin is not
47 defined, [the wiki](https://wiki.ubuntu.com/Snappy/Parts) will be
48 searched for the part, the local values defined in the part will be used
49 to compose the final part.
50 * `after` (list of strings)
51 Specifies any parts that should be built before this part is. This is
52 mostly useful when a part needs a library or build tool built by another
53 part. If the part defined in after is not defined locally, the part will
54 be searched for in [the wiki](https://wiki.ubuntu.com/Snappy/Parts).
55 *If a part is supposed to run after another, the prerequisite part will
56 be staged before the dependent part starts its lifecycle.*
57 * `stage-packages` (list of strings)
58 A list of Ubuntu packages to use that would support the part creation.
59 * `filesets` (yaml subsection)
60 A dictionary with filesets, the key being a recognizable user defined
61 string and its value a list of strings of files to be included or
62 excluded. Globbing is achieved with `*` for either inclusions or
63 exclusion. Exclusions are denoted by a `-`. Globbing is computed from
64 the private sections of the part.
65 * `organize` (yaml subsection)
66 A dictionary exposing replacements, the key is the internal name whilst
67 the value the exposed name, filesets will refer to the exposed named
68 applied after organization is applied.
69 * `stage` (list of strings)
70 A list of files from a part's installation to expose in stage. Rules
71 applying to the list here are the same as those of filesets. Referencing
72 of fileset keys is done with a $ prefixing the fileset key, which will
73 expand with the value of such key.
74 * `snap` (list of strings)
75 A list of files from a part's installation to expose in snap. Rules
76 applying to the list here are the same as those of filesets. Referencing
77 of fileset keys is done with a `$` prefixing the fileset key, which will
78 expand with the value of such key.
79
80The `snapcraft.yaml` in any project is validated to be compliant to these
81keywords, if there is any missing expected component or invalid value,
82`snapcraft` will exit with an error.
83
084
=== added file 'md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md'
--- md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/snapcraft-usage.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,43 @@
1# Trying snapcraft
2
3In the example directory, you can look at each individual `snapcraft.yaml`
4to see how each project is composed. Within each of these directories try
5running `snapcraft` to build a snap for each of these or go through the
6lifecycle by running:
7
8 $ snapcraft pull
9 $ snapcraft build
10 $ snapcraft stage
11 $ snapcraft strip
12 $ snapcraft snap
13
14That sequence of commands basically went through the lifecycle of all the
15defined parts. To quickly inspect the parts a Snapcraft project has, open
16the `snapcraft.yaml` file of the corresponding example and look at the keys
17inside the parts entry.
18
19
20## Sideloading your snap
21
22Consider the `downloader-with-wiki-parts` example and a Snappy Ubuntu Core
23on 192.168.10.10, to install the built snap by following Trying snapcraft
24and run:
25
26 snappy-remote --url ssh://192.168.10.10 install downloader_1.0_amd64.snap
27
28If this is the first time connecting to the system, snappy-remote will try
29and use existing ssh keys for the user to avoid the necessity of password
30prompts.
31
32After installing a summary of installed snaps will be presented, on vanilla
33x86-64 bit system it would look a lot like this:
34
35 Name Date Version Developer
36 ubuntu-core 2015-09-17 5 ubuntu
37 downloader 2015-10-01 ICIEPfXHQOaC sideload
38 generic-amd64 2015-10-01 1.4 canonical
39
40Take notice of the sideload word in the downloader snap, this indicates that
41the snap did not come signed from the store, if an app is sideloaded, it
42also fakes the version to allow easy iteration without the need to change the
43metadata.
044
=== added file 'md_importer/tests/data/snapcraft-test/docs/your-first-snap.md'
--- md_importer/tests/data/snapcraft-test/docs/your-first-snap.md 1970-01-01 00:00:00 +0000
+++ md_importer/tests/data/snapcraft-test/docs/your-first-snap.md 2016-01-19 00:21:38 +0000
@@ -0,0 +1,318 @@
1# Snapcraft Tutorial
2
3Let's make a snap from scratch using Snapcraft! We'll pick something a little
4interesting: a webcam server.
5
6## Preparation
7
8You'll want a webcam and a Snappy device. We'll assume you have those,
9but if you need help setting up a Snappy install, there is help
10[online](https://developer.ubuntu.com/en/snappy/start/).
11
12(Even if you don't have either of those, you can still follow along. You just
13won't be able to use the final snap package you create. But you'll get to see
14how Snapcraft works, which is still super rewarding.)
15
16## Approach
17
18This example is easy because we won't be doing much of the heavy lifting
19ourselves. We're going to integrate a couple pieces of code together to make
20an interesting app.
21
22Namely, we'll combine a web server with a webcam program and combine
23them to serve a new frame every ten seconds.
24
25> The resulting package is also part of the examples directory in the
26> [snapcraft sources](https://github.com/ubuntu-core/snapcraft/tree/master/examples/webcam-webui)
27
28### The Web Server
29
30Go has a simple web server in its standard libraries. So let's just use that.
31
32It's trivial to write a complete (but basic) web server in a few lines:
33
34 package main
35 import "net/http"
36 func main() {
37 panic(http.ListenAndServe(":8080", http.FileServer(http.Dir("."))))
38 }
39
40This will serve the current directory on port `:8080`. If there is an
41`index.html` in the current directory, it will be served. Otherwise a
42directory listing will be shown.
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches