Merge lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache-really into lp:ubuntu-webcatalog

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Foord
Approved revision: 29
Merged at revision: 23
Proposed branch: lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache-really
Merge into: lp:ubuntu-webcatalog
Diff against target: 400 lines (+170/-51)
5 files modified
django_project/config/main.cfg (+1/-0)
src/webcatalog/forms.py (+21/-2)
src/webcatalog/management/commands/import_app_install_data.py (+50/-25)
src/webcatalog/tests/test_commands.py (+76/-21)
src/webcatalog/tests/test_forms.py (+22/-3)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-webcatalog/777871-pull-other-apps-from-apt-cache-really
Reviewer Review Type Date Requested Status
Michael Foord (community) Approve
Ricardo Kirkner (community) Approve
Review via email: mp+65655@code.launchpad.net

Commit message

Import apps from the apt-cache for the distroseries in addition to the app-install-data.

Description of the change

Overview
========
Imports and updates applications from the apt-cache before processing the desktop data in app-install-data. (bug 777871).

Details
=======
As well as providing the new update_from_cache method for use during import, this branch also ensures that when the desktop files are subsequently parsed, we don't overwrite values, like Application.description (that were set from the cache) with empty values from the desktop file.

While there, I simplified the process_desktop_file method by moving some form-related code into the form where it belongs.

Issues
======

As far as I can see, the only useful data I can grab from the apt-cache is: package name, description, section and summary. This leaves us with a few issues that we may either want to fix in this branch, or create bugs and fix soon:
1) Our templates assume that apps have a name (eg. we use Application.name when linking to package details), but for apps that are created from the cache only, I'm not sure what we should use (perhaps a template tag name_or_package_name)
2) We've got required fields, such as popcon on the Application model, which we don't get from the cache, so apps which are created from cache only won't have this (meaning records that can't be edited in the admin).

To test: follow the readme to bootstrap, then `fab test`

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

l. 113-129: this can be replaced by a call to get_or_create, avoiding the need of a try..except block

review: Needs Fixing
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

> l. 113-129: this can be replaced by a call to get_or_create, avoiding the need
> of a try..except block

I was mistaken. This cannot be done without saving twice.

review: Approve
28. By Michael Nelson

Ensure that apt-cache only entries split the cache entry summary into name/comment.

29. By Michael Nelson

Ensure we include updates in the cache, not just the release.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hi, I've pushed revs 28 and 29, which addresses issue (1) that I noted above. It also ensures that we populate the apt-cache using not just main, but also universe for both the release and updates.

Revision history for this message
Michael Foord (mfoord) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/config/main.cfg'
2--- django_project/config/main.cfg 2011-06-21 11:31:27 +0000
3+++ django_project/config/main.cfg 2011-06-24 07:54:54 +0000
4@@ -53,6 +53,7 @@
5 template_dirs = django_project/templates/
6 static_root = ./django_project/static/
7 static_url = /assets/
8+admin_media_prefix = /assets/admin/
9
10 # Django-1.1 backwards compatibility
11 database_engine = sqlite3
12
13=== modified file 'src/webcatalog/forms.py'
14--- src/webcatalog/forms.py 2011-06-17 16:21:59 +0000
15+++ src/webcatalog/forms.py 2011-06-24 07:54:54 +0000
16@@ -56,7 +56,7 @@
17 exclude = ('distroseries','for_purchase', 'archive_id')
18
19 @classmethod
20- def get_form_from_desktop_data(cls, str_data):
21+ def get_form_from_desktop_data(cls, str_data, distroseries):
22 parser = ConfigParser()
23 parser.readfp(StringIO(str_data))
24 data = dict(parser.items('Desktop Entry'))
25@@ -65,4 +65,23 @@
26 data[app_key] = data[desktop_key]
27 del(data[desktop_key])
28
29- return cls(data=data)
30+ try:
31+ instance = Application.objects.get(
32+ package_name=data['package_name'], distroseries=distroseries)
33+ except Application.DoesNotExist:
34+ instance = None
35+
36+ return cls(data=data, instance=instance)
37+
38+ def clean(self):
39+ cleaned_data = super(ApplicationForm, self).clean()
40+
41+ # For all fields, ensure we don't overwrite valid data on an
42+ # associated instance with blank fields.
43+ if self.instance.id:
44+ for key, value in cleaned_data.items():
45+ if not value:
46+ instance_value = getattr(self.instance, key, False)
47+ cleaned_data[key] = instance_value or value
48+
49+ return cleaned_data
50
51=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
52--- src/webcatalog/management/commands/import_app_install_data.py 2011-06-17 14:37:58 +0000
53+++ src/webcatalog/management/commands/import_app_install_data.py 2011-06-24 07:54:54 +0000
54@@ -59,8 +59,11 @@
55 series_cache_location, 'etc', 'apt', 'sources.list')
56 with open(sources_list, "w+") as sources_list_file:
57 sources_list_file.write(
58- 'deb http://archive.ubuntu.com/ubuntu {0} main\n'.format(
59- self.distroseries))
60+ 'deb http://archive.ubuntu.com/ubuntu {distroseries}'
61+ ' main universe\n'
62+ 'deb http://archive.ubuntu.com/ubuntu {distroseries}-updates'
63+ ' main universe\n'
64+ .format(distroseries=self.distroseries))
65
66 def get_apt_cache(self):
67 # Create on-disk cache for distroseries if it
68@@ -121,6 +124,7 @@
69 self.stdout.write(
70 "Created a DistroSeries record with codename '{0}'.".format(
71 distroseries_name))
72+ self.update_from_cache(cache, distroseries_name)
73
74 # Download app install data when requested.
75 if options['local_app_install_deb'] == '':
76@@ -177,31 +181,11 @@
77 cache):
78 with open(desktop_file_path, 'r') as desktop_file:
79 data = desktop_file.read()
80- form = ApplicationForm.get_form_from_desktop_data(data)
81+ form = ApplicationForm.get_form_from_desktop_data(data, distroseries)
82
83 if form.is_valid():
84- # Check if the app already exists in our DB.
85- try:
86- app = Application.objects.get(
87- package_name=form.cleaned_data['package_name'],
88- distroseries=distroseries)
89- # Force the form to update the existing application.
90- # Simply setting the forms.instance doesn't work as the
91- # form constructs its instance during is_valid, and
92- # doesn't reconstruct it to save.
93- app = construct_instance(form, app)
94- except Application.DoesNotExist:
95- app = form.save(commit=False)
96- app.distroseries = distroseries
97-
98- # we don't have the description - just the desktop data, so:
99- # use the package_name to lookup the app in the cache and get
100- # the descrption from there
101- try:
102- cached_app = cache[app.package_name]
103- app.description = cached_app.candidate.description
104- except KeyError:
105- pass
106+ app = form.save(commit=False)
107+ app.distroseries = distroseries
108 app.save()
109 app.update_departments()
110 self.add_icon_to_app(app, icon_dir)
111@@ -248,3 +232,44 @@
112 u"No icon found for {0} at {1}.({2}).\n".format(
113 app.name, icon_path_generic,
114 '|'.join(supported_extensions)).encode('utf-8'))
115+
116+ def update_from_cache(self, cache, distroseries_name):
117+ distroseries, created = DistroSeries.objects.get_or_create(
118+ code_name=distroseries_name)
119+ if created and self.verbosity > 0:
120+ self.stdout.write(
121+ "Created a DistroSeries record with codename '{0}'.".format(
122+ distroseries_name))
123+
124+ # For each package in the apt-cache, we update (or possibly
125+ # create) a matching db entry.
126+ for package in cache:
127+ candidate = package.candidate
128+ # USC uses the first line of the summary as the
129+ # application name and the rest as the comment..
130+ summary_lines = candidate.summary.split('\n')
131+ app_name = summary_lines[0]
132+ app_comment = '\n'.join(summary_lines[1:])
133+ if not app_comment:
134+ app_comment = package.name
135+
136+ try:
137+ app = Application.objects.get(
138+ package_name=package.name,
139+ distroseries=distroseries)
140+ app.description = candidate.description
141+ app.section = candidate.section
142+ app.name = app_name
143+ app.comment = app_comment
144+ app.save()
145+
146+ except Application.DoesNotExist:
147+ app = Application.objects.create(
148+ package_name=package.name,
149+ distroseries=distroseries,
150+ description=candidate.description,
151+ section=candidate.section,
152+ name=app_name,
153+ comment=app_comment,
154+ )
155+ return distroseries
156
157=== modified file 'src/webcatalog/tests/test_commands.py'
158--- src/webcatalog/tests/test_commands.py 2011-06-17 21:27:33 +0000
159+++ src/webcatalog/tests/test_commands.py 2011-06-24 07:54:54 +0000
160@@ -30,11 +30,18 @@
161 from django.core.management import call_command
162 from mock import (
163 patch,
164+ MagicMock,
165 Mock,
166 )
167
168-from webcatalog.models import Application
169-from webcatalog.management.commands.import_app_install_data import Command
170+from webcatalog.models import (
171+ Application,
172+ DistroSeries,
173+ )
174+from webcatalog.management.commands.import_app_install_data import (
175+ Cache as CacheContextManager,
176+ Command,
177+ )
178 from webcatalog.tests.factory import TestCaseWithFactory
179
180 __metaclass__ = type
181@@ -53,11 +60,14 @@
182 # firefox and scribus in the cache.
183 use_mock_apt_cache = True
184
185- def make_mock_apt_package(self, description, uri=''):
186+ def make_mock_apt_package(self, name, description, uri='', summary=''):
187 """Helper to DRY up creating a mock apt package."""
188 mock_package = Mock(spec=apt.package.Package)
189 mock_package.candidate.description = description
190 mock_package.candidate.uri = uri
191+ mock_package.name = name
192+ mock_package.candidate.section = ''
193+ mock_package.candidate.summary = summary
194 return mock_package
195
196 def make_mock_apt_cache(self):
197@@ -65,16 +75,17 @@
198
199 And keeping the setUp readable.
200 """
201- mock_cache = Mock()
202- mock_apt_firefox = self.make_mock_apt_package(
203- description = "Firefox description")
204- mock_apt_scribus = self.make_mock_apt_package(
205- description = "Scribus description")
206- mock_other_app = self.make_mock_apt_package(
207- description = "Otherapp description")
208- mock_app_install_data = self.make_mock_apt_package(
209- description = "App install desc.",
210- uri = 'http://example.com/app-install-1.01.deb')
211+ mock_cache = MagicMock()
212+ mock_apt_firefox = self.make_mock_apt_package('firefox',
213+ description="Firefox description")
214+ mock_apt_scribus=self.make_mock_apt_package('scribus',
215+ description="Scribus description")
216+ mock_other_app=self.make_mock_apt_package('otherapp',
217+ description="Otherapp description",
218+ summary="Otherapp the Internet\nA tagline for Otherapp")
219+ mock_app_install_data = self.make_mock_apt_package('app-install-data',
220+ description="App install desc.",
221+ uri='http://example.com/app-install-1.01.deb')
222 cache_dict = {
223 'firefox': mock_apt_firefox,
224 'scribus': mock_apt_scribus,
225@@ -84,8 +95,15 @@
226
227 def getitem(self, key):
228 return cache_dict.get(key, None)
229-
230 mock_cache.__getitem__ = getitem
231+
232+ # XXX 2011-06-23 michaeln Can't assign a bound method to a magic
233+ # mock method. If the following issue is fixed, we'll be able to
234+ # do `mock_cache.__iter__ = cache_dict.itervalues` without a
235+ # MagicMock.
236+ # https://code.google.com/p/mock/issues/detail?id=98
237+ mock_cache.__iter__.return_value = cache_dict.itervalues()
238+
239 return mock_cache
240
241 def setUp(self):
242@@ -108,7 +126,6 @@
243 self.mock_cache = self.make_mock_apt_cache()
244 self.mock_cache_class.return_value = self.mock_cache
245
246-
247 def tearDown(self):
248 self.patch_cache_setting.stop()
249 shutil.rmtree(self.tmp_apt_cache)
250@@ -166,7 +183,7 @@
251 'app-install-data-test_all.deb'),
252 verbosity=0)
253
254- self.assertEqual(2, Application.objects.count())
255+ self.assertEqual(4, Application.objects.count())
256 scribus = Application.objects.get(package_name='scribus')
257 self.assertEqual("Scribus (Stable)", scribus.name)
258 self.assertEqual(
259@@ -216,7 +233,7 @@
260 'app-install-data-test_all.deb'),
261 verbosity=0)
262
263- self.assertEqual(2, Application.objects.count())
264+ self.assertEqual(4, Application.objects.count())
265 scribus = Application.objects.get(package_name='scribus')
266 self.assertEqual('natty', scribus.distroseries.code_name)
267
268@@ -227,7 +244,7 @@
269 'app-install-data-test_all.deb'),
270 verbosity=0)
271
272- self.assertEqual(2, Application.objects.count())
273+ self.assertEqual(4, Application.objects.count())
274 scribus = Application.objects.get(package_name='scribus')
275 firefox = Application.objects.get(package_name='firefox')
276 self.assertEqual(
277@@ -251,11 +268,15 @@
278 'app-install-data-test_all.deb'),
279 verbosity=0)
280
281- self.assertEqual(2, Application.objects.count())
282+ self.assertEqual(4, Application.objects.count())
283 firefox = Application.objects.get(package_name='firefox')
284 scribus = Application.objects.get(package_name='scribus')
285 self.assertEqual("Firefox description", firefox.description)
286 self.assertEqual("Scribus description", scribus.description)
287+ # The name and comment come directly from the desktop file when
288+ # it exists.
289+ self.assertEqual("Firefox Web Browser", firefox.name)
290+ self.assertEqual("Browse the World Wide Web", firefox.comment)
291
292 def test_description_updated_in_app(self):
293 app = self.factory.make_application(package_name='scribus',
294@@ -282,8 +303,16 @@
295 verbosity=0)
296
297 natty_cache = os.path.join(self.tmp_apt_cache, 'natty')
298- self.assertTrue(os.path.exists(os.path.join(
299- natty_cache, 'etc', 'apt', 'sources.list')))
300+ sources_list_path = os.path.join(natty_cache, 'etc', 'apt',
301+ 'sources.list')
302+ self.assertTrue(os.path.exists(os.path.join(sources_list_path)))
303+ with open(sources_list_path) as content:
304+ sources_list_content = content.read()
305+ self.assertEqual(
306+ 'deb http://archive.ubuntu.com/ubuntu natty main universe\n'
307+ 'deb http://archive.ubuntu.com/ubuntu '
308+ 'natty-updates main universe\n',
309+ sources_list_content)
310 if self.use_mock_apt_cache:
311 self.mock_cache_class.assert_called_with(rootdir=natty_cache)
312 self.assertEqual(1, self.mock_cache.update.call_count)
313@@ -311,6 +340,32 @@
314 self.mock_cache_class.assert_called_with(rootdir=series_cache)
315 self.assertEqual(1, self.mock_cache.update.call_count)
316
317+ def test_update_from_cache_creates_distroseries_when_required(self):
318+ self.assertEqual(0, DistroSeries.objects.filter(
319+ code_name='natty').count())
320+
321+ with CacheContextManager('natty') as cache:
322+ Command().update_from_cache(cache, 'natty')
323+
324+ self.assertEqual(1, DistroSeries.objects.filter(
325+ code_name='natty').count())
326+
327+ def test_apt_cache_apps_used_also(self):
328+ self.assertEqual(0, Application.objects.count())
329+
330+ with CacheContextManager('natty') as cache:
331+ Command().update_from_cache(cache, 'natty')
332+
333+ # There are 4 packages in our mock_cache which is patched in the
334+ # setUp.
335+ self.assertEqual(4, Application.objects.count())
336+ # As cache entries don't include the app name or comment, the
337+ # summary is split to create those fields.
338+ otherapp = Application.objects.get(package_name='otherapp')
339+ self.assertEqual('Otherapp the Internet', otherapp.name)
340+ self.assertEqual('A tagline for Otherapp', otherapp.comment)
341+
342+
343 class ImportForPurchaseAppsTestCase(TestCaseWithFactory):
344
345 def setUp(self):
346
347=== modified file 'src/webcatalog/tests/test_forms.py'
348--- src/webcatalog/tests/test_forms.py 2011-04-12 15:22:54 +0000
349+++ src/webcatalog/tests/test_forms.py 2011-06-24 07:54:54 +0000
350@@ -81,12 +81,30 @@
351 }
352 desktop_entry = self.get_desktop_entry(self.get_desktop_data(data))
353
354- form = ApplicationForm.get_form_from_desktop_data(desktop_entry)
355+ form = ApplicationForm.get_form_from_desktop_data(desktop_entry,
356+ distroseries=self.factory.make_distroseries())
357
358 self.assertTrue(form.is_valid())
359 self.assertEqual('My Package', form.cleaned_data['name'])
360 self.assertEqual('mypkg', form.cleaned_data['package_name'])
361
362+ def test_get_form_from_desktop_data_with_existing(self):
363+ # If the desktop data contains some blanks, then the initial
364+ # data from any existing instance should be used instead.
365+ app = self.factory.make_application(package_name='mypkg',
366+ description='MyPkg description')
367+ data = {
368+ 'Name': 'My Package',
369+ 'X-AppInstall-Package': 'mypkg',
370+ }
371+ desktop_entry = self.get_desktop_entry(self.get_desktop_data(data))
372+
373+ form = ApplicationForm.get_form_from_desktop_data(desktop_entry,
374+ distroseries=app.distroseries)
375+
376+ self.assertTrue(form.is_valid())
377+ self.assertEqual('MyPkg description', form.cleaned_data['description'])
378+
379 def test_extra_fields(self):
380 # There are a bunch of extra fields which are also supported by
381 # the model form.
382@@ -96,7 +114,7 @@
383 'X-AppInstall-Screenshot-Url': 'http://example.com/screenshot',
384 'Type': 'Application',
385 'X-AppInstall-Description': 'A description',
386- 'X-AppInstall-Architectures': 'i386r;amd64',
387+ 'X-AppInstall-Architectures': 'i386;amd64',
388 'X-AppInstall-Popcon': 9876,
389 'X-AppInstall-Keywords': 'jazz,rock,country',
390 'Categories': 'Graphics;Jazz;Publishing',
391@@ -105,7 +123,8 @@
392 desktop_entry = self.get_desktop_entry(self.get_desktop_data(
393 extra_desktop_info))
394
395- form = ApplicationForm.get_form_from_desktop_data(desktop_entry)
396+ form = ApplicationForm.get_form_from_desktop_data(desktop_entry,
397+ distroseries=self.factory.make_distroseries())
398
399 self.assertTrue(form.is_valid())
400 for key, value in extra_desktop_info.items():

Subscribers

People subscribed via source and target branches