Merge lp:~mterry/update-manager/group-by-applications into lp:update-manager

Proposed by Michael Terry
Status: Merged
Merged at revision: 2582
Proposed branch: lp:~mterry/update-manager/group-by-applications
Merge into: lp:update-manager
Diff against target: 2107 lines (+1165/-361)
17 files modified
UpdateManager/Core/MyCache.py (+2/-55)
UpdateManager/Core/UpdateList.py (+300/-67)
UpdateManager/Core/utils.py (+64/-26)
UpdateManager/UpdatesAvailable.py (+415/-153)
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:~mterry/update-manager/group-by-applications
Reviewer Review Type Date Requested Status
Ubuntu Core Development Team Pending
Review via email: mp+144046@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.
2481. By Michael Terry

merge from trunk

2482. By Michael Terry

don't calculate linux meta packages on the fly, because that may require deb-src lines in sources.list; rather just include a hard-coded list

Revision history for this message
Michael Terry (mterry) wrote :

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

Subscribers

People subscribed via source and target branches

to status/vote changes: