Merge lp:~software-updates-spec/update-manager/group-by-applications into lp:update-manager

Proposed by Michael Terry on 2013-01-22
Status: Merged
Merged at revision: 2582
Proposed branch: lp:~software-updates-spec/update-manager/group-by-applications
Merge into: lp:update-manager
Diff against target: 2166 lines (+1173/-366)
17 files modified
UpdateManager/Core/MyCache.py (+2/-55)
UpdateManager/Core/UpdateList.py (+303/-67)
UpdateManager/Core/utils.py (+67/-28)
UpdateManager/UpdatesAvailable.py (+417/-156)
data/com.ubuntu.update-manager.gschema.xml.in (+2/-2)
data/gtkbuilder/UpdateManager.ui (+3/-7)
tests/aptroot-grouping-test/etc/apt/sources.list (+2/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release (+9/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg (+7/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages (+10/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release (+9/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg (+7/-0)
tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_main_binary-amd64_Packages (+80/-0)
tests/aptroot-grouping-test/var/lib/dpkg/status (+82/-0)
tests/test_update_list.py (+89/-9)
tests/test_update_origin.py (+14/-23)
tests/test_utils.py (+70/-19)
To merge this branch: bzr merge lp:~software-updates-spec/update-manager/group-by-applications
Reviewer Review Type Date Requested Status
Matthew Paul Thomas (community) design Approve on 2013-01-24
Michael Vogt 2013-01-22 Approve on 2013-01-23
Review via email: mp+144362@code.launchpad.net

Description of the change

This is a continuation of https://code.launchpad.net/~dylanmccall/update-manager/group-by-applications/+merge/112678

This implements the 'Available Updates' description pane in the Software Updates spec:
https://wiki.ubuntu.com/SoftwareUpdates#Expanded_presentation_of_updates

Notably, it replaces the current list of packages with groups of packages and highlights the description of the package rather than the debian package name.

It also no longer categorizes the updates by source. The only categorization it does is "security or not".

It's a big diff, I know. Sorry. Mostly visual changes. And I added tests for the non-visual bits.

One big unfinished part of the spec is that when two packages have the same description (e.g. "Transitional package") we don't append the debian package name (e.g. "Transitional package (unity-2d)"). This is technically difficult. Talking to mpt, he indicated that just showing the label without the parenthetical would be an acceptable workaround until we can implement that bit. Ideally packages wouldn't have such duplicate descriptions.

Test with: ./update-manager --data-dir=./data

To post a comment you must log in.
2486. By Dylan McCall on 2013-01-22

Fixed application crashing when running with XDG_DATA_DIRS or XDG_CURRENT_DESKTOP unset.

Michael Vogt (mvo) wrote :

On Tue, Jan 22, 2013 at 06:25:25PM -0000, Michael Terry wrote:
> Michael Terry has proposed merging lp:~software-updates-spec/update-manager/group-by-applications into lp:update-manager.
>
> Requested reviews:
> Ubuntu Core Development Team (ubuntu-core-dev)
>
> For more details, see:
> https://code.launchpad.net/~software-updates-spec/update-manager/group-by-applications/+merge/144362
>
> This is a continuation of https://code.launchpad.net/~dylanmccall/update-manager/group-by-applications/+merge/112678
>
> This implements the 'Available Updates' description pane in the Software Updates spec:
> https://wiki.ubuntu.com/SoftwareUpdates#Expanded_presentation_of_updates

Thanks for this branch! I haven't really had a chance to test-run it
yet, my bandwidth right now is not very good. From looking at the diff
it looks good, I like the new tests and the new
"aptroot-group-testing"!

Some things I noticed during reading the diff, nothing that really
needs fixing, just tiny nit-picking.

[..]
> + def __init__(self, parent, dist=None):
> + self.dist = dist
> + if self.dist is None:
> + try:
> + self.dist = subprocess.check_output(
> + ["lsb_release", "-c", "-s"],
> + universal_newlines=True).strip()
> + except subprocess.CalledProcessError as e:
> + print("Error in lsb_release: %s" % e)
[..]

As a tiny nit-pick, this above could be written also via:

import platform
self.dist = platform.dist()[2]

[..]
> +import xml.sax.saxutils
..
> +def get_package_label(pkg):
> + """ this takes a package synopsis and uppercases the first word's
> + first letter
> + """
> + import xml.sax.saxutils
> + name = xml.sax.saxutils.escape(getattr(pkg.candidate, "summary",
> ""))
> + return capitalize_first_word(name)

Looks like one of the imports can be skipped. Or (probably better) we use
GLib.markup_escape_text() instead.

> + if len(cells) != 3 or \
> + not isinstance(cells[0], Gtk.CellRendererToggle) or \
> + not isinstance(cells[1], Gtk.CellRendererPixbuf) or \
> + not isinstance(cells[2], Gtk.CellRendererText):
> + return

Most of the other python code seems to put "(" around ")" the multi
line if statements, either is fine with me, just wanted to mention
it.

Cheers,
 Michael

Michael Vogt (mvo) wrote :

I just got the chance to play with it too, looks very nice indeed.

review: Approve
2487. By Michael Terry on 2013-01-23

merge from trunk

2488. By Michael Terry on 2013-01-23

add myself to UpdateList authors and update copyright in UpdateList and UpdatesAvailable

2489. By Michael Terry on 2013-01-23

fix nits from review

2490. By Michael Terry on 2013-01-23

sort entries case insensitively

Michael Terry (mterry) wrote :

OK, fixed the nits and added one more fix -- sorting entries case insensitively. I wanted mpt to review changes too just to make sure I didn't screw his vision up. He said he'd get to it on Friday likely.

Matthew Paul Thomas (mpt) wrote :

This looks excellent to me. The behavior of the tri-state checkboxes isn't perfect, but that can certainly wait.

review: Approve (design)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'UpdateManager/Core/MyCache.py'
2--- UpdateManager/Core/MyCache.py 2012-06-28 00:10:23 +0000
3+++ UpdateManager/Core/MyCache.py 2013-01-23 15:48:20 +0000
4@@ -43,7 +43,6 @@
5 import re
6 import DistUpgrade.DistUpgradeCache
7 from gettext import gettext as _
8-from .UpdateList import UpdateOrigin
9
10 SYNAPTIC_PINFILE = "/var/lib/synaptic/preferences"
11 CHANGELOGS_POOL = "http://changelogs.ubuntu.com/changelogs/pool/"
12@@ -133,59 +132,6 @@
13 self._depcache.upgrade()
14 return wouldDelete
15
16- def match_package_origin(self, pkg, matcher):
17- """ match 'pkg' origin against 'matcher', take versions between
18- installed.version and candidate.version into account too
19- Useful if installed pkg A v1.0 is available in both
20- -updates (as v1.2) and -security (v1.1). we want to display
21- it as a security update then
22- """
23- inst_ver = pkg._pkg.current_ver
24- cand_ver = self._depcache.get_candidate_ver(pkg._pkg)
25- # init matcher with candidate.version
26- update_origin = matcher[(None, None)]
27- verFileIter = None
28- for (verFileIter, index) in cand_ver.file_list:
29- if (verFileIter.archive, verFileIter.origin) in matcher:
30- indexfile = pkg._pcache._list.find_index(verFileIter)
31- if indexfile: # and indexfile.IsTrusted:
32- match = matcher[verFileIter.archive, verFileIter.origin]
33- update_origin = match
34- break
35- else:
36- # add a node for each origin/archive combination
37- if verFileIter and verFileIter.origin and verFileIter.archive:
38- matcher[verFileIter.archive, verFileIter.origin] = \
39- UpdateOrigin(_("Other updates (%s)") % verFileIter.origin,
40- 0)
41- update_origin = matcher[verFileIter.archive,
42- verFileIter.origin]
43- # if the candidate comes from a unknown source (e.g. a PPA) skip
44- # skip the shadow logic below as it would put e.g. a PPA package
45- # in "Recommended updates" when the version in the PPA
46- # is higher than the one in %s-updates
47- if update_origin.importance <= 0:
48- return update_origin
49- # for known packages, check if we have higher versions that
50- # "shadow" this one
51- for ver in pkg._pkg.version_list:
52- # discard is < than installed ver
53- if (inst_ver and
54- apt_pkg.version_compare(ver.ver_str,
55- inst_ver.ver_str) <= 0):
56- #print("skipping '%s' " % ver.ver_str)
57- continue
58- # check if we have a match
59- for (verFileIter, index) in ver.file_list:
60- if (verFileIter.archive, verFileIter.origin) in matcher:
61- indexfile = pkg._pcache._list.find_index(verFileIter)
62- if indexfile: # and indexfile.IsTrusted:
63- match = matcher[verFileIter.archive,
64- verFileIter.origin]
65- if match.importance > update_origin.importance:
66- update_origin = match
67- return update_origin
68-
69 def _strip_epoch(self, verstr):
70 " strip of the epoch "
71 l = verstr.split(":")
72@@ -356,9 +302,10 @@
73 def get_changelog(self, name):
74 " get the changelog file from the changelog location "
75 origins = self[name].candidate.origins
76- self.all_changes[name] = _("Changes for the versions:\n"
77+ self.all_changes[name] = _("Changes for %s versions:\n"
78 "Installed version: %s\n"
79 "Available version: %s\n\n") % (
80+ name,
81 getattr(self[name].installed, "version",
82 None),
83 self[name].candidate.version)
84
85=== modified file 'UpdateManager/Core/UpdateList.py'
86--- UpdateManager/Core/UpdateList.py 2013-01-22 17:59:35 +0000
87+++ UpdateManager/Core/UpdateList.py 2013-01-23 15:48:20 +0000
88@@ -1,9 +1,11 @@
89 # UpdateList.py
90 # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
91 #
92-# Copyright (c) 2004-2012 Canonical
93+# Copyright (c) 2004-2013 Canonical
94 #
95 # Author: Michael Vogt <mvo@debian.org>
96+# Dylan McCall <dylanmccall@ubuntu.com>
97+# Michael Terry <michael.terry@canonical.com>
98 #
99 # This program is free software; you can redistribute it and/or
100 # modify it under the terms of the GNU General Public License as
101@@ -22,36 +24,131 @@
102
103 from __future__ import print_function
104
105+import warnings
106+warnings.filterwarnings("ignore", "Accessed deprecated property",
107+ DeprecationWarning)
108+
109 from gettext import gettext as _
110-
111 import apt
112 import logging
113 import operator
114+import itertools
115+import platform
116+import subprocess
117+import os
118 import random
119-import subprocess
120-
121-
122-class UpdateOrigin(object):
123+import glob
124+
125+from gi.repository import Gio
126+
127+from UpdateManager.Core import utils
128+
129+
130+class UpdateItem():
131+ def __init__(self, pkg, name, icon):
132+ self.icon = icon
133+ self.name = name
134+ self.pkg = pkg
135+
136+
137+class UpdateGroup(UpdateItem):
138+ def __init__(self, pkg, name, icon):
139+ UpdateItem.__init__(self, pkg, name, icon)
140+ self._items = set()
141+ self.core_item = None
142+ if pkg is not None:
143+ self.core_item = UpdateItem(pkg, name, icon)
144+ self._items.add(self.core_item)
145+
146+ @property
147+ def items(self):
148+ all_items = []
149+ all_items.extend(self._items)
150+ return sorted(all_items, key=lambda a: a.name.lower())
151+
152+ def add(self, pkg):
153+ name = utils.get_package_label(pkg)
154+ icon = Gio.ThemedIcon.new("package")
155+ self._items.add(UpdateItem(pkg, name, icon))
156+
157+ def contains(self, item):
158+ return item in self._items
159+
160+ def _is_dependency_helper(self, cache, pkg, dep, seen=set()):
161+ if pkg is None or pkg.candidate is None or pkg in seen:
162+ return False
163+ elif pkg.name == dep.name:
164+ return True
165+ seen.add(pkg)
166+ candidate = pkg.candidate
167+ dependencies = candidate.get_dependencies('Depends', 'Recommends')
168+ for dependency_pkg in itertools.chain.from_iterable(dependencies):
169+ if dependency_pkg.name in cache and \
170+ self._is_dependency_helper(cache, cache[dependency_pkg.name],
171+ dep, seen):
172+ return True
173+ return False
174+
175+ def is_dependency(self, cache, maybe_dep):
176+ # This is a recursive dependency check. TODO: We do this many times
177+ # when grouping packages, and it could be made more efficient.
178+ seen = set()
179+ for item in self._items:
180+ if self._is_dependency_helper(cache, item.pkg, maybe_dep,
181+ seen=seen):
182+ return True
183+ return False
184+
185+ def packages_are_selected(self):
186+ for item in self.items:
187+ if item.pkg.marked_install or item.pkg.marked_upgrade:
188+ return True
189+ return False
190+
191+ def selection_is_inconsistent(self):
192+ pkgs_installing = [item for item in self.items
193+ if item.pkg.marked_install or item.pkg.marked_upgrade]
194+ return (len(pkgs_installing) > 0 and
195+ len(pkgs_installing) < len(self.items))
196+
197+ def get_total_size(self):
198+ size = 0
199+ for item in self.items:
200+ size += getattr(item.pkg.candidate, "size", 0)
201+ return size
202+
203+
204+class UpdateApplicationGroup(UpdateGroup):
205+ def __init__(self, pkg, application):
206+ name = application.get_display_name()
207+ icon = application.get_icon()
208+ super(UpdateApplicationGroup, self).__init__(pkg, name, icon)
209+
210+
211+class UpdatePackageGroup(UpdateGroup):
212+ def __init__(self, pkg):
213+ name = utils.get_package_label(pkg)
214+ icon = Gio.ThemedIcon.new("package")
215+ super(UpdatePackageGroup, self).__init__(pkg, name, icon)
216+
217+
218+class UpdateSystemGroup(UpdateGroup):
219+ def __init__(self, cache):
220+ # Translators: the %s is a distro name, like 'Ubuntu' and 'base' as in
221+ # the core components and packages.
222+ name = _("%s base") % utils.get_ubuntu_flavor_name(cache=cache)
223+ icon = Gio.ThemedIcon.new("distributor-logo")
224+ super(UpdateSystemGroup, self).__init__(None, name, icon)
225+
226+
227+class UpdateOrigin():
228 def __init__(self, desc, importance):
229 self.packages = []
230 self.importance = importance
231 self.description = desc
232
233
234-class OriginsImportance:
235- # filled in by us
236- SECURITY = 10
237- UPDATES = 9
238- PROPOSED = 8
239- BACKPORTS = 7
240- ARCHIVE = 6
241- # this is filled in by MyCache
242- OTHER = 0
243- # this is used by us
244- OTHER_UNKNOWN = -1
245-
246-
247-class UpdateList(object):
248+class UpdateList():
249 """
250 class that contains the list of available updates in
251 self.pkgs[origin] where origin is the user readable string
252@@ -64,6 +161,8 @@
253 # the file that contains the uniq machine id
254 UNIQ_MACHINE_ID_FILE = "/var/lib/dbus/machine-id"
255
256+ APP_INSTALL_PATH = "/usr/share/app-install/desktop"
257+
258 # the configuration key to turn phased-updates always on
259 ALWAYS_INCLUDE_PHASED_UPDATES = (
260 "Update-Manager::Always-Include-Phased-Updates")
261@@ -71,47 +170,111 @@
262 NEVER_INCLUDE_PHASED_UPDATES = (
263 "Update-Manager::Never-Include-Phased-Updates")
264
265- def __init__(self, parent):
266- # a map of packages under their origin
267- try:
268- dist = subprocess.check_output(
269- ["lsb_release", "-c", "-s"], universal_newlines=True).strip()
270- except subprocess.CalledProcessError as e:
271- print("Error in lsb_release: %s" % e)
272- dist = None
273+ def __init__(self, parent, dist=None):
274+ self.dist = dist if dist else platform.dist()[2]
275 self.distUpgradeWouldDelete = 0
276- self.pkgs = {}
277+ self.update_groups = []
278+ self.security_groups = []
279 self.num_updates = 0
280- self.matcher = self.initMatcher(dist)
281 self.random = random.Random()
282 # a stable machine uniq id
283 with open(self.UNIQ_MACHINE_ID_FILE) as f:
284 self.machine_uniq_id = f.read()
285
286- def initMatcher(self, dist):
287- # (origin, archive, description, importance)
288- matcher_templates = [
289- (None, None, _("Other updates"), OriginsImportance.OTHER_UNKNOWN)
290- ]
291- if dist:
292- matcher_templates += [
293- ("%s-security" % dist, "Ubuntu",
294- _("Important security updates"), OriginsImportance.SECURITY),
295- ("%s-updates" % dist, "Ubuntu",
296- _("Recommended updates"), OriginsImportance.UPDATES),
297- ("%s-proposed" % dist, "Ubuntu",
298- _("Proposed updates"), OriginsImportance.PROPOSED),
299- ("%s-backports" % dist, "Ubuntu",
300- _("Backports"), OriginsImportance.BACKPORTS),
301- (dist, "Ubuntu",
302- _("Distribution updates"), OriginsImportance.ARCHIVE),
303- ]
304- matcher = {}
305- for (origin, archive, desc, importance) in matcher_templates:
306- matcher[(origin, archive)] = UpdateOrigin(desc, importance)
307- return matcher
308-
309- def is_ignored_phased_update(self, pkg):
310+ if 'XDG_DATA_DIRS' in os.environ and os.environ['XDG_DATA_DIRS']:
311+ data_dirs = os.environ['XDG_DATA_DIRS']
312+ else:
313+ data_dirs= '/usr/local/share/:/usr/share/'
314+ self.application_dirs = [os.path.join(base, 'applications')
315+ for base in data_dirs.split(':')]
316+
317+ if 'XDG_CURRENT_DESKTOP' in os.environ:
318+ self.current_desktop = os.environ.get('XDG_CURRENT_DESKTOP')
319+ else:
320+ self.current_desktop = ''
321+
322+ def _file_is_application(self, file_path):
323+ file_path = os.path.abspath(file_path)
324+ is_application = False
325+ for app_dir in self.application_dirs:
326+ is_application = is_application or file_path.startswith(app_dir)
327+ extension = os.path.splitext(file_path)[1]
328+ is_application = is_application and (extension == '.desktop')
329+ return is_application
330+
331+ def _rate_application_for_package(self, application, pkg):
332+ score = 0
333+ desktop_file = os.path.basename(application.get_filename())
334+ application_id = os.path.splitext(desktop_file)[0]
335+
336+ if application.should_show():
337+ score += 1
338+
339+ if application_id == pkg.name:
340+ score += 5
341+
342+ return score
343+
344+ def _get_application_for_package(self, pkg):
345+ desktop_files = []
346+ rated_applications = []
347+
348+ for installed_file in pkg.installed_files:
349+ if self._file_is_application(installed_file):
350+ desktop_files.append(installed_file)
351+
352+ app_install_pattern = os.path.join(self.APP_INSTALL_PATH,
353+ '%s:*' % pkg.name)
354+ for desktop_file in glob.glob(app_install_pattern):
355+ desktop_files.append(desktop_file)
356+
357+ for desktop_file in desktop_files:
358+ try:
359+ application = Gio.DesktopAppInfo.new_from_filename(
360+ desktop_file)
361+ application.set_desktop_env(self.current_desktop)
362+ except Exception as e:
363+ print("Error loading .desktop file %s: %s" %
364+ (installed_file, e))
365+ continue
366+ score = self._rate_application_for_package(application, pkg)
367+ if score > 0:
368+ rated_applications.append((score, application))
369+
370+ rated_applications.sort(key=lambda app: app[0], reverse=True)
371+ if len(rated_applications) > 0:
372+ return rated_applications[0][1]
373+ else:
374+ return None
375+
376+ def _is_security_update(self, pkg):
377+ """ This will test if the pkg is a security update.
378+ This includes if there is a newer version in -updates, but also
379+ an older update available in -security. For example, if
380+ installed pkg A v1.0 is available in both -updates (as v1.2) and
381+ -security (v1.1). we want to display it as a security update.
382+
383+ :return: True if the update comes from the security pocket
384+ """
385+ if not self.dist:
386+ return False
387+ inst_ver = pkg._pkg.current_ver
388+ for ver in pkg._pkg.version_list:
389+ # discard is < than installed ver
390+ if (inst_ver and
391+ apt.apt_pkg.version_compare(ver.ver_str,
392+ inst_ver.ver_str) <= 0):
393+ continue
394+ # check if we have a match
395+ for (verFileIter, index) in ver.file_list:
396+ if verFileIter.archive == "%s-security" % self.dist and \
397+ verFileIter.origin == "Ubuntu":
398+ indexfile = pkg._pcache._list.find_index(verFileIter)
399+ if indexfile: # and indexfile.IsTrusted:
400+ return True
401+ return False
402+
403+ def _is_ignored_phased_update(self, pkg):
404 """ This will test if the pkg is a phased update and if
405 it needs to get installed or ignored.
406
407@@ -142,15 +305,88 @@
408 return True
409 return False
410
411+ def _get_linux_packages(self):
412+ "Return all binary packages made by the linux-meta source package"
413+ # Hard code this rather than generate from source info in cache because
414+ # that might only be available if we have deb-src lines. I think we
415+ # could also generate it by iterating over all the binary package info
416+ # we have, but that is costly. These don't change often.
417+ return ['linux', 'linux-image', 'linux-headers-generic',
418+ 'linux-image-generic', 'linux-generic',
419+ 'linux-headers-generic-pae', 'linux-image-generic-pae',
420+ 'linux-generic-pae', 'linux-headers-omap', 'linux-image-omap',
421+ 'linux-omap', 'linux-headers-server', 'linux-image-server',
422+ 'linux-server', 'linux-signed-image-generic',
423+ 'linux-signed-generic', 'linux-headers-virtual',
424+ 'linux-image-virtual', 'linux-virtual',
425+ 'linux-image-extra-virtual']
426+
427+ def _make_groups(self, cache, pkgs):
428+ pkgs_by_source = {}
429+ ungrouped_pkgs = []
430+ app_groups = []
431+ pkg_groups = []
432+
433+ # Index packages by source package name
434+ for pkg in pkgs:
435+ srcpkg = pkg.candidate.source_name
436+ pkgs_by_source.setdefault(srcpkg, []).append(pkg)
437+
438+ for srcpkg, pkgs in pkgs_by_source.items():
439+ for pkg in pkgs:
440+ app = self._get_application_for_package(pkg)
441+ if app is not None:
442+ app_group = UpdateApplicationGroup(pkg, app)
443+ app_groups.append(app_group)
444+ else:
445+ ungrouped_pkgs.append(pkg)
446+
447+ # Stick together applications and their immediate dependencies
448+ for pkg in list(ungrouped_pkgs):
449+ dep_groups = []
450+ for group in app_groups:
451+ if group.is_dependency(cache, pkg):
452+ dep_groups.append(group)
453+ if len(dep_groups) > 1:
454+ break
455+ if len(dep_groups) == 1:
456+ dep_groups[0].add(pkg)
457+ ungrouped_pkgs.remove(pkg)
458+
459+ # Separate out system base packages
460+ system_group = None
461+ meta_group = UpdateGroup(None, None, None)
462+ flavor_package = utils.get_ubuntu_flavor_package(cache=cache)
463+ meta_pkgs = [flavor_package, "ubuntu-standard", "ubuntu-minimal"]
464+ meta_pkgs.extend(self._get_linux_packages())
465+ for pkg in meta_pkgs:
466+ if pkg in cache:
467+ meta_group.add(cache[pkg])
468+ for pkg in ungrouped_pkgs:
469+ if meta_group.contains(pkg) or meta_group.is_dependency(cache, pkg):
470+ if system_group is None:
471+ system_group = UpdateSystemGroup(cache)
472+ system_group.add(pkg)
473+ else:
474+ pkg_groups.append(UpdatePackageGroup(pkg))
475+
476+ app_groups.sort(key=lambda a: a.name.lower())
477+ pkg_groups.sort(key=lambda a: a.name.lower())
478+ if system_group:
479+ pkg_groups.append(system_group)
480+
481+ return app_groups + pkg_groups
482+
483 def update(self, cache):
484 self.held_back = []
485
486 # do the upgrade
487 self.distUpgradeWouldDelete = cache.saveDistUpgrade()
488
489- #dselect_upgrade_origin = UpdateOrigin(_("Previous selected"), 1)
490+ security_pkgs = []
491+ upgrade_pkgs = []
492
493- # sort by origin
494+ # Find all upgradable packages
495 for pkg in cache:
496 if pkg.is_upgradable or pkg.marked_install:
497 if getattr(pkg.candidate, "origins", None) is None:
498@@ -159,22 +395,22 @@
499 print("WARNING: upgradable but no candidate.origins?!?: ",
500 pkg.name)
501 continue
502- # check where the package belongs
503- origin_node = cache.match_package_origin(pkg, self.matcher)
504
505 # see if its a phased update and *not* a security update
506- # or shadowing a security update
507- if (origin_node.importance != OriginsImportance.SECURITY and
508- self.is_ignored_phased_update(pkg)):
509+ is_security_update = self._is_security_update(pkg)
510+ if (not is_security_update and
511+ self._is_ignored_phased_update(pkg)):
512 continue
513
514- if origin_node not in self.pkgs:
515- self.pkgs[origin_node] = []
516- self.pkgs[origin_node].append(pkg)
517+ if is_security_update:
518+ security_pkgs.append(pkg)
519+ else:
520+ upgrade_pkgs.append(pkg)
521 self.num_updates = self.num_updates + 1
522+
523 if pkg.is_upgradable and not (pkg.marked_upgrade or
524 pkg.marked_install):
525 self.held_back.append(pkg.name)
526- for l in self.pkgs.keys():
527- self.pkgs[l].sort(key=operator.attrgetter("name"))
528- self.keepcount = cache._depcache.keep_count
529+
530+ self.update_groups = self._make_groups(cache, upgrade_pkgs)
531+ self.security_groups = self._make_groups(cache, security_pkgs)
532
533=== modified file 'UpdateManager/Core/utils.py'
534--- UpdateManager/Core/utils.py 2012-12-14 23:03:19 +0000
535+++ UpdateManager/Core/utils.py 2013-01-23 15:48:20 +0000
536@@ -1,9 +1,10 @@
537 # utils.py
538 # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
539 #
540-# Copyright (c) 2004-2008 Canonical
541+# Copyright (c) 2004-2013 Canonical
542 #
543-# Author: Michael Vogt <mvo@debian.org>
544+# Authors: Michael Vogt <mvo@debian.org>
545+# Michael Terry <michael.terry@canonical.com>
546 #
547 # This program is free software; you can redistribute it and/or
548 # modify it under the terms of the GNU General Public License as
549@@ -27,6 +28,7 @@
550 from stat import (S_IMODE, ST_MODE, S_IXUSR)
551 from math import ceil
552
553+import apt
554 import apt_pkg
555 apt_pkg.init_config()
556
557@@ -58,6 +60,7 @@
558 from urlparse import urlsplit
559
560 from copy import copy
561+from gi.repository import GLib
562
563
564 class ExecutionTime(object):
565@@ -402,36 +405,56 @@
566 return None
567
568
569-def get_ubuntu_flavor():
570+def get_ubuntu_flavor(cache=None):
571 """ try to guess the flavor based on the running desktop """
572 # this will (of course) not work in a server environment,
573 # but the main use case for this is to show the right
574- # release notes
575- # TODO: actually examine which meta packages are installed, like
576- # DistUpgrade/DistUpgradeCache.py does and use that to choose a flavor.
577- denv = os.environ.get("DESKTOP_SESSION", "")
578- if "gnome" in denv:
579- return "ubuntu"
580- elif "kde" in denv:
581- return "kubuntu"
582- elif "xfce" in denv or "xubuntu" in denv:
583- return "xubuntu"
584- elif "LXDE" in denv or "Lubuntu" in denv:
585- return "lubuntu"
586- # default to ubuntu if nothing more specific is found
587- return "ubuntu"
588-
589-
590-def get_ubuntu_flavor_name():
591- flavor = get_ubuntu_flavor()
592- if flavor == "kubuntu":
593- return "Kubuntu"
594- elif flavor == "xubuntu":
595- return "Xubuntu"
596- elif flavor == "lubuntu":
597- return "Lubuntu"
598+ # release notes.
599+ pkg = get_ubuntu_flavor_package(cache=cache)
600+ return pkg.split('-', 1)[0]
601+
602+
603+def _load_meta_pkg_list():
604+ # This could potentially introduce a circular dependency, but the config
605+ # parser logic is simple, and doesn't rely on any UpdateManager code.
606+ from DistUpgrade.DistUpgradeConfigParser import DistUpgradeConfig
607+ parser = DistUpgradeConfig('/usr/share/ubuntu-release-upgrader')
608+ return parser.getlist('Distro', 'MetaPkgs')
609+
610+
611+def get_ubuntu_flavor_package(cache=None):
612+ """ try to guess the flavor metapackage based on the running desktop """
613+ # From spec, first if ubuntu-desktop is installed, use that.
614+ # Second, grab first installed one from DistUpgrade.cfg.
615+ # Lastly, fallback to ubuntu-desktop again.
616+ meta_pkgs = ['ubuntu-desktop']
617+
618+ try:
619+ meta_pkgs.extend(sorted(_load_meta_pkg_list()))
620+ except Exception as e:
621+ print('Could not load list of meta packages:', e)
622+
623+ if cache is None:
624+ cache = apt.Cache()
625+ for meta_pkg in meta_pkgs:
626+ cache_pkg = cache[meta_pkg] if meta_pkg in cache else None
627+ if cache_pkg and cache_pkg.is_installed:
628+ return meta_pkg
629+ return 'ubuntu-desktop'
630+
631+
632+def get_ubuntu_flavor_name(cache=None):
633+ """ try to guess the flavor name based on the running desktop """
634+ pkg = get_ubuntu_flavor_package(cache=cache)
635+ lookup = {'ubuntustudio-desktop': 'Ubuntu Studio'}
636+ if pkg in lookup:
637+ return lookup[pkg]
638+ elif pkg.endswith('-desktop'):
639+ return capitalize_first_word(pkg.rsplit('-desktop', 1)[0])
640+ elif pkg.endswith('-netbook'):
641+ return capitalize_first_word(pkg.rsplit('-netbook', 1)[0])
642 else:
643- return "Ubuntu"
644+ return 'Ubuntu'
645
646
647 # Unused by update-manager, but still used by ubuntu-release-upgrader
648@@ -523,6 +546,22 @@
649 return True
650
651
652+def capitalize_first_word(string):
653+ """ this uppercases the first word's first letter
654+ """
655+ if len(string) > 1 and string[0].isalpha() and not string[0].isupper():
656+ return string[0].capitalize() + string[1:]
657+ return string
658+
659+
660+def get_package_label(pkg):
661+ """ this takes a package synopsis and uppercases the first word's
662+ first letter
663+ """
664+ name = GLib.markup_escape_text(getattr(pkg.candidate, "summary", ""))
665+ return capitalize_first_word(name)
666+
667+
668 if __name__ == "__main__":
669 #print(mirror_from_sources_list())
670 #print(on_battery())
671
672=== modified file 'UpdateManager/UpdatesAvailable.py'
673--- UpdateManager/UpdatesAvailable.py 2012-12-13 20:39:37 +0000
674+++ UpdateManager/UpdatesAvailable.py 2013-01-23 15:48:20 +0000
675@@ -1,7 +1,7 @@
676 # UpdatesAvailable.py
677 # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
678 #
679-# Copyright (c) 2004-2012 Canonical
680+# Copyright (c) 2004-2013 Canonical
681 # 2004 Michiel Sikkes
682 # 2005 Martin Willemoes Hansen
683 # 2010 Mohamed Amine IL Idrissi
684@@ -12,6 +12,7 @@
685 # Mohamed Amine IL Idrissi <ilidrissiamine@gmail.com>
686 # Alex Launi <alex.launi@canonical.com>
687 # Michael Terry <michael.terry@canonical.com>
688+# Dylan McCall <dylanmccall@ubuntu.com>
689 #
690 # This program is free software; you can redistribute it and/or
691 # modify it under the terms of the GNU General Public License as
692@@ -30,6 +31,7 @@
693
694 from __future__ import absolute_import, print_function
695
696+from gi.repository import GLib
697 from gi.repository import Gtk
698 from gi.repository import Gdk
699 from gi.repository import GObject
700@@ -46,18 +48,17 @@
701 import os
702 import re
703 import logging
704-import operator
705 import subprocess
706 import time
707 import threading
708-import xml.sax.saxutils
709
710 from gettext import gettext as _
711 from gettext import ngettext
712
713
714-from .Core.utils import humanize_size
715+from .Core.utils import (get_package_label, humanize_size)
716 from .Core.AlertWatcher import AlertWatcher
717+from .Core.UpdateList import UpdateSystemGroup
718
719 from DistUpgrade.DistUpgradeCache import NotEnoughFreeSpaceError
720
721@@ -70,15 +71,126 @@
722
723 # FIXME:
724 # - kill "all_changes" and move the changes into the "Update" class
725+# - screen reader does not read update toggle state
726+# - screen reader does not say "Downloaded" for downloaded updates
727
728 # list constants
729-(LIST_CONTENTS, LIST_NAME, LIST_PKG,
730- LIST_ORIGIN, LIST_TOGGLE_CHECKED) = range(5)
731+(LIST_NAME, LIST_UPDATE_DATA, LIST_SIZE, LIST_TOGGLE_ACTIVE) = range(4)
732
733 # NetworkManager enums
734 from .Core.roam import NetworkManagerHelper
735
736
737+class UpdateData():
738+ def __init__(self, groups, group, item):
739+ self.groups = groups if groups else []
740+ self.group = group
741+ self.item = item
742+
743+
744+class CellAreaPackage(Gtk.CellAreaBox):
745+ """This CellArea lays our package cells side by side, without allocating
746+ width for a cell if it isn't present (like icons for header labels).
747+ """
748+
749+ def __init__(self, indent_toplevel=False):
750+ Gtk.CellAreaBox.__init__(self)
751+ self.indent_toplevel = indent_toplevel
752+ self.column = None
753+ self.toggle_size = None
754+ self.pixbuf_size = None
755+ self.cached_cell_start = {}
756+
757+ def do_foreach_alloc(self, context, widget, cell_area_in, bg_area_in,
758+ callback):
759+ # First, gather cell renderers and make sure they are what we expect
760+ cells = []
761+ def gather(cell, data):
762+ cells.append(cell)
763+ self.foreach(gather, None)
764+ if (len(cells) != 3 or
765+ not isinstance(cells[0], Gtk.CellRendererToggle) or
766+ not isinstance(cells[1], Gtk.CellRendererPixbuf) or
767+ not isinstance(cells[2], Gtk.CellRendererText)):
768+ return
769+ toggle = cells[0]
770+ pixbuf = cells[1]
771+ text = cells[2]
772+
773+ # Now just grab some size info
774+ cell_area = cell_area_in.copy()
775+ bg_area = bg_area_in.copy()
776+ spacing = self.get_property("spacing")
777+ gicon = pixbuf.get_property("gicon")
778+ cell_start = self.get_cell_start(widget)
779+ orig_end = cell_area.width + cell_area.x
780+ if self.toggle_size is None:
781+ toggle_min, self.toggle_size = toggle.get_preferred_width(widget)
782+ if gicon and self.pixbuf_size is None:
783+ pixbuf_min, self.pixbuf_size = pixbuf.get_preferred_width(widget)
784+
785+ # And finally, start handling each cell
786+
787+ cur_path = self.get_current_path_string()
788+ depth = Gtk.TreePath.new_from_string(cur_path).get_depth()
789+ if gicon is not None and self.indent_toplevel:
790+ # if not a header, align with header rows
791+ depth = depth + 1
792+ if depth == 1:
793+ cell_area.x = cell_start
794+ elif depth == 2:
795+ cell_area.x = cell_start + self.toggle_size + spacing
796+ elif depth == 3:
797+ # Oddly, cells don't line up if we simply use spacing * 2
798+ cell_area.x = cell_start + self.toggle_size * 2 + spacing + 1
799+ cell_area.width = self.toggle_size
800+ if callback(cells[0], cell_area.copy(), bg_area.copy()):
801+ return
802+
803+ cell_area.x = cell_area.x + cell_area.width + spacing
804+ if gicon is None:
805+ cell_area.width = 0
806+ else:
807+ cell_area.width = self.pixbuf_size
808+ if callback(cells[1], cell_area.copy(), bg_area.copy()):
809+ return
810+
811+ if gicon is not None:
812+ cell_area.x = cell_area.x + cell_area.width + spacing
813+ cell_area.width = orig_end - cell_area.x
814+ if callback(cells[2], cell_area.copy(), bg_area.copy()):
815+ return
816+
817+ def do_event(self, context, widget, event, cell_area, flags):
818+ # This override is just to trick our parent implementation into
819+ # allowing clicks on toggle cells when they are where the expanders
820+ # usually are. It doesn't expect that, so we expand the cell_area
821+ # here to be equivalent to bg_area.
822+ cell_start = self.get_cell_start(widget)
823+ cell_area.width = cell_area.width + cell_area.x - cell_start
824+ cell_area.x = cell_start
825+ return Gtk.CellAreaBox.do_event(self, context, widget, event,
826+ cell_area, flags)
827+
828+ def get_cell_start(self, widget):
829+ if not self.column:
830+ return 0
831+ else:
832+ val = GObject.Value()
833+ val.init(int)
834+ widget.style_get_property("horizontal-separator", val)
835+ h_sep = val.get_int()
836+ widget.style_get_property("grid-line-width", val)
837+ line_width = val.get_int()
838+ cell_start = self.column.get_x_offset() - h_sep - line_width
839+ if not self.indent_toplevel: # i.e. if no headers
840+ widget.style_get_property("expander-size", val)
841+ spacing = self.get_property("spacing")
842+ # Hardcode 4 because GTK+ hardcodes 4 internally
843+ cell_start = cell_start + val.get_int() + 4 + spacing
844+ return cell_start
845+
846+
847 class UpdatesAvailable(SimpleGtkbuilderApp):
848
849 def __init__(self, app, header=None, desc=None):
850@@ -134,38 +246,71 @@
851 self.update_close_button()
852
853 # the treeview (move into it's own code!)
854- self.store = Gtk.ListStore(str, str, GObject.TYPE_PYOBJECT,
855- GObject.TYPE_PYOBJECT, bool)
856+ self.store = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT, str, bool)
857 self.treeview_update.set_model(self.store)
858- self.treeview_update.set_headers_clickable(True)
859+
860+ restart_icon_renderer = Gtk.CellRendererPixbuf()
861+ restart_icon_renderer.set_property("xpad", 4)
862+ restart_icon_renderer.set_property("ypad", 2)
863+ restart_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
864+ restart_icon_renderer.set_property("follow-state", True)
865+ restart_column = Gtk.TreeViewColumn(None, restart_icon_renderer)
866+ restart_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
867+ restart_column.set_fixed_width(20)
868+ self.treeview_update.append_column(restart_column)
869+ restart_column.set_cell_data_func(restart_icon_renderer,
870+ self.restart_icon_renderer_data_func)
871+
872+ pkg_area = CellAreaPackage(bool(self.list.security_groups))
873+ pkg_column = Gtk.TreeViewColumn.new_with_area(pkg_area)
874+ pkg_area.column = pkg_column
875+ pkg_column.set_title(_("Install"))
876+ pkg_column.set_property("spacing", 4)
877+ pkg_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
878+ pkg_column.set_expand(True)
879+ self.treeview_update.append_column(pkg_column)
880+
881+ pkg_toggle_renderer = Gtk.CellRendererToggle()
882+ pkg_toggle_renderer.set_property("ypad", 2)
883+ pkg_toggle_renderer.connect("toggled", self.on_update_toggled)
884+ pkg_column.pack_start(pkg_toggle_renderer, False)
885+ pkg_column.add_attribute(pkg_toggle_renderer,
886+ 'active', LIST_TOGGLE_ACTIVE)
887+ pkg_column.set_cell_data_func(pkg_toggle_renderer,
888+ self.pkg_toggle_renderer_data_func)
889+
890+ pkg_icon_renderer = Gtk.CellRendererPixbuf()
891+ pkg_icon_renderer.set_property("ypad", 2)
892+ pkg_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
893+ pkg_column.pack_start(pkg_icon_renderer, False)
894+ pkg_column.set_cell_data_func(pkg_icon_renderer,
895+ self.pkg_icon_renderer_data_func)
896+
897+ pkg_label_renderer = Gtk.CellRendererText()
898+ pkg_label_renderer.set_property("ypad", 2)
899+ pkg_column.pack_start(pkg_label_renderer, True)
900+ pkg_column.set_cell_data_func(pkg_label_renderer,
901+ self.pkg_label_renderer_data_func)
902+
903+ size_renderer = Gtk.CellRendererText()
904+ size_renderer.set_property("xpad", 6)
905+ size_renderer.set_property("ypad", 0)
906+ size_renderer.set_property("xalign", 1)
907+ # 1.0/1.2 == PANGO.Scale.SMALL. Constant is not (yet) introspected.
908+ size_renderer.set_property("scale", 1.0 / 1.2)
909+ size_column = Gtk.TreeViewColumn(_("Download"), size_renderer,
910+ text=LIST_SIZE)
911+ size_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
912+ self.treeview_update.append_column(size_column)
913+
914+ self.treeview_update.set_headers_visible(True)
915+ self.treeview_update.set_headers_clickable(False)
916 self.treeview_update.set_direction(Gtk.TextDirection.LTR)
917-
918- tr = Gtk.CellRendererText()
919- tr.set_property("xpad", 6)
920- tr.set_property("ypad", 6)
921- cr = Gtk.CellRendererToggle()
922- cr.set_property("activatable", True)
923- cr.set_property("xpad", 6)
924- cr.connect("toggled", self.toggled)
925-
926- column_install = Gtk.TreeViewColumn(_("Install"), cr,
927- active=LIST_TOGGLE_CHECKED)
928- column_install.set_cell_data_func(cr, self.install_column_view_func)
929- column = Gtk.TreeViewColumn(_("Name"), tr, markup=LIST_CONTENTS)
930- column.set_resizable(True)
931-
932- column_install.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
933- column_install.set_fixed_width(30)
934- column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
935- column.set_fixed_width(100)
936 self.treeview_update.set_fixed_height_mode(False)
937-
938- self.treeview_update.append_column(column_install)
939- column_install.set_visible(True)
940- self.treeview_update.append_column(column)
941+ self.treeview_update.set_expander_column(pkg_column)
942 self.treeview_update.set_search_column(LIST_NAME)
943 self.treeview_update.connect("button-press-event",
944- self.show_context_menu)
945+ self.on_treeview_button_press)
946
947 # setup the help viewer and disable the help button if there
948 # is no viewer available
949@@ -210,26 +355,97 @@
950 self.button_close.set_use_underline(False)
951
952 def install_all_updates(self, menu, menuitem, data):
953- self.select_all_updgrades(None)
954+ self.select_all_upgrades(None)
955 self.on_button_install_clicked(None)
956
957- def install_column_view_func(self, cell_layout, renderer, model, iter,
958- data):
959- pkg = model.get_value(iter, LIST_PKG)
960- if pkg is None:
961- renderer.set_property("activatable", True)
962- return
963- current_state = renderer.get_property("active")
964- to_install = pkg.marked_install or pkg.marked_upgrade
965- renderer.set_property("active", to_install)
966- # we need to update the store as well to ensure orca knowns
967- # about state changes (it will not read view_func changes)
968- if to_install != current_state:
969- self.store[iter][LIST_TOGGLE_CHECKED] = to_install
970- if pkg.name in self.list.held_back:
971- renderer.set_property("activatable", False)
972+ def restart_icon_renderer_data_func(self, cell_layout, renderer, model,
973+ iter, data):
974+ def pkg_requires_restart(pkg):
975+ restart_condition = pkg.candidate.record.get('XB-Restart-Required')
976+ return restart_condition == 'system'
977+
978+ data = model.get_value(iter, LIST_UPDATE_DATA)
979+ path = model.get_path(iter)
980+
981+ requires_restart = False
982+ if data.item:
983+ requires_restart = pkg_requires_restart(data.item.pkg)
984+ elif data.group:
985+ if not self.treeview_update.row_expanded(path):
986+ # A package in the group requires restart
987+ for group_item in data.group.items:
988+ if pkg_requires_restart(group_item.pkg):
989+ requires_restart = True
990+ break
991+
992+ # FIXME: Non-standard, incorrect icon name (from app category).
993+ # Theme support for what we want seems to be lacking.
994+ if requires_restart:
995+ restart_icon_names = ['view-refresh-symbolic',
996+ 'system-restart',
997+ 'system-reboot']
998+ gicon = Gio.ThemedIcon.new_from_names(restart_icon_names)
999 else:
1000- renderer.set_property("activatable", True)
1001+ gicon = None
1002+ renderer.set_property("gicon", gicon)
1003+
1004+ def pkg_toggle_renderer_data_func(self, cell_layout, renderer, model, iter,
1005+ data):
1006+ data = model.get_value(iter, LIST_UPDATE_DATA)
1007+
1008+ activatable = False
1009+ inconsistent = False
1010+ if data.item:
1011+ activatable = data.item.name not in self.list.held_back
1012+ inconsistent = False
1013+ elif data.group:
1014+ activatable = True
1015+ inconsistent = data.group.selection_is_inconsistent()
1016+ elif data.groups:
1017+ activatable = True
1018+ inconsistent = False
1019+ saw_install = None
1020+ for group in data.groups:
1021+ for item in group.items:
1022+ pkg = item.pkg
1023+ this_install = pkg.marked_install or pkg.marked_upgrade
1024+ if saw_install is not None and saw_install != this_install:
1025+ inconsistent = True
1026+ break
1027+ saw_install = this_install
1028+ if inconsistent:
1029+ break
1030+
1031+ # The "active" attribute is already set via LIST_TOGGLE_ACTIVE in the
1032+ # tree model, so we don't set it here.
1033+ renderer.set_property("activatable", activatable)
1034+ renderer.set_property("inconsistent", inconsistent)
1035+
1036+ def pkg_icon_renderer_data_func(self, cell_layout, renderer, model, iter,
1037+ data):
1038+ data = model.get_value(iter, LIST_UPDATE_DATA)
1039+
1040+ gicon = None
1041+ if data.group:
1042+ gicon = data.group.icon
1043+ elif data.item:
1044+ gicon = data.item.icon
1045+
1046+ renderer.set_property("gicon", gicon)
1047+
1048+ def pkg_label_renderer_data_func(self, cell_layout, renderer, model, iter,
1049+ data):
1050+ data = model.get_value(iter, LIST_UPDATE_DATA)
1051+ name = model.get_value(iter, LIST_NAME)
1052+
1053+ if data.group:
1054+ markup = name
1055+ elif data.item:
1056+ markup = name
1057+ else: # header
1058+ markup = "<b>%s</b>" % name
1059+
1060+ renderer.set_property("markup", markup)
1061
1062 def set_changes_buffer(self, changes_buffer, text, name, srcpkg):
1063 changes_buffer.set_text("")
1064@@ -264,16 +480,21 @@
1065 iter = model.get_iter(path)
1066
1067 # set descr
1068- pkg = model.get_value(iter, LIST_PKG)
1069- if (pkg is None or pkg.candidate is None or
1070- pkg.candidate.description is None):
1071+ data = model.get_value(iter, LIST_UPDATE_DATA)
1072+ item = data.item
1073+ if (item is None and data.group is not None and
1074+ data.group.core_item is not None):
1075+ item = data.group.core_item
1076+ if (item is None or item.pkg is None or
1077+ item.pkg.candidate is None or
1078+ item.pkg.candidate.description is None):
1079 changes_buffer = self.textview_changes.get_buffer()
1080 changes_buffer.set_text("")
1081 desc_buffer = self.textview_descr.get_buffer()
1082 desc_buffer.set_text("")
1083 self.notebook_details.set_sensitive(False)
1084 return
1085- long_desc = pkg.candidate.description
1086+ long_desc = item.pkg.candidate.description
1087 self.notebook_details.set_sensitive(True)
1088 # do some regular expression magic on the description
1089 # Add a newline before each bullet
1090@@ -290,7 +511,7 @@
1091 desc_buffer.set_text(long_desc)
1092
1093 # now do the changelog
1094- name = model.get_value(iter, LIST_NAME)
1095+ name = item.pkg.name
1096 if name is None:
1097 return
1098
1099@@ -334,11 +555,10 @@
1100 button.disconnect(id)
1101 # check if we still are in the right pkg (the download may have taken
1102 # some time and the user may have clicked on a new pkg)
1103- path = widget.get_cursor()[0]
1104- if path is None:
1105+ now_path = widget.get_cursor()[0]
1106+ if now_path is None:
1107 return
1108- now_name = widget.get_model()[path][LIST_NAME]
1109- if name != now_name:
1110+ if path != now_path:
1111 return
1112 # display NEWS.Debian first, then the changelog
1113 changes = ""
1114@@ -350,7 +570,7 @@
1115 if changes:
1116 self.set_changes_buffer(changes_buffer, changes, name, srcpkg)
1117
1118- def show_context_menu(self, widget, event):
1119+ def on_treeview_button_press(self, widget, event):
1120 """
1121 Show a context menu if a right click was performed on an update entry
1122 """
1123@@ -361,10 +581,13 @@
1124 self.menu = menu = Gtk.Menu()
1125 item_select_none = \
1126 Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))
1127- item_select_none.connect("activate", self.select_none_updgrades)
1128+ item_select_none.connect("activate", self.select_none_upgrades)
1129 menu.append(item_select_none)
1130+ num_updates = self.cache.install_count
1131+ if num_updates == 0:
1132+ item_select_none.set_property("sensitive", False)
1133 item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))
1134- item_select_all.connect("activate", self.select_all_updgrades)
1135+ item_select_all.connect("activate", self.select_all_upgrades)
1136 menu.append(item_select_all)
1137 menu.show_all()
1138 menu.popup_for_device(
1139@@ -373,35 +596,36 @@
1140 return True
1141
1142 # we need this for select all/unselect all
1143- def _toggle_origin_headers(self, new_selection_value):
1144- """ small helper that will set/unset the origin headers
1145+ def _toggle_group_headers(self, new_selection_value):
1146+ """ small helper that will set/unset the group headers
1147 """
1148 model = self.treeview_update.get_model()
1149 for row in model:
1150- if not model.get_value(row.iter, LIST_PKG):
1151- model.set_value(row.iter, LIST_TOGGLE_CHECKED,
1152+ data = model.get_value(row.iter, LIST_UPDATE_DATA)
1153+ if data.groups is not None or data.group is not None:
1154+ model.set_value(row.iter, LIST_TOGGLE_ACTIVE,
1155 new_selection_value)
1156
1157- def select_all_updgrades(self, widget):
1158+ def select_all_upgrades(self, widget):
1159 """
1160 Select all updates
1161 """
1162 self.setBusy(True)
1163 self.cache.saveDistUpgrade()
1164- self._toggle_origin_headers(True)
1165+ self._toggle_group_headers(True)
1166 self.treeview_update.queue_draw()
1167- self.refresh_updates_count()
1168+ self.updates_changed()
1169 self.setBusy(False)
1170
1171- def select_none_updgrades(self, widget):
1172+ def select_none_upgrades(self, widget):
1173 """
1174 Select none updates
1175 """
1176 self.setBusy(True)
1177 self.cache.clear()
1178- self._toggle_origin_headers(False)
1179+ self._toggle_group_headers(False)
1180 self.treeview_update.queue_draw()
1181- self.refresh_updates_count()
1182+ self.updates_changed()
1183 self.setBusy(False)
1184
1185 def setBusy(self, flag):
1186@@ -417,7 +641,21 @@
1187 while Gtk.events_pending():
1188 Gtk.main_iteration()
1189
1190- def refresh_updates_count(self):
1191+ def _mark_selected_updates(self):
1192+ def foreach_cb(model, path, iter, data):
1193+ data = model.get_value(iter, LIST_UPDATE_DATA)
1194+ active = False
1195+ if data.item:
1196+ active = data.item.pkg.marked_install or \
1197+ data.item.pkg.marked_upgrade
1198+ elif data.group:
1199+ active = data.group.packages_are_selected()
1200+ elif data.groups:
1201+ active = any([g.packages_are_selected() for g in data.groups])
1202+ model.set_value(iter, LIST_TOGGLE_ACTIVE, active)
1203+ self.store.foreach(foreach_cb, None)
1204+
1205+ def _refresh_updates_count(self):
1206 self.button_install.set_sensitive(self.cache.install_count)
1207 try:
1208 inst_count = self.cache.install_count
1209@@ -458,10 +696,14 @@
1210 self.hbox_downsize.show()
1211 self.vbox_alerts.show()
1212
1213+ def updates_changed(self):
1214+ self._mark_selected_updates()
1215+ self._refresh_updates_count()
1216+
1217 def update_count(self):
1218 """activate or disable widgets and show dialog texts correspoding to
1219 the number of available updates"""
1220- self.refresh_updates_count()
1221+ self.updates_changed()
1222
1223 text_header = None
1224 text_desc = None
1225@@ -486,8 +728,6 @@
1226 self.notebook_details.set_sensitive(True)
1227 self.treeview_update.set_sensitive(True)
1228 self.button_install.grab_default()
1229- self.treeview_update.set_cursor(Gtk.TreePath.new_from_string("1"),
1230- None, False)
1231 self.label_header.set_label(text_header)
1232
1233 if text_desc is not None:
1234@@ -573,14 +813,14 @@
1235 # can deal with dialup connections properly
1236 if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:
1237 self.label_offline.set_text(_("Connecting..."))
1238- self.refresh_updates_count()
1239+ self.updates_changed()
1240 self.hbox_offline.show()
1241 self.vbox_alerts.show()
1242 self.connected = False
1243 # in doubt (STATE_UNKNOWN), assume connected
1244 elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST or
1245 state == NetworkManagerHelper.NM_STATE_UNKNOWN):
1246- self.refresh_updates_count()
1247+ self.updates_changed()
1248 self.hbox_offline.hide()
1249 self.connected = True
1250 # trigger re-showing the current app to get changelog info (if
1251@@ -590,7 +830,7 @@
1252 self.connected = False
1253 self.label_offline.set_text(_("You may not be able to check for "
1254 "updates or download new updates."))
1255- self.refresh_updates_count()
1256+ self.updates_changed()
1257 self.hbox_offline.show()
1258 self.vbox_alerts.show()
1259
1260@@ -613,63 +853,37 @@
1261 self.hbox_on_3g.hide()
1262 self.hbox_roaming.hide()
1263
1264- def row_activated(self, treeview, path, column):
1265- iter = self.store.get_iter(path)
1266-
1267- pkg = self.store.get_value(iter, LIST_PKG)
1268- origin = self.store.get_value(iter, LIST_ORIGIN)
1269- if pkg is not None:
1270- return
1271- self.toggle_from_origin(pkg, origin, True)
1272-
1273- def toggle_from_origin(self, pkg, origin, select_all=True):
1274- self.setBusy(True)
1275- actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
1276- for pkg in self.list.pkgs[origin]:
1277- if pkg.marked_install or pkg.marked_upgrade:
1278- #print("marking keep: ", pkg.name)
1279- pkg.mark_keep()
1280- elif not (pkg.name in self.list.held_back):
1281- #print("marking install: ", pkg.name)
1282- pkg.mark_install(auto_fix=False, auto_inst=False)
1283- # check if we left breakage
1284- if self.cache._depcache.broken_count:
1285- Fix = apt_pkg.ProblemResolver(self.cache._depcache)
1286- Fix.resolve_by_keep()
1287- self.refresh_updates_count()
1288- self.treeview_update.queue_draw()
1289- del actiongroup
1290- self.setBusy(False)
1291-
1292- def toggled(self, renderer, path):
1293+ def on_update_toggled(self, renderer, path):
1294 """ a toggle button in the listview was toggled """
1295 iter = self.store.get_iter(path)
1296- pkg = self.store.get_value(iter, LIST_PKG)
1297- origin = self.store.get_value(iter, LIST_ORIGIN)
1298+ data = self.store.get_value(iter, LIST_UPDATE_DATA)
1299 # make sure that we don't allow to toggle deactivated updates
1300 # this is needed for the call by the row activation callback
1301- if pkg is None:
1302- toggled_value = not self.store.get_value(iter, LIST_TOGGLE_CHECKED)
1303- self.toggle_from_origin(pkg, origin, toggled_value)
1304- self.store.set_value(iter, LIST_TOGGLE_CHECKED, toggled_value)
1305+ if data.groups or data.group:
1306+ if data.groups:
1307+ self.toggle_from_groups(data.groups)
1308+ elif data.group:
1309+ self.toggle_from_groups([data.group])
1310+ self.updates_changed()
1311 self.treeview_update.queue_draw()
1312 return
1313- if pkg is None or pkg.name in self.list.held_back:
1314+ if (data.item is None or data.item.pkg is None or
1315+ data.item.pkg.name in self.list.held_back):
1316 return False
1317 self.setBusy(True)
1318 # update the cache
1319- if pkg.marked_install or pkg.marked_upgrade:
1320- pkg.mark_keep()
1321+ if data.item.pkg.marked_install or data.item.pkg.marked_upgrade:
1322+ data.item.pkg.mark_keep()
1323 if self.cache._depcache.broken_count:
1324 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
1325 Fix.resolve_by_keep()
1326 else:
1327 try:
1328- pkg.mark_install()
1329+ data.item.pkg.mark_install()
1330 except SystemError:
1331 pass
1332+ self.updates_changed()
1333 self.treeview_update.queue_draw()
1334- self.refresh_updates_count()
1335 self.setBusy(False)
1336
1337 def on_treeview_update_row_activated(self, treeview, path, column, *args):
1338@@ -677,7 +891,29 @@
1339 If an update row was activated (by pressing space), toggle the
1340 install check box
1341 """
1342- self.toggled(None, path)
1343+ self.on_update_toggled(None, path)
1344+
1345+ def toggle_from_groups(self, groups):
1346+ self.setBusy(True)
1347+ actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
1348+
1349+ # Deselect all updates if any in group are selected
1350+ keep_packages = any([g.packages_are_selected() for g in groups])
1351+ for group in groups:
1352+ for item in group.items:
1353+ if keep_packages:
1354+ item.pkg.mark_keep()
1355+ elif not item.pkg.name in self.list.held_back:
1356+ item.pkg.mark_install(auto_fix=False, auto_inst=False)
1357+
1358+ # check if we left breakage
1359+ if self.cache._depcache.broken_count:
1360+ Fix = apt_pkg.ProblemResolver(self.cache._depcache)
1361+ Fix.resolve_by_keep()
1362+ self.updates_changed()
1363+ self.treeview_update.queue_draw()
1364+ del actiongroup
1365+ self.setBusy(False)
1366
1367 def save_state(self):
1368 """ save the state (window-size for now) """
1369@@ -693,9 +929,58 @@
1370 expanded = self.expander_details.get_expanded()
1371 self.window_main.set_resizable(expanded)
1372 if w > 0 and h > 0 and expanded:
1373- self.window_main.resize(w, h)
1374+ # There is a race here. If we immediately resize, it often doesn't
1375+ # take. Using idle_add helps, but we *still* occasionally don't
1376+ # restore the size correctly. Help needed to track this down!
1377+ GLib.idle_add(lambda: self.window_main.resize(w, h))
1378 return False
1379
1380+ def _add_header(self, name, groups):
1381+ total_size = 0
1382+ for group in groups:
1383+ total_size = total_size + group.get_total_size()
1384+ header_row = [
1385+ name,
1386+ UpdateData(groups, None, None),
1387+ humanize_size(total_size),
1388+ True
1389+ ]
1390+ return self.store.append(None, header_row)
1391+
1392+ def _add_groups(self, groups):
1393+ # Each row contains:
1394+ # row label (for screen reader),
1395+ # update data tuple (is_toplevel, group object, package object),
1396+ # update size,
1397+ # update selection state
1398+ for group in groups:
1399+ if not group.items:
1400+ continue
1401+
1402+ group_is_item = None
1403+ if not isinstance(group, UpdateSystemGroup) and \
1404+ len(group.items) == 1:
1405+ group_is_item = group.items[0]
1406+
1407+ group_row = [
1408+ group.name,
1409+ UpdateData(None, group, group_is_item),
1410+ humanize_size(group.get_total_size()),
1411+ True
1412+ ]
1413+ group_iter = self.store.append(None, group_row)
1414+
1415+ if group_is_item:
1416+ continue
1417+ for item in group.items:
1418+ item_row = [
1419+ item.name,
1420+ UpdateData(None, None, item),
1421+ humanize_size(getattr(item.pkg.candidate, "size", 0)),
1422+ True
1423+ ]
1424+ self.store.append(group_iter, item_row)
1425+
1426 def fillstore(self):
1427 # use the watch cursor
1428 self.setBusy(True)
1429@@ -706,46 +991,22 @@
1430 self.dl_size = 0
1431
1432 self.scrolledwindow_update.show()
1433- origin_list = sorted(
1434- self.list.pkgs, key=operator.attrgetter("importance"),
1435- reverse=True)
1436- for origin in origin_list:
1437- self.store.append(['<b><big>%s</big></b>' % origin.description,
1438- origin.description, None, origin, True])
1439- for pkg in self.list.pkgs[origin]:
1440- name = xml.sax.saxutils.escape(pkg.name)
1441- if not pkg.is_installed:
1442- name += _(" (New install)")
1443- summary = xml.sax.saxutils.escape(getattr(pkg.candidate,
1444- "summary", ""))
1445- if self.summary_before_name:
1446- contents = "%s\n<small>%s</small>" % (summary, name)
1447- else:
1448- contents = "<b>%s</b>\n<small>%s</small>" % (name, summary)
1449- #TRANSLATORS: the b stands for Bytes
1450- size = _("(Size: %s)") % humanize_size(getattr(pkg.candidate,
1451- "size", 0))
1452- installed_version = getattr(pkg.installed, "version", None)
1453- candidate_version = getattr(pkg.candidate, "version", None)
1454- if installed_version is not None:
1455- version = _("From version %(old_version)s "
1456- "to %(new_version)s") % {
1457- "old_version": installed_version,
1458- "new_version": candidate_version}
1459- else:
1460- version = _("Version %s") % candidate_version
1461- if self.show_versions:
1462- contents = "%s\n<small>%s %s</small>" % (contents, version,
1463- size)
1464- else:
1465- contents = "%s <small>%s</small>" % (contents, size)
1466- self.store.append([contents, pkg.name, pkg, None, True])
1467+
1468+ # add security and update groups to self.store
1469+ if self.list.security_groups:
1470+ self._add_header(_("Security updates"), self.list.security_groups)
1471+ self._add_groups(self.list.security_groups)
1472+ if self.list.security_groups and self.list.update_groups:
1473+ self._add_header(_("Other updates"), self.list.update_groups)
1474+ if self.list.update_groups:
1475+ self._add_groups(self.list.update_groups)
1476+
1477 self.treeview_update.set_model(self.store)
1478 self.update_count()
1479 self.setBusy(False)
1480 while Gtk.events_pending():
1481 Gtk.main_iteration()
1482- self.refresh_updates_count()
1483+ self.updates_changed()
1484 return False
1485
1486 def main(self):
1487
1488=== modified file 'data/com.ubuntu.update-manager.gschema.xml.in'
1489--- data/com.ubuntu.update-manager.gschema.xml.in 2012-05-31 22:28:41 +0000
1490+++ data/com.ubuntu.update-manager.gschema.xml.in 2013-01-23 15:48:20 +0000
1491@@ -6,12 +6,12 @@
1492 <description>Stores the state of the expander that contains the list of changes and the description</description>
1493 </key>
1494 <key name="window-width" type="i">
1495- <default>0</default>
1496+ <default>1</default>
1497 <summary>The window width</summary>
1498 <description>Stores the width of the update-manager dialog</description>
1499 </key>
1500 <key name="window-height" type="i">
1501- <default>0</default>
1502+ <default>400</default>
1503 <summary>The window height</summary>
1504 <description>Stores the height of the update-manager dialog</description>
1505 </key>
1506
1507=== modified file 'data/gtkbuilder/UpdateManager.ui'
1508--- data/gtkbuilder/UpdateManager.ui 2012-12-14 22:11:25 +0000
1509+++ data/gtkbuilder/UpdateManager.ui 2013-01-23 15:48:20 +0000
1510@@ -112,11 +112,13 @@
1511 <property name="visible">True</property>
1512 <property name="can_focus">True</property>
1513 <property name="shadow_type">in</property>
1514+ <property name="min_content_height">100</property>
1515 <child>
1516 <object class="GtkTreeView" id="treeview_update">
1517 <property name="visible">True</property>
1518 <property name="can_focus">True</property>
1519 <property name="headers_visible">False</property>
1520+ <property name="headers_clickable">False</property>
1521 <property name="rules_hint">True</property>
1522 <child internal-child="accessible">
1523 <object class="AtkObject" id="treeview_update-atkobject">
1524@@ -470,11 +472,9 @@
1525 <child>
1526 <object class="GtkButton" id="button_settings">
1527 <property name="label" translatable="yes">_Settings...</property>
1528- <property name="use_action_appearance">False</property>
1529 <property name="visible">True</property>
1530 <property name="can_focus">True</property>
1531 <property name="receives_default">True</property>
1532- <property name="use_action_appearance">False</property>
1533 <property name="use_underline">True</property>
1534 <signal name="clicked" handler="on_button_settings_clicked" swapped="no"/>
1535 </object>
1536@@ -488,15 +488,13 @@
1537 <child>
1538 <object class="GtkButton" id="button_close">
1539 <property name="label">gtk-cancel</property>
1540- <property name="use_action_appearance">False</property>
1541 <property name="visible">True</property>
1542 <property name="can_focus">True</property>
1543 <property name="can_default">True</property>
1544 <property name="receives_default">True</property>
1545- <property name="use_action_appearance">False</property>
1546 <property name="use_stock">True</property>
1547+ <accelerator key="W" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
1548 <accelerator key="Q" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
1549- <accelerator key="W" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
1550 </object>
1551 <packing>
1552 <property name="expand">False</property>
1553@@ -507,13 +505,11 @@
1554 <child>
1555 <object class="GtkButton" id="button_install">
1556 <property name="label" translatable="yes">_Install Now</property>
1557- <property name="use_action_appearance">False</property>
1558 <property name="visible">True</property>
1559 <property name="can_focus">True</property>
1560 <property name="can_default">True</property>
1561 <property name="has_default">True</property>
1562 <property name="receives_default">True</property>
1563- <property name="use_action_appearance">False</property>
1564 <property name="image">image-apply</property>
1565 <property name="use_underline">True</property>
1566 <signal name="clicked" handler="on_button_install_clicked" swapped="no"/>
1567
1568=== added directory 'tests/aptroot-grouping-test'
1569=== added directory 'tests/aptroot-grouping-test/etc'
1570=== added directory 'tests/aptroot-grouping-test/etc/apt'
1571=== added file 'tests/aptroot-grouping-test/etc/apt/sources.list'
1572--- tests/aptroot-grouping-test/etc/apt/sources.list 1970-01-01 00:00:00 +0000
1573+++ tests/aptroot-grouping-test/etc/apt/sources.list 2013-01-23 15:48:20 +0000
1574@@ -0,0 +1,2 @@
1575+deb http://archive.ubuntu.com/ubuntu lucid main
1576+deb http://archive.ubuntu.com/ubuntu lucid-security main
1577
1578=== added symlink 'tests/aptroot-grouping-test/etc/apt/trusted.gpg'
1579=== target is u'/etc/apt/trusted.gpg'
1580=== added directory 'tests/aptroot-grouping-test/var'
1581=== added directory 'tests/aptroot-grouping-test/var/cache'
1582=== added directory 'tests/aptroot-grouping-test/var/lib'
1583=== added directory 'tests/aptroot-grouping-test/var/lib/apt'
1584=== added directory 'tests/aptroot-grouping-test/var/lib/apt/lists'
1585=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release'
1586--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release 1970-01-01 00:00:00 +0000
1587+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release 2013-01-23 15:48:20 +0000
1588@@ -0,0 +1,9 @@
1589+Origin: Ubuntu
1590+Label: Ubuntu
1591+Suite: lucid-security
1592+Version: 10.04
1593+Codename: lucid
1594+Date: Thu, 29 Apr 2010 17:24:55 UTC
1595+Architectures: amd64 armel i386 ia64 powerpc sparc
1596+Components: main restricted universe multiverse
1597+Description: Ubuntu Lucid 10.04
1598
1599=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg'
1600--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg 1970-01-01 00:00:00 +0000
1601+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg 2013-01-23 15:48:20 +0000
1602@@ -0,0 +1,7 @@
1603+-----BEGIN PGP SIGNATURE-----
1604+Version: GnuPG v1.4.6 (GNU/Linux)
1605+
1606+iD8DBQBL2cDzQJdur0N9BbURAmk2AJ9ungOjKn0ektAH87KhRIHht+1cDQCfck7P
1607+ZoIb2P0v2PEqa4Az8KnIIW4=
1608+=b/mY
1609+-----END PGP SIGNATURE-----
1610
1611=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages'
1612--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages 1970-01-01 00:00:00 +0000
1613+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages 2013-01-23 15:48:20 +0000
1614@@ -0,0 +1,10 @@
1615+Package: base-pkg
1616+Priority: optional
1617+Section: admin
1618+Installed-Size: 1
1619+Maintainer: Foo <foo@bar.com>
1620+Architecture: all
1621+Version: 2
1622+Size: 1
1623+Description: a base package
1624+Origin: Ubuntu
1625
1626=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release'
1627--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release 1970-01-01 00:00:00 +0000
1628+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release 2013-01-23 15:48:20 +0000
1629@@ -0,0 +1,9 @@
1630+Origin: Ubuntu
1631+Label: Ubuntu
1632+Suite: lucid
1633+Version: 10.04
1634+Codename: lucid
1635+Date: Thu, 29 Apr 2010 17:24:55 UTC
1636+Architectures: amd64 armel i386 ia64 powerpc sparc
1637+Components: main restricted universe multiverse
1638+Description: Ubuntu Lucid 10.04
1639
1640=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg'
1641--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg 1970-01-01 00:00:00 +0000
1642+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg 2013-01-23 15:48:20 +0000
1643@@ -0,0 +1,7 @@
1644+-----BEGIN PGP SIGNATURE-----
1645+Version: GnuPG v1.4.6 (GNU/Linux)
1646+
1647+iD8DBQBL2cDzQJdur0N9BbURAmk2AJ9ungOjKn0ektAH87KhRIHht+1cDQCfck7P
1648+ZoIb2P0v2PEqa4Az8KnIIW4=
1649+=b/mY
1650+-----END PGP SIGNATURE-----
1651
1652=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_main_binary-amd64_Packages'
1653--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_main_binary-amd64_Packages 1970-01-01 00:00:00 +0000
1654+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_main_binary-amd64_Packages 2013-01-23 15:48:20 +0000
1655@@ -0,0 +1,80 @@
1656+Package: installed-app
1657+Priority: optional
1658+Section: admin
1659+Installed-Size: 1
1660+Maintainer: Foo <foo@bar.com>
1661+Architecture: all
1662+Version: 2
1663+Size: 1
1664+Depends: installed-pkg-multiple-deps
1665+Suggests: installed-pkg-single-dep
1666+Description: just an app
1667+Origin: Ubuntu
1668+
1669+Package: installed-app-with-subitems
1670+Priority: optional
1671+Section: admin
1672+Installed-Size: 1
1673+Maintainer: Foo <foo@bar.com>
1674+Architecture: all
1675+Version: 2
1676+Size: 1
1677+Recommends: intermediate, installed-pkg-multiple-deps
1678+Description: app with subitems
1679+Origin: Ubuntu
1680+
1681+Package: intermediate
1682+Priority: optional
1683+Section: admin
1684+Installed-Size: 1
1685+Maintainer: Foo <foo@bar.com>
1686+Architecture: all
1687+Version: 0
1688+Size: 1
1689+Description: intermediate pkg
1690+Origin: Ubuntu
1691+
1692+Package: installed-pkg
1693+Priority: optional
1694+Section: admin
1695+Installed-Size: 1
1696+Maintainer: Foo <foo@bar.com>
1697+Architecture: all
1698+Version: 2
1699+Size: 1
1700+Description: just a pkg
1701+Origin: Ubuntu
1702+
1703+Package: installed-pkg-multiple-deps
1704+Priority: optional
1705+Section: admin
1706+Installed-Size: 1
1707+Maintainer: Foo <foo@bar.com>
1708+Architecture: all
1709+Version: 2
1710+Size: 1
1711+Description: pkg with multiple deps
1712+Origin: Ubuntu
1713+
1714+Package: installed-pkg-single-dep
1715+Priority: optional
1716+Section: admin
1717+Installed-Size: 1
1718+Maintainer: Foo <foo@bar.com>
1719+Architecture: all
1720+Version: 2
1721+Size: 1
1722+Description: pkg with single dep
1723+Origin: Ubuntu
1724+
1725+Package: ubuntu-minimal
1726+Priority: optional
1727+Section: admin
1728+Installed-Size: 1
1729+Maintainer: Foo <foo@bar.com>
1730+Architecture: all
1731+Version: 2
1732+Size: 1
1733+Recommends: base-pkg
1734+Description: meta package
1735+Origin: Ubuntu
1736
1737=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/lock'
1738=== added directory 'tests/aptroot-grouping-test/var/lib/apt/lists/partial'
1739=== added directory 'tests/aptroot-grouping-test/var/lib/dpkg'
1740=== added file 'tests/aptroot-grouping-test/var/lib/dpkg/status'
1741--- tests/aptroot-grouping-test/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
1742+++ tests/aptroot-grouping-test/var/lib/dpkg/status 2013-01-23 15:48:20 +0000
1743@@ -0,0 +1,82 @@
1744+Package: base-pkg
1745+Status: install ok installed
1746+Priority: optional
1747+Section: admin
1748+Installed-Size: 1
1749+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1750+Architecture: all
1751+Version: 0.1
1752+Description: a base package
1753+
1754+Package: installed-app
1755+Status: install ok installed
1756+Priority: optional
1757+Section: admin
1758+Installed-Size: 1
1759+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1760+Architecture: all
1761+Version: 0.1
1762+Description: an installed app
1763+
1764+Package: installed-app-with-subitems
1765+Status: install ok installed
1766+Priority: optional
1767+Section: admin
1768+Installed-Size: 1
1769+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1770+Architecture: all
1771+Version: 0.1
1772+Description: an installed app with subitems
1773+
1774+Package: intermediate
1775+Status: install ok installed
1776+Priority: optional
1777+Section: admin
1778+Installed-Size: 1
1779+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1780+Architecture: all
1781+Version: 0.1
1782+Depends: installed-pkg-single-dep
1783+Description: an intermediate pkg
1784+
1785+Package: installed-pkg
1786+Status: install ok installed
1787+Priority: optional
1788+Section: admin
1789+Installed-Size: 1
1790+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1791+Architecture: all
1792+Version: 0.1
1793+Description: an installed package
1794+
1795+Package: not-updatable
1796+Status: install ok installed
1797+Priority: optional
1798+Section: admin
1799+Installed-Size: 1
1800+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1801+Architecture: all
1802+Version: 0.1
1803+Depends: installed-pkg-single-dep
1804+Description: extra pkg
1805+ to confirm we only look at updatable pkgs when calculating groupings
1806+
1807+Package: installed-pkg-multiple-deps
1808+Status: install ok installed
1809+Priority: optional
1810+Section: admin
1811+Installed-Size: 1
1812+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1813+Architecture: all
1814+Version: 0.1
1815+Description: an installed package that multiple apps depend on
1816+
1817+Package: installed-pkg-single-dep
1818+Status: install ok installed
1819+Priority: optional
1820+Section: admin
1821+Installed-Size: 1
1822+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
1823+Architecture: all
1824+Version: 0.1
1825+Description: an installed package that only one app depends on
1826
1827=== added directory 'tests/aptroot-grouping-test/var/lib/dpkg/updates'
1828=== modified file 'tests/test_update_list.py'
1829--- tests/test_update_list.py 2013-01-16 12:45:38 +0000
1830+++ tests/test_update_list.py 2013-01-23 15:48:20 +0000
1831@@ -6,15 +6,16 @@
1832 import apt
1833 import unittest
1834
1835-from UpdateManager.Core.UpdateList import UpdateList
1836+from UpdateManager.Core import UpdateList
1837 from UpdateManager.Core.MyCache import MyCache
1838
1839-from mock import patch
1840+from gi.repository import Gio
1841+from mock import patch, PropertyMock, MagicMock
1842
1843 CURDIR = os.path.dirname(os.path.abspath(__file__))
1844
1845
1846-class UpdateListTestCase(unittest.TestCase):
1847+class PhasedTestCase(unittest.TestCase):
1848
1849 def setUp(self):
1850 # mangle the arch
1851@@ -30,11 +31,10 @@
1852 self.cache._listsLock = 0
1853 self.cache.update()
1854 self.cache.open()
1855- self.updates_list = UpdateList(parent=None)
1856+ self.updates_list = UpdateList.UpdateList(parent=None)
1857
1858 def assertUpdatesListLen(self, nr):
1859- origin = list(self.updates_list.pkgs.keys())[0]
1860- self.assertEqual(len(self.updates_list.pkgs[origin]), nr)
1861+ self.assertEqual(self.updates_list.num_updates, nr)
1862
1863 def test_phased_percentage_not_included(self):
1864 """ Test that updates above the threshold are not included"""
1865@@ -76,16 +76,96 @@
1866 self.updates_list.update(self.cache)
1867 self.assertUpdatesListLen(1)
1868
1869- @patch('UpdateManager.Core.UpdateList.OriginsImportance')
1870- def test_phased_percentage_from_security(self, mock_origin_importance):
1871+ @patch('UpdateManager.Core.UpdateList.UpdateList._is_security_update')
1872+ def test_phased_percentage_from_security(self, mock_security):
1873 """ Test that updates from the security node go in"""
1874 # pretend all updates come from security for the sake of this test
1875- mock_origin_importance.SECURITY = 0
1876+ mock_security.return_value = True
1877 with patch.object(self.updates_list.random, "randint") as mock_randint:
1878 mock_randint.return_value = 100
1879 self.updates_list.update(self.cache)
1880 self.assertUpdatesListLen(2)
1881
1882+class GroupingTestCase(unittest.TestCase):
1883+ # installed_files does not respect aptroot, so we have to patch it
1884+ @patch('apt.package.Package.installed_files', new_callable=PropertyMock)
1885+ @patch('gi.repository.Gio.DesktopAppInfo.new_from_filename')
1886+ def setUp(self, mock_desktop, mock_installed):
1887+ # mangle the arch
1888+ real_arch = apt.apt_pkg.config.find("APT::Architecture")
1889+ apt.apt_pkg.config.set("APT::Architecture", "amd64")
1890+ self.addCleanup(
1891+ lambda: apt.apt_pkg.config.set("APT::Architecture", real_arch))
1892+ self.aptroot = os.path.join(CURDIR,
1893+ "aptroot-grouping-test")
1894+ self.cache = MyCache(apt.progress.base.OpProgress(),
1895+ rootdir=self.aptroot)
1896+ mock_installed.__get__ = self.fake_installed_files
1897+ mock_desktop.side_effect = self.fake_desktop
1898+ self.updates_list = UpdateList.UpdateList(parent=None, dist='lucid')
1899+ self.updates_list.update(self.cache)
1900+
1901+ def fake_installed_files(self, mock_prop, pkg, pkg_class):
1902+ if pkg.name == 'installed-app':
1903+ return ['/usr/share/applications/installed-app.desktop']
1904+ elif pkg.name == 'installed-app-with-subitems':
1905+ return ['/usr/share/applications/installed-app2.desktop']
1906+ else:
1907+ return []
1908+
1909+ def fake_desktop(self, path):
1910+ # These can all be the same for our purposes
1911+ app = MagicMock()
1912+ app.get_filename.return_value = path
1913+ app.get_display_name.return_value = 'App ' + os.path.basename(path)
1914+ app.get_icon.return_value = Gio.ThemedIcon.new("package")
1915+ return app
1916+
1917+ def test_app(self):
1918+ self.assertGreater(len(self.updates_list.update_groups), 0)
1919+ group = self.updates_list.update_groups[0]
1920+ self.assertIsInstance(group, UpdateList.UpdateApplicationGroup)
1921+ self.assertIsNotNone(group.core_item)
1922+ self.assertEqual(group.core_item.pkg.name, 'installed-app')
1923+ self.assertListEqual([x.pkg.name for x in group.items],
1924+ ['installed-app'])
1925+
1926+ def test_app_with_subitems(self):
1927+ self.assertGreater(len(self.updates_list.update_groups), 1)
1928+ group = self.updates_list.update_groups[1]
1929+ self.assertIsInstance(group, UpdateList.UpdateApplicationGroup)
1930+ self.assertIsNotNone(group.core_item)
1931+ self.assertEqual(group.core_item.pkg.name,
1932+ 'installed-app-with-subitems')
1933+ self.assertListEqual([x.pkg.name for x in group.items],
1934+ ['installed-app-with-subitems',
1935+ 'installed-pkg-single-dep'])
1936+
1937+ def test_pkg(self):
1938+ self.assertGreater(len(self.updates_list.update_groups), 2)
1939+ group = self.updates_list.update_groups[2]
1940+ self.assertIsInstance(group, UpdateList.UpdatePackageGroup)
1941+ self.assertIsNotNone(group.core_item)
1942+ self.assertEqual(group.core_item.pkg.name, 'installed-pkg')
1943+ self.assertListEqual([x.pkg.name for x in group.items],
1944+ ['installed-pkg'])
1945+
1946+ def test_pkg_multiple_deps(self):
1947+ self.assertEqual(len(self.updates_list.update_groups), 4)
1948+ group = self.updates_list.update_groups[3]
1949+ self.assertIsInstance(group, UpdateList.UpdatePackageGroup)
1950+ self.assertIsNotNone(group.core_item)
1951+ self.assertEqual(group.core_item.pkg.name,
1952+ 'installed-pkg-multiple-deps')
1953+ self.assertListEqual([x.pkg.name for x in group.items],
1954+ ['installed-pkg-multiple-deps'])
1955+
1956+ def test_security(self):
1957+ self.assertEqual(len(self.updates_list.security_groups), 1)
1958+ group = self.updates_list.security_groups[0]
1959+ self.assertIsInstance(group, UpdateList.UpdateSystemGroup)
1960+ self.assertIsNone(group.core_item)
1961+ self.assertListEqual([x.pkg.name for x in group.items], ['base-pkg'])
1962
1963 if __name__ == "__main__":
1964 unittest.main()
1965
1966=== modified file 'tests/test_update_origin.py'
1967--- tests/test_update_origin.py 2012-11-19 16:20:30 +0000
1968+++ tests/test_update_origin.py 2013-01-23 15:48:20 +0000
1969@@ -48,15 +48,11 @@
1970 if l.archive == "lucid-security"]:
1971 test_pkgs.add(pkg.name)
1972 self.assertTrue(len(test_pkgs) > 0)
1973- ul = UpdateList(None)
1974- matcher = ul.initMatcher("lucid")
1975+ ul = UpdateList(None, dist="lucid")
1976 for pkgname in test_pkgs:
1977 pkg = self.cache[pkgname]
1978- origin = self.cache.match_package_origin(pkg, matcher)
1979- self.assertEqual(self.cache.match_package_origin(pkg, matcher),
1980- matcher[("lucid-security", "Ubuntu")],
1981- "pkg '%s' is not in lucid-security but in '%s' "
1982- "instead" % (pkg.name, origin.description))
1983+ self.assertTrue(ul._is_security_update(pkg),
1984+ "pkg '%s' is not in lucid-security" % pkg.name)
1985
1986 def testOriginMatcherWithVersionInUpdatesAndSecurity(self):
1987 # empty dpkg status
1988@@ -90,15 +86,13 @@
1989 "newer")
1990
1991 # now test if versions in -security are detected
1992- ul = UpdateList(None)
1993- matcher = ul.initMatcher("lucid")
1994+ ul = UpdateList(None, dist="lucid")
1995 for pkgname in test_pkgs:
1996 pkg = self.cache[pkgname]
1997- self.assertEqual(self.cache.match_package_origin(pkg, matcher),
1998- matcher[("lucid-security", "Ubuntu")],
1999- "package '%s' from lucid-updates contains also a "
2000- "(not yet installed) security updates, but it is "
2001- "not labeled as such" % pkg.name)
2002+ self.assertTrue(ul._is_security_update(pkg),
2003+ "package '%s' from lucid-updates contains also a "
2004+ "(not yet installed) security updates, but it is "
2005+ "not labeled as such" % pkg.name)
2006
2007 # now check if it marks the version with -update if the -security
2008 # version is installed
2009@@ -118,17 +112,14 @@
2010 self.cache.open()
2011 for pkgname in test_pkgs:
2012 pkg = self.cache[pkgname]
2013- self.assertNotEqual(None, pkg._pkg.current_ver,
2014- "no package '%s' installed" % pkg.name)
2015+ self.assertIsNotNone(pkg._pkg.current_ver,
2016+ "no package '%s' installed" % pkg.name)
2017 candidate_version = getattr(pkg.candidate, "version", None)
2018- origin = self.cache.match_package_origin(pkg, matcher)
2019- self.assertEqual(self.cache.match_package_origin(pkg, matcher),
2020- matcher[("lucid-updates", "Ubuntu")],
2021+ self.assertFalse(ul._is_security_update(pkg),
2022 "package '%s' (%s) from lucid-updates is "
2023- "labelled '%s' even though we have marked this "
2024- "version as installed already" %
2025- (pkg.name, candidate_version,
2026- origin.description))
2027+ "labelled as a security update even though we "
2028+ "have marked this version as installed already" %
2029+ (pkg.name, candidate_version))
2030
2031
2032 if __name__ == "__main__":
2033
2034=== modified file 'tests/test_utils.py'
2035--- tests/test_utils.py 2012-08-17 21:11:57 +0000
2036+++ tests/test_utils.py 2013-01-23 15:48:20 +0000
2037@@ -3,14 +3,11 @@
2038
2039 import logging
2040 import glob
2041+import mock
2042 import sys
2043 import unittest
2044
2045-from UpdateManager.Core.utils import (
2046- is_child_of_process_name,
2047- get_string_with_no_auth_from_source_entry,
2048- humanize_size,
2049- estimate_kernel_size_in_boot)
2050+from UpdateManager.Core import utils
2051
2052
2053 class TestUtils(unittest.TestCase):
2054@@ -18,27 +15,27 @@
2055 def test_humanize_size(self):
2056 # humanize size is a bit funny, it rounds up to kB as the meaningful
2057 # unit for users
2058- self.assertEqual(humanize_size(1000), "1 kB")
2059- self.assertEqual(humanize_size(10), "1 kB")
2060- self.assertEqual(humanize_size(1200), "2 kB")
2061+ self.assertEqual(utils.humanize_size(1000), "1 kB")
2062+ self.assertEqual(utils.humanize_size(10), "1 kB")
2063+ self.assertEqual(utils.humanize_size(1200), "2 kB")
2064 # but not for MB as well
2065- self.assertEqual(humanize_size(1200 * 1000), "1.2 MB")
2066- self.assertEqual(humanize_size(1478 * 1000), "1.5 MB")
2067+ self.assertEqual(utils.humanize_size(1200 * 1000), "1.2 MB")
2068+ self.assertEqual(utils.humanize_size(1478 * 1000), "1.5 MB")
2069 # and we don't go to Gb just yet (as its not really needed
2070 # in a upgrade context most of the time
2071- self.assertEqual(humanize_size(1000 * 1000 * 1000), "1000.0 MB")
2072+ self.assertEqual(utils.humanize_size(1000 * 1000 * 1000), "1000.0 MB")
2073
2074 @unittest.skipIf(not glob.glob("/boot/*"), "inside chroot")
2075 def test_estimate_kernel_size(self):
2076- estimate = estimate_kernel_size_in_boot()
2077+ estimate = utils.estimate_kernel_size_in_boot()
2078 self.assertTrue(estimate > 0)
2079
2080 def test_is_child_of_process_name(self):
2081- self.assertTrue(is_child_of_process_name("init"))
2082- self.assertFalse(is_child_of_process_name("mvo"))
2083+ self.assertTrue(utils.is_child_of_process_name("init"))
2084+ self.assertFalse(utils.is_child_of_process_name("mvo"))
2085 for e in glob.glob("/proc/[0-9]*"):
2086 pid = int(e[6:])
2087- is_child_of_process_name("gdm", pid)
2088+ utils.is_child_of_process_name("gdm", pid)
2089
2090 def test_is_port_listening(self):
2091 from UpdateManager.Core.utils import is_port_already_listening
2092@@ -49,16 +46,70 @@
2093 # entry with PW
2094 s = SourceEntry("deb http://user:pass@some-ppa/ ubuntu main")
2095 self.assertTrue(
2096- not "user" in get_string_with_no_auth_from_source_entry(s))
2097+ not "user" in utils.get_string_with_no_auth_from_source_entry(s))
2098 self.assertTrue(
2099- not "pass" in get_string_with_no_auth_from_source_entry(s))
2100- self.assertEqual(get_string_with_no_auth_from_source_entry(s),
2101+ not "pass" in utils.get_string_with_no_auth_from_source_entry(s))
2102+ self.assertEqual(utils.get_string_with_no_auth_from_source_entry(s),
2103 "deb http://hidden-u:hidden-p@some-ppa/ ubuntu main")
2104 # no pw
2105 s = SourceEntry("deb http://some-ppa/ ubuntu main")
2106- self.assertEqual(get_string_with_no_auth_from_source_entry(s),
2107+ self.assertEqual(utils.get_string_with_no_auth_from_source_entry(s),
2108 "deb http://some-ppa/ ubuntu main")
2109
2110+ @mock.patch('UpdateManager.Core.utils._load_meta_pkg_list')
2111+ def test_flavor_package_ubuntu_first(self, mock_load):
2112+ cache = {'ubuntu-desktop': mock.MagicMock(),
2113+ 'other-desktop': mock.MagicMock()}
2114+ cache['ubuntu-desktop'].is_installed = True
2115+ cache['other-desktop'].is_installed = True
2116+ mock_load.return_value = ['other-desktop']
2117+ self.assertEqual(utils.get_ubuntu_flavor_package(cache=cache),
2118+ 'ubuntu-desktop')
2119+
2120+ @mock.patch('UpdateManager.Core.utils._load_meta_pkg_list')
2121+ def test_flavor_package_match(self, mock_load):
2122+ cache = {'a': mock.MagicMock(),
2123+ 'b': mock.MagicMock(),
2124+ 'c': mock.MagicMock()}
2125+ cache['a'].is_installed = True
2126+ cache['b'].is_installed = True
2127+ cache['c'].is_installed = True
2128+ mock_load.return_value = ['c', 'a', 'b']
2129+ # Must pick alphabetically first
2130+ self.assertEqual(utils.get_ubuntu_flavor_package(cache=cache), 'a')
2131+
2132+ def test_flavor_package_default(self):
2133+ self.assertEqual(utils.get_ubuntu_flavor_package(cache={}),
2134+ 'ubuntu-desktop')
2135+
2136+ def test_flavor_default(self):
2137+ self.assertEqual(utils.get_ubuntu_flavor(cache={}), 'ubuntu')
2138+
2139+ @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
2140+ def test_flavor_simple(self, mock_package):
2141+ mock_package.return_value = 'd'
2142+ self.assertEqual(utils.get_ubuntu_flavor(), 'd')
2143+
2144+ @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
2145+ def test_flavor_chop(self, mock_package):
2146+ mock_package.return_value = 'd-pkg'
2147+ self.assertEqual(utils.get_ubuntu_flavor(), 'd')
2148+
2149+ @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
2150+ def test_flavor_name_desktop(self, mock_package):
2151+ mock_package.return_value = 'something-desktop'
2152+ self.assertEqual(utils.get_ubuntu_flavor_name(), 'Something')
2153+
2154+ @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
2155+ def test_flavor_name_netbook(self, mock_package):
2156+ mock_package.return_value = 'something-netbook'
2157+ self.assertEqual(utils.get_ubuntu_flavor_name(), 'Something')
2158+
2159+ @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
2160+ def test_flavor_name_studio(self, mock_package):
2161+ mock_package.return_value = 'ubuntustudio-desktop'
2162+ self.assertEqual(utils.get_ubuntu_flavor_name(), 'Ubuntu Studio')
2163+
2164
2165 if __name__ == '__main__':
2166 if len(sys.argv) > 1 and sys.argv[1] == "-v":

Subscribers

People subscribed via source and target branches

to status/vote changes: