Merge lp:~bac/charmworld/bug-1257878 into lp:charmworld

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: 472
Merged at revision: 468
Proposed branch: lp:~bac/charmworld/bug-1257878
Merge into: lp:charmworld
Diff against target: 618 lines (+315/-132)
12 files modified
charmworld/forms/qa_assessment.py (+16/-14)
charmworld/migrations/migrate.py (+6/-2)
charmworld/migrations/versions/022_Remove_all_QA_questions_and_data.py (+17/-0)
charmworld/migrations/versions/023_insert_new_questions.py (+13/-0)
charmworld/migrations/versions/tests/test_migrations.py (+69/-0)
charmworld/qa_questions.py (+161/-104)
charmworld/static/css/base.css (+13/-0)
charmworld/templates/form/mapping_item.pt (+1/-1)
charmworld/templates/qa_edit.pt (+2/-0)
charmworld/testing/factory.py (+5/-5)
charmworld/views/charms.py (+11/-6)
docs/migrations.rst (+1/-0)
To merge this branch: bzr merge lp:~bac/charmworld/bug-1257878
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Benji York (community) Approve
Review via email: mp+199713@code.launchpad.net

Commit message

New QA assessment questions.

A migration is necessary to remove the old questions and answers and then to
reinstate the new ones. I've separated these two tasks into two different
migrations for easier testing.

Note the second one is forced to reach into Version and call the newly
refactored setup_qa_data because initialize is only called if the database has
not previously been versioned.

https://codereview.appspot.com/44070045/

R=rick, benji

Description of the change

New QA assessment questions.

A migration is necessary to remove the old questions and answers and then to
reinstate the new ones. I've separated these two tasks into two different
migrations for easier testing.

Note the second one is forced to reach into Version and call the newly
refactored setup_qa_data because initialize is only called if the database has
not previously been versioned.

https://codereview.appspot.com/44070045/

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Reviewers: mp+199713_code.launchpad.net,

Message:
Please take a look.

Description:
New QA assessment questions.

A migration is necessary to remove the old questions and answers and
then to
reinstate the new ones. I've separated these two tasks into two
different
migrations for easier testing.

Note the second one is forced to reach into Version and call the newly
refactored setup_qa_data because initialize is only called if the
database has
not previously been versioned.

https://code.launchpad.net/~bac/charmworld/bug-1257878/+merge/199713

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/44070045/

Affected files (+317, -132 lines):
   A [revision details]
   M charmworld/forms/qa_assessment.py
   M charmworld/migrations/migrate.py
   A charmworld/migrations/versions/022_Remove_all_QA_questions_and_data.py
   A charmworld/migrations/versions/023_insert_new_questions.py
   M charmworld/migrations/versions/tests/test_migrations.py
   M charmworld/qa_questions.py
   M charmworld/static/css/base.css
   M charmworld/templates/form/mapping_item.pt
   M charmworld/templates/qa_edit.pt
   M charmworld/testing/factory.py
   M charmworld/views/charms.py
   M docs/migrations.rst

Revision history for this message
Richard Harding (rharding) wrote :

Code looks good. QA'ing next.

https://codereview.appspot.com/44070045/diff/1/charmworld/migrations/versions/tests/test_migrations.py
File charmworld/migrations/versions/tests/test_migrations.py (right):

https://codereview.appspot.com/44070045/diff/1/charmworld/migrations/versions/tests/test_migrations.py#newcode148
charmworld/migrations/versions/tests/test_migrations.py:148: # Test
removal of QA questions and data. The new questions will be
double space

https://codereview.appspot.com/44070045/diff/1/charmworld/templates/form/mapping_item.pt
File charmworld/templates/form/mapping_item.pt (right):

https://codereview.appspot.com/44070045/diff/1/charmworld/templates/form/mapping_item.pt#newcode5
charmworld/templates/form/mapping_item.pt:5:
title='${field.description}'
any reason to change this one from " to '?

https://codereview.appspot.com/44070045/

Revision history for this message
Benji York (benji) wrote :

QA looks good.

review: Approve
Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/forms/qa_assessment.py'
2--- charmworld/forms/qa_assessment.py 2013-02-11 17:04:31 +0000
3+++ charmworld/forms/qa_assessment.py 2013-12-19 20:19:08 +0000
4@@ -40,25 +40,27 @@
5 ('0', 'No'),
6 ('', 'Unknown'),
7 )
8- for q in score_questions:
9- category = q['name']
10- category_description = q['description']
11- questions = q['questions']
12+ validator = OneOf([x[0] for x in choices])
13+ for sq in score_questions:
14+ category = sq['name']
15+ category_description = sq['description']
16+ questions = sq['questions']
17
18 category_schema = Schema(
19 name=category,
20 title=category_description)
21- for q in questions:
22- category_schema.add(
23- SchemaNode(
24- String(),
25- missing='',
26- validator=OneOf([x[0] for x in choices]),
27- widget=RadioChoiceWidget(values=choices),
28- name=q['id'],
29- title=q['description'],
30- description=q['extended_description'])
31+
32+ for question in questions:
33+ node = SchemaNode(
34+ String(),
35+ missing='',
36+ validator=validator,
37+ widget=RadioChoiceWidget(values=choices),
38+ name=question['id'],
39+ title=question['description'],
40+ description=question['extended_description'],
41 )
42+ category_schema.add(node)
43
44 schema.add(category_schema)
45
46
47=== modified file 'charmworld/migrations/migrate.py'
48--- charmworld/migrations/migrate.py 2013-11-05 19:56:37 +0000
49+++ charmworld/migrations/migrate.py 2013-12-19 20:19:08 +0000
50@@ -147,10 +147,14 @@
51 else:
52 pass # Must be a helper file or something, let's ignore it.
53
54+ @classmethod
55+ def setup_qa_data(cls, db):
56+ for category_dict in iter_categories():
57+ db.qa.insert(category_dict)
58+
59 def initialize(self, datastore):
60 datastore.version_datastore()
61- for category_dict in iter_categories():
62- datastore.db.qa.insert(category_dict)
63+ self.setup_qa_data(datastore.db)
64
65 def ensure_initialized(self, datastore, init=False):
66 if datastore.current_version is not None:
67
68=== added file 'charmworld/migrations/versions/022_Remove_all_QA_questions_and_data.py'
69--- charmworld/migrations/versions/022_Remove_all_QA_questions_and_data.py 1970-01-01 00:00:00 +0000
70+++ charmworld/migrations/versions/022_Remove_all_QA_questions_and_data.py 2013-12-19 20:19:08 +0000
71@@ -0,0 +1,17 @@
72+
73+# Remove all QA questions and data
74+
75+def upgrade(db, index_client):
76+ """Complete this function with work to be done for the migration/update.
77+
78+ db is the pymongo db instance for our datastore. Charms are in db.charms
79+ for instance.
80+
81+ index_client is the ElasticSearch client.
82+ """
83+
84+ # Remove the QA questions.
85+ db.qa.remove()
86+
87+ # Remove the old QA answers.
88+ db.charm_qa_answers.remove()
89
90=== added file 'charmworld/migrations/versions/023_insert_new_questions.py'
91--- charmworld/migrations/versions/023_insert_new_questions.py 1970-01-01 00:00:00 +0000
92+++ charmworld/migrations/versions/023_insert_new_questions.py 2013-12-19 20:19:08 +0000
93@@ -0,0 +1,13 @@
94+from charmworld.migrations.migrate import Versions
95+
96+# insert new questions
97+
98+def upgrade(db, index_client):
99+ """Complete this function with work to be done for the migration/update.
100+
101+ db is the pymongo db instance for our datastore. Charms are in db.charms
102+ for instance.
103+
104+ index_client is the ElasticSearch client.
105+ """
106+ Versions.setup_qa_data(db)
107
108=== modified file 'charmworld/migrations/versions/tests/test_migrations.py'
109--- charmworld/migrations/versions/tests/test_migrations.py 2013-11-05 21:57:46 +0000
110+++ charmworld/migrations/versions/tests/test_migrations.py 2013-12-19 20:19:08 +0000
111@@ -6,9 +6,13 @@
112
113 from charmworld.models import (
114 Bundle,
115+ Charm,
116 CharmSource,
117+ QAData,
118+ QADataSource,
119 store_bundles,
120 )
121+from charmworld import qa_questions
122 from charmworld.search import (
123 BUNDLE,
124 IndexMissing,
125@@ -139,6 +143,71 @@
126 self.assertEqual(0, len(results[BUNDLE]))
127
128
129+class TestMigration022(MigrationTestBase):
130+
131+ # Test removal of QA questions and data. The new questions will be
132+ # updated in the next migration.
133+
134+ def setUp(self):
135+ super(TestMigration022, self).setUp()
136+ self.use_index_client()
137+
138+ def test_question_removal(self):
139+ # Load question set.
140+ category = factory.makeQAQuestionGroup()
141+ self.db.qa.insert(category)
142+ self.qa_data_source = QADataSource.from_db(self.db)
143+ # Show that questions exist before running the migration.
144+ qadata = QAData(self.qa_data_source)
145+ self.assertListEqual(['reliable'], qadata.categories)
146+ # Run the migration.
147+ self.versions.run_migration(
148+ self.db, self.index_client,
149+ '022_Remove_all_QA_questions_and_data.py')
150+ # And verify the data were removed.
151+ qadata = QAData(self.qa_data_source)
152+ self.assertListEqual([], qadata.categories)
153+
154+ def test_charm_score_removal(self):
155+ charm_data = factory.makeCharm(self.db)[1]
156+ charm = Charm(charm_data)
157+ qa_data_source = QADataSource.from_db(self.db)
158+ qa_data_source.save_qa_data(charm, {'a': 'b'})
159+ answers = self.db.charm_qa_answers.find()
160+ self.assertEqual(1, answers.count())
161+ # Run the migration.
162+ self.versions.run_migration(
163+ self.db, self.index_client,
164+ '022_Remove_all_QA_questions_and_data.py')
165+ # And verify the data were removed.
166+ answers = self.db.charm_qa_answers.find()
167+ self.assertEqual(0, answers.count())
168+
169+
170+class TestMigration023(MigrationTestBase):
171+
172+ # Test restoration of new QA questions.
173+
174+ def setUp(self):
175+ super(TestMigration023, self).setUp()
176+ self.use_index_client()
177+
178+ def test_questions_inserted(self):
179+ # Load question set.
180+ self.qa_data_source = QADataSource.from_db(self.db)
181+ # Show that no questions exist before running the migration.
182+ qadata = QAData(self.qa_data_source)
183+ self.assertListEqual([], qadata.categories)
184+ # Run the migration.
185+ self.versions.run_migration(
186+ self.db, self.index_client,
187+ '023_insert_new_questions.py')
188+ # And verify the data were removed.
189+ qadata = QAData(self.qa_data_source)
190+ self.assertEqual(7, len(qadata.categories))
191+ expected = [cd['name'] for cd in qa_questions.iter_categories()]
192+ self.assertListEqual(sorted(expected), sorted(qadata.categories))
193+
194
195 class TestExodusRestrictions(MigrationTestBase):
196
197
198=== modified file 'charmworld/qa_questions.py'
199--- charmworld/qa_questions.py 2013-09-10 13:54:39 +0000
200+++ charmworld/qa_questions.py 2013-12-19 20:19:08 +0000
201@@ -16,27 +16,113 @@
202 for instance.
203 """
204
205+ # A list of tuples for question categories.
206+ # (name, description, questions)
207+ # where questions = (description, points, extended_description)
208+
209 initial = [
210 (
211+ u'data_handling',
212+ u'Data Handling',
213+ [
214+ (u"Handles the service's user data", 1,
215+ u"Gracefully handle the service's user data so that the user "
216+ "doesn't have to manage it separately."),
217+
218+ (u'Provides backup mechanism', 1,
219+ u"Provide a backup mechanism to the service's user data, "
220+ "such as a relationship to a backup subordinate charm, "
221+ "snapshots to a bucket, or other means of getting the data "
222+ "off of the instance."),
223+
224+ (u'Provides a restore mechanism', 1,
225+ u"Provide a restore mechanism to the service's user data, "
226+ "such as a relationship to a backup subordinate charm, "
227+ "restoring from a bucket, or other means of getting data "
228+ "onto the instance."),
229+
230+ (u'Provides encryption', 1,
231+ u'Provides encryption of service user data.'),
232+ ],
233+ ),
234+
235+ (
236+ u'secure',
237+ u'Secure',
238+ [
239+ (u'Contains a well-tested '
240+ '<a href='
241+ '"https://help.ubuntu.com/12.04/serverguide/apparmor.html">'
242+ 'AppArmor</a> profile', 1,
243+ u'These can be provided by the package itself.'),
244+
245+ (u"Doesn't run as root", 1,
246+ u'The service should not run as root. Refer to the '
247+ '<a href="http://upstart.ubuntu.com/cookbook/'
248+ '#run-a-job-as-a-different-user">'
249+ 'Upstart documentation</a> for tips on how to do this.'),
250+
251+ (u'Per instance or service access control', 1,
252+ u'Accept relationships only from trusted instances and/or '
253+ 'services.'),
254+
255+ (u'Defaults to secure communication', 1,
256+ u'Defaults to secure channels when communicating with other '
257+ 'services and/or multiple units of the same service.'),
258+ ],
259+ ),
260+
261+ (
262 u'reliable',
263 u'Reliable',
264 [
265- (u'Check for integrity from upstream source', 1, u''),
266- (u'Fail gracefully if upstream source goes missing', 1, u''),
267- (u'Contain a suite of tests with the charm that pass', 1, u''),
268- (u'Passes tests from Jenkins on jujucharms.com', 1, u''),
269- ]
270- ),
271-
272- (
273- u'secure',
274- u'Secure',
275- [
276- (u'Contain a well tested AppArmor profile', 1, ''),
277- (u'Conform to security policies of the charm store', 1,
278- 'Tight access control'),
279- (u"Doesn't run as root", 1, ''),
280- (u'Per instance or service access control', 1, ''),
281+ (u'Fails gracefully if upstream source goes missing', 1,
282+ u'Sometimes URLs change or upstream services are not '
283+ 'available.'),
284+
285+ (u'Contains a suite of integration tests with the charm '
286+ 'that pass',
287+ 1,
288+ u'We provide <a href="howto-amulet.html">Amulet</a> '
289+ 'to help you do this.'),
290+
291+ (u'Configuration options have safe defaults', 1, u''),
292+ ]
293+ ),
294+
295+ (
296+ u'scaleable',
297+ u'Scaleable',
298+ [
299+ (u"Responds to add-unit based on the service's needs", 1,
300+ u'Users should be able to <tt>juju add-unit</tt> to your '
301+ 'service and have it scale horizontally.'),
302+
303+ (u"Responds to remove-unit based on the service's needs", 1,
304+ u'Users should be able to <tt>juju remove-unit</tt> to '
305+ 'your service and have it scale down.'),
306+
307+ (u'Reuses existing charms for supporting services', 1,
308+ u'If the service needs relationships to other services it '
309+ 'should reuse existing charms from the charm store instead '
310+ 'of bundling its own.'),
311+ ]
312+ ),
313+
314+
315+ (
316+ u'upstream_friendly',
317+ u'Upstream Friendly',
318+ [
319+ (u'Follow deployment recommendations from upstream best '
320+ 'practices',
321+ 1,
322+ u'Most services have a known-good recommendation from the '
323+ 'project itself, these should be available for users'),
324+
325+ (u'Provide up to date versions of the upstream release', 1,
326+ u'Provide a config option to allow the user to run newer '
327+ 'versions of the service.')
328 ]
329 ),
330
331@@ -44,102 +130,73 @@
332 u'flexible',
333 u'Flexible',
334 [
335- (
336- u'Contain opinionated tuning options', 1,
337- u'Examples (depends on the service): "safe", "default",'
338- ' "fast", "real fast, not so safe". Don\'t expose every'
339- ' configuration, pick those that reflect real world usage.'
340- ' Make it so I don\'t have to read the book.'
341- ),
342- (u'Use existing interfaces with other charms', 1,
343- u'Highly relatable'),
344- ]
345- ),
346-
347- (
348- u'data_handling',
349- u'Data Handling',
350- [
351- (u'Integrate data storage best practices', 1,
352- u'Backups based on service usage'),
353- (u"Handle the service's user data", 1, u'Version control'),
354- (u"Handle the service's user data", 1,
355- u'Automated snapshots and backup.'),
356- ]
357- ),
358-
359- (
360- u'scalable',
361- u'Scaleable',
362- [
363- (u"Responds to add-unit based on the service's needs", 1,
364- u'Configuration should not require additional steps to scale'
365- ' horizontally'),
366- (u'Be tested with a real workload, not just a synthetic'
367- ' benchmark', 1, ''),
368- (u'From upstream and existing devops practices for that'
369- ' service', 1, ''),
370- (u'Community peer reviewed', 1, ''),
371- (u'Have a configure option for most performant configuration'
372- ' if not the default', 1, ''),
373- ]
374- ),
375-
376- (
377- u'easy_deploy',
378+ (u'Exposes version of service as a config option to allow '
379+ 'easy upgrades',
380+ 1,
381+ u'Allows users to maintain their deployments without '
382+ 'having to redeploy.'),
383+
384+ (u'Adheres to the coding guidelines of the language your '
385+ 'charm is written',
386+ 1,
387+ u''),
388+
389+ (u'Exposed configurations are subsets of opinionated '
390+ 'deployments',
391+ 1,
392+ u'Don\'t expose every service option as configuration. '
393+ 'Make sets of decisions and expose those as one '
394+ 'configuration option. For example "fast" vs. "default" '
395+ 'vs. "slow-and-safer".'),
396+
397+ (u'Allow installation from the Ubuntu repository',
398+ 1,
399+ u'Should be the default if the service is in Ubuntu.'),
400+
401+ (u'Allow installation from pure upstream source',
402+ 1,
403+ u'Sometimes people prefer to deploy from pure upstream'),
404+
405+ (u'Allow installation from your local source',
406+ 1,
407+ u'Sometimes people prefer to deploy from their vetted '
408+ 'local source.'),
409+
410+ (u'Allow installation from PPA or other repository if '
411+ 'available',
412+ 1,
413+ u'Some upstreams run their own repositories '
414+ 'that they gate and support. Users should have an option '
415+ 'of using these.')
416+ ]
417+ ),
418+
419+ (
420+ u'easy_to_deploy',
421 u'Easy to Deploy',
422 [
423- (u'README with examples of use for a typical workload', 1, ''),
424- (u'README with examples of use for workloads at scale', 1, ''),
425- (u'README with examples of use recommend best-practice'
426- ' relationships', 1, ''),
427- (u'Allow installation from pure upstream source', 1, ''),
428- (u'Allow installation from your local source', 1, ''),
429- (u'Allow installation from PPA (if available)', 1, ''),
430- (u'Allow installation from the Ubuntu repository', 1, ''),
431- ]
432- ),
433-
434- (
435- u'responsive',
436- u'Responsive to DevOps Needs',
437- [
438- (u'Allow for easy upgrade via juju upgrade-charm', 1, ''),
439- (u'Allow upgrading the service itself.', 1, ''),
440- (u'Responsive to user bug reports and concerns', 1, ''),
441- (u'Maintainable, easy to read and modify', 1, ''),
442- ]
443- ),
444-
445- (
446- u'upstream',
447- u'Upstream Friendly',
448- [
449- (u'Follow upstream best practices', 1,
450- u'Provide an option for a barebones "pure upstream"'
451- ' configuration'),
452- (u'Should go lock-step with deployment recommendations', 1,
453- u'Provide tip-of-trunk testing if feasible'),
454- (u'Fresh charm on release day!', 1, ''),
455- (
456- u"Endeavour to be upstream's recommended way to deploy"
457- ' that service in the cloud (website mention or'
458- ' something)', 1, ''
459- ),
460+ (u'README with examples of use for a typical workload', 1,
461+ u''),
462+ (u'README with examples of use for workloads at scale', 1,
463+ u''),
464+ (u'README with examples of use of recommended best-practice '
465+ 'relationships',
466+ 1,
467+ u'')
468 ]
469 ),
470 ]
471
472 """Add the sample data into the db."""
473- for cat in initial:
474+ for name, description, questions in initial:
475 category_dict = {
476- 'name': cat[0],
477- 'description': cat[1],
478+ 'name': name,
479+ 'description': description,
480 'questions': [{
481- 'id': '{0}_{1}'.format(cat[0].lower(), i),
482- 'description': q[0],
483- 'points': q[1],
484- 'extended_description': q[2]
485- } for i, q in enumerate(cat[2])]
486+ 'id': '{0}_{1}'.format(name.lower(), i),
487+ 'description': question[0],
488+ 'points': question[1],
489+ 'extended_description': question[2],
490+ } for i, question in enumerate(questions)]
491 }
492 yield category_dict
493
494=== modified file 'charmworld/static/css/base.css'
495--- charmworld/static/css/base.css 2013-11-20 14:29:00 +0000
496+++ charmworld/static/css/base.css 2013-12-19 20:19:08 +0000
497@@ -42,3 +42,16 @@
498 height: 120px;
499 width: 120px;
500 }
501+
502+.qa-edit legend {
503+ font-weight: bold;
504+}
505+
506+.qa-edit .help-block {
507+ font-style: italic;
508+ padding-bottom: 20px;
509+}
510+
511+.qa-edit .control-label {
512+ padding-top: 20px;
513+}
514
515=== modified file 'charmworld/templates/form/mapping_item.pt'
516--- charmworld/templates/form/mapping_item.pt 2013-01-04 13:50:38 +0000
517+++ charmworld/templates/form/mapping_item.pt 2013-12-19 20:19:08 +0000
518@@ -2,7 +2,7 @@
519 field.widget.category == 'structural'"
520 class="control-group ${field.error and 'error' or ''}"
521 id="item-${field.oid}"
522- title="${field.description}"
523+ title='${field.description}'
524 tal:omit-tag="structural"
525 i18n:domain="deform">
526
527
528=== modified file 'charmworld/templates/qa_edit.pt'
529--- charmworld/templates/qa_edit.pt 2013-01-21 14:01:03 +0000
530+++ charmworld/templates/qa_edit.pt 2013-12-19 20:19:08 +0000
531@@ -1,11 +1,13 @@
532 <html tal:define="main load: main.pt" metal:use-macro="main.macros['page']">
533 <body>
534 <div class="container">
535+ <div class="qa-edit">
536 <metal:block metal:fill-slot="content">
537 <div tal:content="structure form">
538 form
539 </div>
540 </metal:block>
541+ </div>
542 </div>
543 <!-- /container -->
544 </body>
545
546=== modified file 'charmworld/testing/factory.py'
547--- charmworld/testing/factory.py 2013-11-18 20:38:47 +0000
548+++ charmworld/testing/factory.py 2013-12-19 20:19:08 +0000
549@@ -708,11 +708,11 @@
550 'name': category[0],
551 'description': category[1],
552 'questions': [{
553- 'id': hashlib.md5(q[0]).hexdigest(),
554- 'description': q[0],
555- 'points': q[1],
556- 'extended_description': q[2]
557- } for q in category[2]]
558+ 'id': hashlib.md5(question[0]).hexdigest(),
559+ 'description': question[0],
560+ 'points': question[1],
561+ 'extended_description': question[2],
562+ } for question in category[2]]
563 }
564
565 return category_dict
566
567=== modified file 'charmworld/views/charms.py'
568--- charmworld/views/charms.py 2013-10-17 15:35:53 +0000
569+++ charmworld/views/charms.py 2013-12-19 20:19:08 +0000
570@@ -5,6 +5,7 @@
571 Form,
572 ValidationFailure,
573 )
574+from HTMLParser import HTMLParser
575 import json
576 import os
577 import pymongo
578@@ -395,8 +396,9 @@
579 qa_questions = request.db.qa.find()
580 schema = get_qa_form_schema(qa_questions)
581 qa_form = Form(schema, buttons=('submit',))
582- qa_form.css_class = 'form-horizontal well'
583+ qa_form.css_class = 'form-horizontal well qa-edit'
584 qa_data_source = QADataSource.from_db(request.db)
585+
586 if 'submit' in request.POST:
587 data = request.POST.items()
588 try:
589@@ -409,11 +411,14 @@
590 form = qa_form.render()
591 else:
592 qa_data = qa_data_source.get_qa_data(Charm(charm)).charm_data
593- if qa_data is not None:
594- # There's existing data to use to fill the form with.
595- valid_data = qa_data
596- form = qa_form.render(valid_data)
597-
598+ valid_data = qa_data or {}
599+ if valid_data:
600+ form = qa_form.render(valid_data)
601+ else:
602+ form = qa_form.render()
603+ # The form rendering escapes all HTML in the text. In order to have links
604+ # in the QA descriptions and titles we must unescape it here.
605+ form = HTMLParser().unescape(form)
606 return {
607 'charm': charm,
608 'form': form,
609
610=== modified file 'docs/migrations.rst'
611--- docs/migrations.rst 2013-08-28 20:54:26 +0000
612+++ docs/migrations.rst 2013-12-19 20:19:08 +0000
613@@ -59,6 +59,7 @@
614 ::
615
616 $ bin/migrations prepare-upgrade --init
617+ $ bin/migrations upgrade
618 Updated the datastore to version: 1
619
620

Subscribers

People subscribed via source and target branches

to all changes: