Merge lp:~abentley/charmworld/remove-api-0 into lp:~juju-jitsu/charmworld/trunk
- remove-api-0
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Aaron Bentley |
Approved revision: | 250 |
Merged at revision: | 243 |
Proposed branch: | lp:~abentley/charmworld/remove-api-0 |
Merge into: | lp:~juju-jitsu/charmworld/trunk |
Diff against target: |
727 lines (+140/-317) 13 files modified
charmworld/routes.py (+0/-3) charmworld/search.py (+24/-7) charmworld/tests/test_search.py (+30/-6) charmworld/views/api.py (+0/-30) charmworld/views/tests/test_api.py (+33/-252) migrations/migrate.py (+11/-6) migrations/test_migrate.py (+8/-4) migrations/versions/001_load_qa_questions.py (+1/-1) migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py (+1/-1) migrations/versions/003_remove_provider_qa_questions.py (+1/-1) migrations/versions/004_remove_charm_errors.py (+3/-1) migrations/versions/005_delete_icon_field.py (+8/-0) migrations/versions/tests/test_migrations.py (+20/-5) |
To merge this branch: | bzr merge lp:~abentley/charmworld/remove-api-0 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+167324@code.launchpad.net |
Commit message
Remove API 0
Description of the change
Remove API 0 and introduce a migration to remove icons from charm data.
Since the charm formatting changes, the migration must also update elasticsearch. This is done using reindex, which gains a charms parameter so that mongodb can be used as a data source, and now handles the case where the index does not already exist.
Much test refactoring was done, since all current APIs (i.e. API1) run all
tests now.
copy_index was broken, because it invoked index_charms on itself, not the pending copy. This is now fixed. It is also renamed to "create_
unlimited_search is updated to raise IndexMissing if the search errors out with an IndexMissingExc
Charmworld Lander (charmworld-lander) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Preview Diff
1 | === modified file 'charmworld/routes.py' |
2 | --- charmworld/routes.py 2013-04-29 18:21:35 +0000 |
3 | +++ charmworld/routes.py 2013-06-04 16:12:32 +0000 |
4 | @@ -80,9 +80,6 @@ |
5 | config.add_route('auth_callback', '/auth_callback') |
6 | config.add_route('logout', '/logout') |
7 | |
8 | - # API |
9 | - config.add_route('api_0', '/api/0/{endpoint}{remainder:.*}') |
10 | - config.add_view('charmworld.views.api.API0', route_name='api_0') |
11 | config.add_route('api_1', '/api/1/{endpoint}{remainder:.*}') |
12 | config.add_view('charmworld.views.api.API1', route_name='api_1') |
13 | |
14 | |
15 | === modified file 'charmworld/search.py' |
16 | --- charmworld/search.py 2013-05-29 12:36:35 +0000 |
17 | +++ charmworld/search.py 2013-06-04 16:12:32 +0000 |
18 | @@ -233,7 +233,8 @@ |
19 | kwargs = {'index': self.index_name} |
20 | if limit is not None: |
21 | kwargs['size'] = limit |
22 | - hits = self._client.search(dsl, **kwargs) |
23 | + with translate_error(): |
24 | + hits = self._client.search(dsl, **kwargs) |
25 | if limit is None and len(hits['hits']['hits']) < hits['hits']['total']: |
26 | kwargs['size'] = hits['hits']['total'] |
27 | hits = self._client.search(dsl, **kwargs) |
28 | @@ -348,40 +349,56 @@ |
29 | name = '%s-%d' % (self.index_name, random.randint(1, 99999)) |
30 | return self.__class__(self._client, name) |
31 | |
32 | - def copy_index(self, name=None): |
33 | - """Create a copy of this index with the specified name. |
34 | + def create_replacement(self, name=None, charms=None): |
35 | + """Create a replacement for this index. |
36 | |
37 | The mapping will not be this index's mapping, it will be the current |
38 | mapping (e.g. put_mapping). |
39 | |
40 | + :param name: If not supplied, a name based on this index's name will |
41 | + be used. |
42 | + :param charms: A list of charms. If None, the charms in the current |
43 | + index will be used. |
44 | :return: The ElasticSearchClient for the supplied mapping. |
45 | """ |
46 | copy = self.get_aliasable_client(name) |
47 | copy.create_index() |
48 | try: |
49 | copy.put_mapping() |
50 | - self.index_charms(self.api_search(valid_charms_only=False)) |
51 | + if charms is None: |
52 | + try: |
53 | + charms = self.api_search(valid_charms_only=False) |
54 | + except IndexMissing: |
55 | + charms = [] |
56 | + copy.index_charms(charms) |
57 | return copy |
58 | except: |
59 | copy.delete_index() |
60 | raise |
61 | |
62 | |
63 | -def reindex(index_client): |
64 | +def reindex(index_client, charms=None): |
65 | """Reindex documents with the current mapping. |
66 | |
67 | This works by creating a new index and then aliasing it to the old index. |
68 | If the old index was already an alias, this is a simple swap. If not, the |
69 | old index is deleted. |
70 | + |
71 | + :param charms: A list of charms to use. If None, the charms in the index |
72 | + will be used. |
73 | """ |
74 | - new_index = index_client.copy_index() |
75 | + new_index = index_client.create_replacement(charms=charms) |
76 | try: |
77 | aliased = index_client.get_aliased() |
78 | if aliased == []: |
79 | - index_client.delete_index() |
80 | + try: |
81 | + index_client.delete_index() |
82 | + except IndexMissing: |
83 | + pass |
84 | index_client.update_aliased(new_index.index_name, aliased) |
85 | except: |
86 | new_index.delete_index() |
87 | + raise |
88 | if aliased != []: |
89 | for name in aliased: |
90 | ElasticSearchClient(index_client._client, name).delete_index() |
91 | |
92 | === modified file 'charmworld/tests/test_search.py' |
93 | --- charmworld/tests/test_search.py 2013-05-29 12:36:35 +0000 |
94 | +++ charmworld/tests/test_search.py 2013-06-04 16:12:32 +0000 |
95 | @@ -444,12 +444,29 @@ |
96 | alias.update_aliased('bar', alias.get_aliased()) |
97 | self.assertEqual(['bar'], alias.get_aliased()) |
98 | |
99 | - def test_copy_index(self): |
100 | - self.index_client.index_charm({'_id': 'a', 'name': 'foo'}) |
101 | - copy = self.index_client.copy_index('index-copy') |
102 | - self.addCleanup(copy.delete_index) |
103 | - copy.wait_for_startup() |
104 | - self.assertIn('series', copy.get_mapping()['charm']['properties']) |
105 | + def test_create_replacement(self): |
106 | + self.index_client.index_charm({'_id': 'a', 'name': 'foo'}) |
107 | + copy = self.index_client.create_replacement('index-copy') |
108 | + self.addCleanup(copy.delete_index) |
109 | + copy.wait_for_startup() |
110 | + self.assertIn('series', copy.get_mapping()['charm']['properties']) |
111 | + self.assertEqual({'_id': 'a', 'name': 'foo'}, copy.get('a')) |
112 | + |
113 | + def test_create_replacement_charms(self): |
114 | + self.index_client.index_charm({'_id': 'a', 'name': 'foo'}) |
115 | + copy = self.index_client.create_replacement( |
116 | + charms=[{'_id': 'a', 'name': 'bar'}]) |
117 | + self.addCleanup(copy.delete_index) |
118 | + copy.wait_for_startup() |
119 | + self.assertIn('series', copy.get_mapping()['charm']['properties']) |
120 | + self.assertEqual({'_id': 'a', 'name': 'bar'}, copy.get('a')) |
121 | + |
122 | + def test_create_replacement_misssing(self): |
123 | + client = ElasticSearchClient.from_settings(get_ini(), 'temp-index') |
124 | + # This should not raise an exception, even though the index has not |
125 | + # been created. |
126 | + copy = client.create_replacement() |
127 | + self.addCleanup(copy.delete_index) |
128 | |
129 | def test_put_mapping_incompatible_mapping_error(self): |
130 | # The error we get from an incompatible mapping is IncompatibleMapping |
131 | @@ -600,3 +617,10 @@ |
132 | mapping = alias.get_mapping() |
133 | self.assertEqual('not_analyzed', |
134 | mapping['charm']['properties']['name']['index']) |
135 | + |
136 | + def test_reindexed_no_client_charms(self): |
137 | + client = ElasticSearchClient.from_settings(get_ini()) |
138 | + # This should not raise an exception, even though the index does not |
139 | + # exist. |
140 | + new_client = reindex(client, charms=[]) |
141 | + new_client.delete_index() |
142 | |
143 | === modified file 'charmworld/views/api.py' |
144 | --- charmworld/views/api.py 2013-06-03 16:18:58 +0000 |
145 | +++ charmworld/views/api.py 2013-06-04 16:12:32 +0000 |
146 | @@ -412,33 +412,3 @@ |
147 | 'popular': self._charm_results(popular), |
148 | } |
149 | } |
150 | - |
151 | - |
152 | -class API0(API1): |
153 | - """Implementation of API 0. |
154 | - |
155 | - All methods whose names do not begin with an underscore are exposed. |
156 | - Methods may return a webob Response, which is returned directly, or a |
157 | - json-serializable value, which will be returned as a json HTTP response. |
158 | - """ |
159 | - |
160 | - @classmethod |
161 | - def _charm_result(cls, charm, requires, provides): |
162 | - return cls._format_charm(charm) |
163 | - |
164 | - def _charm_results(self, charms): |
165 | - return [self._charm_result(charm, {}, {}) for charm in charms] |
166 | - |
167 | - def _charms(self, limit=None, name=None, series=None, owner=None, |
168 | - provides=None, requires=None, type_=None, provider=None, |
169 | - scope=None, category=None, categories=None, text=None): |
170 | - return super(API0, self)._charms( |
171 | - limit, name, series, owner, provides, requires, type_, provider, |
172 | - scope, categories, text) |
173 | - |
174 | - @staticmethod |
175 | - def _format_charm(charm): |
176 | - output = API1._format_charm(charm) |
177 | - if 'icon' in charm: |
178 | - output['icon'] = charm['icon'] |
179 | - return output |
180 | |
181 | === modified file 'charmworld/views/tests/test_api.py' |
182 | --- charmworld/views/tests/test_api.py 2013-06-03 16:18:58 +0000 |
183 | +++ charmworld/views/tests/test_api.py 2013-06-04 16:12:32 +0000 |
184 | @@ -15,7 +15,6 @@ |
185 | ) |
186 | from charmworld.models import CharmSource |
187 | from charmworld.views.api import ( |
188 | - API0, |
189 | API1, |
190 | json_response, |
191 | ) |
192 | @@ -73,6 +72,30 @@ |
193 | return self.api_class(request)() |
194 | |
195 | |
196 | +class API1Mixin(APITestBase): |
197 | + |
198 | + api_class = API1 |
199 | + |
200 | + @staticmethod |
201 | + def weightless(charm): |
202 | + """Remove weight from charm because we cannot predict its value.""" |
203 | + charm = dict(charm) |
204 | + del charm['weight'] |
205 | + return charm |
206 | + |
207 | + @classmethod |
208 | + def weightless_relation(cls, relation): |
209 | + new_relation = {} |
210 | + for interface, charms in relation.items(): |
211 | + charms = [cls.weightless(charm) for charm in charms] |
212 | + new_relation[interface] = charms |
213 | + return new_relation |
214 | + |
215 | + @classmethod |
216 | + def related_weightless(cls, charm): |
217 | + return cls.weightless(cls.api_class._format_related(charm, 0)) |
218 | + |
219 | + |
220 | class TestAPICharms: |
221 | |
222 | def setUp(self): |
223 | @@ -259,19 +282,20 @@ |
224 | self.assertIn('new', data['result']) |
225 | self.assertIn('featured', data['result']) |
226 | self.assertEqual(['pop%d' % num for num in range(14, 4, -1)], |
227 | - [self.get_name(r) for r in data['result']['popular']]) |
228 | + [r['charm']['name'] |
229 | + for r in data['result']['popular']]) |
230 | self.assertEqual(self.api_class._charm_result(last_popular_charm, {}, |
231 | {}), |
232 | data['result']['popular'][0]) |
233 | self.assertEqual(['new%d' % num for num in range(14, 4, -1)], |
234 | - [self.get_name(r) for r in data['result']['new']]) |
235 | + [r['charm']['name'] for r in data['result']['new']]) |
236 | self.assertEqual(self.api_class._charm_result(last_new_charm, {}, {}), |
237 | data['result']['new'][0]) |
238 | self.assertItemsEqual( |
239 | ['featured%d' % num for num in range(14, -1, -1)], |
240 | - [self.get_name(r) for r in data['result']['featured']]) |
241 | + [r['charm']['name'] for r in data['result']['featured']]) |
242 | featured14, = [charm for charm in data['result']['featured'] if |
243 | - self.get_name(charm) == 'featured14'] |
244 | + charm['charm']['name'] == 'featured14'] |
245 | self.assertEqual( |
246 | self.api_class._charm_result(last_featured_charm, {}, {}), |
247 | featured14) |
248 | @@ -281,40 +305,6 @@ |
249 | response = self.get_response('charms', categories='asdf') |
250 | self.assertEqual(200, response.status_code) |
251 | |
252 | - |
253 | -class API1Mixin: |
254 | - |
255 | - api_class = API1 |
256 | - |
257 | - @staticmethod |
258 | - def weightless(charm): |
259 | - """Remove weight from charm because we cannot predict its value.""" |
260 | - charm = dict(charm) |
261 | - del charm['weight'] |
262 | - return charm |
263 | - |
264 | - @classmethod |
265 | - def weightless_relation(cls, relation): |
266 | - new_relation = {} |
267 | - for interface, charms in relation.items(): |
268 | - charms = [cls.weightless(charm) for charm in charms] |
269 | - new_relation[interface] = charms |
270 | - return new_relation |
271 | - |
272 | - @classmethod |
273 | - def related_weightless(cls, charm): |
274 | - return cls.weightless(cls.api_class._format_related(charm, 0)) |
275 | - |
276 | - |
277 | -class TestAPI1Charms(TestAPICharms, APITestBase, API1Mixin): |
278 | - |
279 | - def setUp(self): |
280 | - super(TestAPI1Charms, self).setUp() |
281 | - |
282 | - @staticmethod |
283 | - def get_name(charm): |
284 | - return charm['charm']['name'] |
285 | - |
286 | def test_charms(self): |
287 | """Charms endpoint produces expected result.""" |
288 | one_changes = [factory.makeChange(created=datetime(1999, 12, 31), |
289 | @@ -585,190 +575,8 @@ |
290 | ) |
291 | |
292 | |
293 | -class TestAPI0Charms(TestAPICharms, APITestBase): |
294 | - |
295 | - api_class = API0 |
296 | - |
297 | - @staticmethod |
298 | - def get_name(charm): |
299 | - return charm['name'] |
300 | - |
301 | - def test_charms(self): |
302 | - """Charms endpoint produces expected result.""" |
303 | - one_changes = [factory.makeChange(created=datetime(1999, 12, 31), |
304 | - revno=37, message='Hello')] |
305 | - one_id, one = self.makeCharm(name='name1', changes=one_changes, |
306 | - date_created=datetime(1997, 10, 29, |
307 | - 12, 10, 9), |
308 | - promulgated=True) |
309 | - self.index_client.index_charm(one) |
310 | - maintainer2 = 'jrandom@example.com (J. Random Hacker)' |
311 | - provides2 = {'cookie-factory': { |
312 | - 'interface': 'imap', |
313 | - }} |
314 | - requires2 = {'postal-service': { |
315 | - 'interface': 'envelope', |
316 | - }} |
317 | - options2 = { |
318 | - 'foobar': { |
319 | - 'type': 'str', |
320 | - 'default': 'baz', |
321 | - 'description': 'quxx', |
322 | - }, |
323 | - } |
324 | - files2 = { |
325 | - 'readme': { |
326 | - 'filename': 'README.md', |
327 | - 'subdir': '', |
328 | - }, |
329 | - 'config-changed': { |
330 | - 'filename': 'config-changed', |
331 | - 'subdir': 'hooks', |
332 | - }, |
333 | - } |
334 | - two_changes = [factory.makeChange(created=datetime(1998, 11, 30), |
335 | - revno=32, message='Hi', |
336 | - authors=['Baz <baz@example.org>'])] |
337 | - two_id, two = self.makeCharm(owner='jrandom', revision=3, |
338 | - series='warty', summary='Charm two', |
339 | - name='name2', revno=13, |
340 | - maintainer=maintainer2, |
341 | - commit_message='message2', |
342 | - provides=provides2, |
343 | - requires=requires2, options=options2, |
344 | - files=files2, subordinate=True, |
345 | - changes=two_changes, |
346 | - date_created=datetime(1996, 9, 28, |
347 | - 11, 9, 8)) |
348 | - response = self.get_response('charms') |
349 | - self.assertEqual(200, response.status_code) |
350 | - self.assertEqual('application/json', response.content_type) |
351 | - result = response.json_body['result'] |
352 | - self.assertEqual(2, len(result)) |
353 | - one_url = make_store_url(1, get_address(one, short=True)) |
354 | - self.assertEqual({'owner': 'charmers', |
355 | - 'maintainer': { |
356 | - 'name': 'John Doe', |
357 | - 'email': 'jdoe@example.com', |
358 | - }, |
359 | - 'id': one_url[3:], |
360 | - 'categories': [], |
361 | - 'date_created': '1997-10-29T12:10:09', |
362 | - 'name': 'name1', |
363 | - 'url': one_url, |
364 | - 'description': one['description'], |
365 | - 'revision': 1, |
366 | - 'summary': "Remote terminal classroom over https", |
367 | - 'distro_series': 'precise', |
368 | - 'is_approved': True, |
369 | - 'rating_numerator': 0, |
370 | - 'rating_denominator': 0, |
371 | - 'code_source': { |
372 | - 'type': 'bzr', |
373 | - 'location': |
374 | - 'lp:~charmers/charms/precise/name1/trunk', |
375 | - 'revision': '12', |
376 | - 'last_log': 'maintainer', |
377 | - 'bugs_link': |
378 | - 'https://bugs.launchpad.net/charms/' |
379 | - '+source/name1', |
380 | - 'revisions': [{ |
381 | - 'revno': 37, |
382 | - 'message': 'Hello', |
383 | - 'date': '1999-12-31T00:00:00Z', |
384 | - 'authors': [ |
385 | - { |
386 | - 'name': 'Foo', |
387 | - 'email': 'foo@example.com' |
388 | - }, |
389 | - { |
390 | - 'name': 'Bar', |
391 | - 'email': 'bar@example.com' |
392 | - }, |
393 | - ], |
394 | - }], |
395 | - }, |
396 | - 'relations': { |
397 | - 'provides': { |
398 | - 'website': { |
399 | - 'interface': 'https', |
400 | - }, |
401 | - }, |
402 | - 'requires': { |
403 | - 'database': { |
404 | - 'interface': 'mongodb', |
405 | - }, |
406 | - }, |
407 | - }, |
408 | - 'options': { |
409 | - 'script-interval': { |
410 | - 'type': 'int', |
411 | - 'default': 5, |
412 | - 'description': |
413 | - 'The interval between script runs.' |
414 | - }, |
415 | - }, |
416 | - 'files': ['README', 'hooks/install'], |
417 | - 'tested_providers': {}, |
418 | - 'downloads_in_past_30_days': 0, |
419 | - 'is_subordinate': False, |
420 | - }, result[0]) |
421 | - two_url = make_store_url(1, get_address(two, short=False)) |
422 | - self.assertEqual({'owner': 'jrandom', |
423 | - 'maintainer': { |
424 | - 'name': 'J. Random Hacker', |
425 | - 'email': 'jrandom@example.com', |
426 | - }, |
427 | - 'id': two_url[3:], |
428 | - 'categories': [], |
429 | - 'date_created': '1996-09-28T11:09:08', |
430 | - 'name': 'name2', |
431 | - 'url': two_url, |
432 | - 'description': two['description'], |
433 | - 'revision': 3, |
434 | - 'summary': 'Charm two', |
435 | - 'distro_series': 'warty', |
436 | - 'is_approved': False, |
437 | - 'rating_numerator': 0, |
438 | - 'rating_denominator': 0, |
439 | - 'code_source': { |
440 | - 'type': 'bzr', |
441 | - 'location': |
442 | - 'lp:~jrandom/charms/warty/name2/trunk', |
443 | - 'revision': '13', |
444 | - 'revisions': [{ |
445 | - 'revno': 32, |
446 | - 'message': 'Hi', |
447 | - 'date': '1998-11-30T00:00:00Z', |
448 | - 'authors': [ |
449 | - { |
450 | - 'name': 'Baz', |
451 | - 'email': 'baz@example.org' |
452 | - }, |
453 | - ], |
454 | - }], |
455 | - 'last_log': 'message2', |
456 | - 'bugs_link': |
457 | - 'https://bugs.launchpad.net/charms/+source/name2' |
458 | - }, |
459 | - 'relations': { |
460 | - 'provides': provides2, |
461 | - 'requires': requires2, |
462 | - |
463 | - }, |
464 | - 'options': options2, |
465 | - 'files': ['README.md', 'hooks/config-changed'], |
466 | - 'tested_providers': {}, |
467 | - 'downloads_in_past_30_days': 0, |
468 | - 'is_subordinate': True, |
469 | - }, result[1]) |
470 | - self.assertNotEqual(one_id, two_id) |
471 | - self.assertNotEqual(one_url, two_url) |
472 | - |
473 | - def test_charms_accepts_category(self): |
474 | - """Category is accepted (but ignored).""" |
475 | - response = self.get_response('charms', category='asdf') |
476 | - self.assertEqual(200, response.status_code) |
477 | +class TestAPI1Charms(TestAPICharms, API1Mixin): |
478 | + """Test the AP1 implementation of charms endpoint.""" |
479 | |
480 | |
481 | class TestAPI: |
482 | @@ -1151,9 +959,6 @@ |
483 | # Only the headers matter for a CORS preflight response. |
484 | self.assertEqual(0, response.content_length) |
485 | |
486 | - |
487 | -class TestAPI1(APITestBase, TestAPI, API1Mixin): |
488 | - |
489 | def test_charm_result_includes_charm_and_metadata(self): |
490 | charm = factory.get_charm_json(promulgated=False) |
491 | result = self.api_class._charm_result(charm, {}, {}) |
492 | @@ -1283,29 +1088,5 @@ |
493 | self.assertNotIn('icon', formatted) |
494 | |
495 | |
496 | -class TestAPI0(APITestBase, TestAPI): |
497 | - |
498 | - api_class = API0 |
499 | - |
500 | - @staticmethod |
501 | - def get_id(charm): |
502 | - return charm['id'] |
503 | - |
504 | - def test_charm_result_excludes_charm_and_metadata(self): |
505 | - charm = factory.get_charm_json(promulgated=False) |
506 | - result = self.api_class._charm_result(charm, {}, {}) |
507 | - self.assertNotIn('metadata', result) |
508 | - self.assertNotIn('charm', result) |
509 | - |
510 | - def test_format_charm_adds_icon(self): |
511 | - """If a charm provides an icon, it is added to the data returned |
512 | - by the API. |
513 | - """ |
514 | - charmid, charm = factory.makeCharm(self.db) |
515 | - # The test data does not contain an icon. |
516 | - self.assertFalse('icon' in charm) |
517 | - formatted = self.api_class._format_charm(charm) |
518 | - self.assertFalse('icon' in formatted) |
519 | - charm['icon'] = 'fake' |
520 | - formatted = self.api_class._format_charm(charm) |
521 | - self.assertTrue('icon' in formatted) |
522 | +class TestAPI1(TestAPI, API1Mixin): |
523 | + """Test API 1.""" |
524 | |
525 | === modified file 'migrations/migrate.py' |
526 | --- migrations/migrate.py 2013-02-12 11:36:14 +0000 |
527 | +++ migrations/migrate.py 2013-06-04 16:12:32 +0000 |
528 | @@ -17,6 +17,7 @@ |
529 | |
530 | from charmworld.models import getconnection |
531 | from charmworld.models import getdb |
532 | +from charmworld.search import ElasticSearchClient |
533 | from charmworld.utils import get_ini |
534 | |
535 | SCRIPT_TEMPLATE = """ |
536 | @@ -152,7 +153,13 @@ |
537 | """:returns: Latest version in Collection""" |
538 | return self.version_indexes[-1] if len(self.version_indexes) > 0 else 0 |
539 | |
540 | - def upgrade(self, datastore, init): |
541 | + def run_migration(self, db, index_client, module_name): |
542 | + module = imp.load_source( |
543 | + module_name.strip('.py'), |
544 | + join(self.versions_dir, module_name)) |
545 | + getattr(module, 'upgrade')(db, index_client) |
546 | + |
547 | + def upgrade(self, datastore, index_client, init): |
548 | """Run `upgrade` methods for required version files. |
549 | |
550 | :param datastore: An instance of DataStore |
551 | @@ -176,10 +183,7 @@ |
552 | # Let's get processing. |
553 | next_version = current_version + 1 |
554 | module_name = self.versions[next_version] |
555 | - module = imp.load_source( |
556 | - module_name.strip('.py'), |
557 | - join(self.versions_dir, module_name)) |
558 | - getattr(module, 'upgrade')(datastore.db) |
559 | + self.run_migration(datastore.db, index_client, module_name) |
560 | current_version = next_version |
561 | datastore.update_version(current_version) |
562 | return current_version |
563 | @@ -252,8 +256,9 @@ |
564 | def upgrade(cls, ini, args): |
565 | """Upgrade the data store to the latest available migration.""" |
566 | ds = cls.get_datastore(ini) |
567 | + index_client = ElasticSearchClient.from_settings(ini) |
568 | migrations = Versions(ini['migrations']) |
569 | - new_version = migrations.upgrade(ds, args.init) |
570 | + new_version = migrations.upgrade(ds, index_client, args.init) |
571 | |
572 | if new_version is None: |
573 | print 'There are no new migrations to apply' |
574 | |
575 | === modified file 'migrations/test_migrate.py' |
576 | --- migrations/test_migrate.py 2013-02-12 18:57:02 +0000 |
577 | +++ migrations/test_migrate.py 2013-06-04 16:12:32 +0000 |
578 | @@ -6,10 +6,10 @@ |
579 | from os.path import exists |
580 | import pymongo |
581 | from tempfile import mkdtemp |
582 | -from unittest import TestCase |
583 | |
584 | from charmworld.models import getconnection |
585 | from charmworld.models import getdb |
586 | +from charmworld.testing import TestCase |
587 | from charmworld.testing.factory import random_string |
588 | from charmworld.utils import get_ini |
589 | |
590 | @@ -155,16 +155,19 @@ |
591 | |
592 | def test_upgrade_wo_migrations(self): |
593 | """Upgrade runs silently if no migrations are created yet.""" |
594 | + self.use_index_client() |
595 | self.ds.version_datastore() |
596 | tmpdir = mkdtemp() |
597 | versions = Versions(tmpdir) |
598 | - self.assertEqual(versions.upgrade(self.ds, False), None) |
599 | + self.assertEqual(versions.upgrade(self.ds, self.index_client, False), |
600 | + None) |
601 | |
602 | def test_upgrade_succeeds(self): |
603 | """upgrade will run the methods and we get a version of 4.""" |
604 | self.ds.version_datastore() |
605 | + self.use_index_client() |
606 | # We should get 4 versions done. The upgrade returns 4 |
607 | - last_version = self.version.upgrade(self.ds, False) |
608 | + last_version = self.version.upgrade(self.ds, self.index_client, False) |
609 | self.assertEqual(4, last_version) |
610 | |
611 | # and we can query the db for the revision of 4 |
612 | @@ -172,7 +175,8 @@ |
613 | |
614 | def test_upgrade_succeeds_from_unversioned(self): |
615 | """Forcing init no an unversioned db will upgrade successfully.""" |
616 | - last_version = self.version.upgrade(self.ds, True) |
617 | + self.use_index_client() |
618 | + last_version = self.version.upgrade(self.ds, self.index_client, True) |
619 | self.assertEqual(4, last_version) |
620 | |
621 | # and we can query the db for the revision of 4 |
622 | |
623 | === modified file 'migrations/versions/001_load_qa_questions.py' |
624 | --- migrations/versions/001_load_qa_questions.py 2013-02-12 11:36:14 +0000 |
625 | +++ migrations/versions/001_load_qa_questions.py 2013-06-04 16:12:32 +0000 |
626 | @@ -9,7 +9,7 @@ |
627 | """ |
628 | |
629 | |
630 | -def upgrade(db): |
631 | +def upgrade(db, index_client): |
632 | """Complete this function with work to be done for the migration/update. |
633 | |
634 | db is the pymongo db instance for our datastore. Charms are in db.charms |
635 | |
636 | === modified file 'migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py' |
637 | --- migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py 2013-02-12 11:36:14 +0000 |
638 | +++ migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py 2013-06-04 16:12:32 +0000 |
639 | @@ -5,7 +5,7 @@ |
640 | |
641 | from charmworld.utils import quote_yaml |
642 | |
643 | -def upgrade(db): |
644 | +def upgrade(db, index_client): |
645 | """Iterate all charm data and replace all dictionary keys containing |
646 | a '.', '$' or '%' with an escaped variant. |
647 | """ |
648 | |
649 | === modified file 'migrations/versions/003_remove_provider_qa_questions.py' |
650 | --- migrations/versions/003_remove_provider_qa_questions.py 2013-04-23 19:23:17 +0000 |
651 | +++ migrations/versions/003_remove_provider_qa_questions.py 2013-06-04 16:12:32 +0000 |
652 | @@ -21,7 +21,7 @@ |
653 | ] |
654 | |
655 | |
656 | -def upgrade(db): |
657 | +def upgrade(db, index_client): |
658 | """Update each category of questions from new_category_data. |
659 | |
660 | This update elaborates on the 001_* migration. Subsequent qa form |
661 | |
662 | === modified file 'migrations/versions/004_remove_charm_errors.py' |
663 | --- migrations/versions/004_remove_charm_errors.py 2013-05-27 19:02:19 +0000 |
664 | +++ migrations/versions/004_remove_charm_errors.py 2013-06-04 16:12:32 +0000 |
665 | @@ -1,2 +1,4 @@ |
666 | -def upgrade(db): |
667 | +# Copyright 2013 Canonical Ltd. This software is licensed under the |
668 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
669 | +def upgrade(db, index_client): |
670 | db.drop_collection('charm-errors') |
671 | |
672 | === added file 'migrations/versions/005_delete_icon_field.py' |
673 | --- migrations/versions/005_delete_icon_field.py 1970-01-01 00:00:00 +0000 |
674 | +++ migrations/versions/005_delete_icon_field.py 2013-06-04 16:12:32 +0000 |
675 | @@ -0,0 +1,8 @@ |
676 | +# Copyright 2013 Canonical Ltd. This software is licensed under the |
677 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
678 | +from charmworld.search import reindex |
679 | + |
680 | + |
681 | +def upgrade(db, index_client): |
682 | + db.charms.update({}, {'$unset': {'icon':''}}) |
683 | + reindex(index_client, list(db.charms.find({}))) |
684 | |
685 | === modified file 'migrations/versions/tests/test_migrations.py' |
686 | --- migrations/versions/tests/test_migrations.py 2013-05-27 19:02:19 +0000 |
687 | +++ migrations/versions/tests/test_migrations.py 2013-06-04 16:12:32 +0000 |
688 | @@ -2,8 +2,15 @@ |
689 | # GNU Affero General Public License version 3 (see the file LICENSE). |
690 | |
691 | import imp |
692 | +from charmworld.models import CharmSource |
693 | from charmworld.testing import MongoTestBase |
694 | from charmworld.testing.factory import random_string |
695 | +from migrations.migrate import Versions |
696 | + |
697 | + |
698 | +def run_migration(db, index_client, module_name): |
699 | + versions = Versions('migrations/versions/') |
700 | + versions.run_migration(db, index_client, module_name) |
701 | |
702 | |
703 | class TestMigration002(MongoTestBase): |
704 | @@ -53,10 +60,18 @@ |
705 | class TestMigration004(MongoTestBase): |
706 | |
707 | def test_migration(self): |
708 | + self.use_index_client() |
709 | self.db.create_collection('charm-errors') |
710 | - module = imp.load_source( |
711 | - '004_remove_charm_errors', |
712 | - 'migrations/versions/' |
713 | - '004_remove_charm_errors.py') |
714 | - module.upgrade(self.db) |
715 | + run_migration(self.db, self.index_client, '004_remove_charm_errors.py') |
716 | self.assertNotIn('charm-errors', self.db.collection_names()) |
717 | + |
718 | + |
719 | +class TestMigration005(MongoTestBase): |
720 | + |
721 | + def test_migration(self): |
722 | + self.use_index_client() |
723 | + source = CharmSource.from_request(self) |
724 | + source.save({'_id': 'a', 'icon': 'asdf', 'asdf': 'asdf'}) |
725 | + run_migration(self.db, self.index_client, '005_delete_icon_field.py') |
726 | + for charm in source._get_all('a'): |
727 | + self.assertNotIn('icon', charm) |
Thank you.