Merge lp:~abentley/charmworld/icon-as-file into lp:~juju-jitsu/charmworld/trunk

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: 232
Merged at revision: 229
Proposed branch: lp:~abentley/charmworld/icon-as-file
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 194 lines (+74/-11)
4 files modified
charmworld/models.py (+16/-1)
charmworld/tests/test_models.py (+19/-0)
charmworld/views/api.py (+13/-10)
charmworld/views/tests/test_api.py (+26/-0)
To merge this branch: bzr merge lp:~abentley/charmworld/icon-as-file
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+164262@code.launchpad.net

Commit message

Store icon.svg

Description of the change

This branch enables using charm/file to retrieve the icon for a charm.

As well as ensuring it's provided, it also ensures the HTTP headers are suitable-- the MIME type is provided, and Cache-Control ensures that icons are cached for 24 hours. It is save to cache them this long because they change rarely, and when they do change, their default URL also changes.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworld/models.py'
--- charmworld/models.py 2013-05-03 15:47:38 +0000
+++ charmworld/models.py 2013-05-16 20:49:26 +0000
@@ -4,6 +4,7 @@
44
5import errno5import errno
6import logging6import logging
7from mimetypes import guess_type
7from os import listdir8from os import listdir
8from os.path import dirname9from os.path import dirname
9from os.path import exists10from os.path import exists
@@ -260,6 +261,7 @@
260 'metadata.yaml',261 'metadata.yaml',
261 'readme.*',262 'readme.*',
262 'revision',263 'revision',
264 'icon.svg',
263 ]265 ]
264266
265 store_directories = [267 store_directories = [
@@ -270,11 +272,13 @@
270 def make_charm_file(cls, fs, charm_data, file_path, filename):272 def make_charm_file(cls, fs, charm_data, file_path, filename):
271 charmid = cls.gen_charmid(charm_data)273 charmid = cls.gen_charmid(charm_data)
272 fileid = cls.gen_fileid(charm_data, file_path)274 fileid = cls.gen_fileid(charm_data, file_path)
275 content_type = guess_type(filename)[0]
273 return CharmFile(276 return CharmFile(
274 fs,277 fs,
275 fileid,278 fileid,
276 charmid,279 charmid,
277 filename=filename,280 filename=filename,
281 contentType=content_type,
278 )282 )
279283
280 @classmethod284 @classmethod
@@ -406,6 +410,7 @@
406 attributes = [410 attributes = [
407 'fileid',411 'fileid',
408 'charmid',412 'charmid',
413 'contentType',
409 'subdir',414 'subdir',
410 'filename',415 'filename',
411 'length',416 'length',
@@ -448,8 +453,11 @@
448 else:453 else:
449 return self._id.split('/')[-1]454 return self._id.split('/')[-1]
450455
456 # value to use as default to distinguish between None/null and unset.
457 undefined = object()
458
451 def __init__(self, fs, fileid, charmid, filename=None,459 def __init__(self, fs, fileid, charmid, filename=None,
452 instance=None, metadata=None):460 instance=None, metadata=None, contentType=undefined):
453 """Get a file object started.461 """Get a file object started.
454462
455 :param fs: the gridfs client to use.463 :param fs: the gridfs client to use.
@@ -466,6 +474,12 @@
466 self._id = fileid474 self._id = fileid
467 self.charmid = charmid475 self.charmid = charmid
468 self._filename = None476 self._filename = None
477 if contentType is not self.undefined:
478 self.contentType = contentType
479 elif instance is not None:
480 self.contentType = instance.contentType
481 else:
482 self.contentType = None
469483
470 if instance:484 if instance:
471 self._gridout = instance485 self._gridout = instance
@@ -506,6 +520,7 @@
506 self.fs.put(520 self.fs.put(
507 contents,521 contents,
508 filename=self.filename,522 filename=self.filename,
523 contentType=self.contentType,
509 **self.metadata524 **self.metadata
510 )525 )
511526
512527
=== modified file 'charmworld/tests/test_models.py'
--- charmworld/tests/test_models.py 2013-05-03 19:46:57 +0000
+++ charmworld/tests/test_models.py 2013-05-16 20:49:26 +0000
@@ -223,6 +223,7 @@
223 quote_key('config.yaml'),223 quote_key('config.yaml'),
224 quote_key('metadata.yaml'),224 quote_key('metadata.yaml'),
225 quote_key('README.md'),225 quote_key('README.md'),
226 quote_key('icon.svg'),
226 'hooks/config-changed',227 'hooks/config-changed',
227 'hooks/database-relation-broken',228 'hooks/database-relation-broken',
228 'hooks/database-relation-changed',229 'hooks/database-relation-changed',
@@ -352,6 +353,24 @@
352 self.assertEqual(expected, messages)353 self.assertEqual(expected, messages)
353 self.assertEqual([], files)354 self.assertEqual([], files)
354355
356 def test_provides_content_type(self):
357 path = self.use_context(temp_dir())
358 with file(join(path, 'icon.svg'), 'w') as f:
359 f.write('foo')
360 with file(join(path, 'readme.txt'), 'w') as f:
361 f.write('foo')
362 CharmFileSet.save_files(
363 self.fs,
364 self.charm_data,
365 path,
366 self.logger)
367 icon_id = self.base_fileid + quote_key('icon.svg')
368 cf = CharmFileSet.get_by_id(self.fs, icon_id)
369 self.assertEqual('image/svg+xml', cf.contentType)
370 readme_id = self.base_fileid + quote_key('readme.txt')
371 cf = CharmFileSet.get_by_id(self.fs, readme_id)
372 self.assertEqual('text/plain', cf.contentType)
373
355374
356class TestCharmFile(MongoTestBase):375class TestCharmFile(MongoTestBase):
357376
358377
=== modified file 'charmworld/views/api.py'
--- charmworld/views/api.py 2013-05-16 15:44:30 +0000
+++ charmworld/views/api.py 2013-05-16 20:49:26 +0000
@@ -5,7 +5,6 @@
5from inspect import getargspec5from inspect import getargspec
6import json6import json
7from os.path import (7from os.path import (
8 basename,
9 join,8 join,
10)9)
11import re10import re
@@ -311,19 +310,23 @@
311310
312 def _charm_file(self, charm, trailing):311 def _charm_file(self, charm, trailing):
313 path = trailing.split('/', 1)[1]312 path = trailing.split('/', 1)[1]
313 file_id = CharmFileSet.gen_fileid(charm, path)
314 fs = getfs(self.request.db)314 fs = getfs(self.request.db)
315 charm_file = CharmFileSet.make_charm_file(fs, charm, path,
316 basename(path))
317 try:315 try:
318 return Response(316 charm_file = CharmFileSet.get_by_id(fs, file_id)
319 charm_file.read(),
320 headerlist=[
321 ("Access-Control-Allow-Origin", "*"),
322 ("Access-Control-Allow-Headers", "X-Requested-With"),
323 ],
324 status_code=200)
325 except NoFile:317 except NoFile:
326 return json_response(404, {'type': 'no_such_file', 'path': path})318 return json_response(404, {'type': 'no_such_file', 'path': path})
319 headerlist = [
320 ("Access-Control-Allow-Origin", "*"),
321 ("Access-Control-Allow-Headers", "X-Requested-With"),
322 ("Cache-Control", "max-age=86400, public"),
323 ]
324 if charm_file.contentType is not None:
325 headerlist.append(('Content-Type', charm_file.contentType))
326 return Response(
327 charm_file.read(),
328 headerlist=headerlist,
329 status_code=200)
327330
328 @staticmethod331 @staticmethod
329 def _format_category(category):332 def _format_category(category):
330333
=== modified file 'charmworld/views/tests/test_api.py'
--- charmworld/views/tests/test_api.py 2013-05-16 14:46:25 +0000
+++ charmworld/views/tests/test_api.py 2013-05-16 20:49:26 +0000
@@ -1015,6 +1015,32 @@
1015 self.assertEqual({'type': 'no_such_file', 'path': 'subdir/README'},1015 self.assertEqual({'type': 'no_such_file', 'path': 'subdir/README'},
1016 response.json_body)1016 response.json_body)
10171017
1018 def test_file_includes_content_type(self):
1019 charm = factory.makeCharm(self.db)[1]
1020 factory.make_charm_file(self.db, charm, 'readme.txt')
1021 factory.make_charm_file(self.db, charm, 'readme')
1022 factory.make_charm_file(self.db, charm, 'readme.svg')
1023 charm_id = self.api_class._get_api_id(charm)
1024 response = self.get_response(
1025 'charm', remainder='/' + charm_id + '/file/readme.txt')
1026 self.assertEqual('text/plain', response.content_type)
1027 response = self.get_response(
1028 'charm', remainder='/' + charm_id + '/file/readme')
1029 response = self.get_response(
1030 'charm', remainder='/' + charm_id + '/file/readme.svg')
1031 self.assertEqual('image/svg+xml', response.content_type)
1032
1033 def test_file_includes_cache_instructions(self):
1034 # The files can be cached a long time because they rarely change, and
1035 # when they do, their default URL will also change.
1036 charm = factory.makeCharm(self.db)[1]
1037 factory.make_charm_file(self.db, charm, 'readme.txt')
1038 charm_id = self.api_class._get_api_id(charm)
1039 response = self.get_response(
1040 'charm', remainder='/' + charm_id + '/file/readme.txt')
1041 self.assertIn(("Cache-Control", "max-age=86400, public"),
1042 response.headerlist)
1043
1018 def test_unknown_sub_endpoint(self):1044 def test_unknown_sub_endpoint(self):
1019 charm = factory.makeCharm(self.db)[1]1045 charm = factory.makeCharm(self.db)[1]
1020 charm_id = self.api_class._get_api_id(charm)1046 charm_id = self.api_class._get_api_id(charm)

Subscribers

People subscribed via source and target branches