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
=== modified file 'UpdateManager/Core/MyCache.py'
--- UpdateManager/Core/MyCache.py 2012-06-28 00:10:23 +0000
+++ UpdateManager/Core/MyCache.py 2013-01-23 15:48:20 +0000
@@ -43,7 +43,6 @@
43import re43import re
44import DistUpgrade.DistUpgradeCache44import DistUpgrade.DistUpgradeCache
45from gettext import gettext as _45from gettext import gettext as _
46from .UpdateList import UpdateOrigin
4746
48SYNAPTIC_PINFILE = "/var/lib/synaptic/preferences"47SYNAPTIC_PINFILE = "/var/lib/synaptic/preferences"
49CHANGELOGS_POOL = "http://changelogs.ubuntu.com/changelogs/pool/"48CHANGELOGS_POOL = "http://changelogs.ubuntu.com/changelogs/pool/"
@@ -133,59 +132,6 @@
133 self._depcache.upgrade()132 self._depcache.upgrade()
134 return wouldDelete133 return wouldDelete
135134
136 def match_package_origin(self, pkg, matcher):
137 """ match 'pkg' origin against 'matcher', take versions between
138 installed.version and candidate.version into account too
139 Useful if installed pkg A v1.0 is available in both
140 -updates (as v1.2) and -security (v1.1). we want to display
141 it as a security update then
142 """
143 inst_ver = pkg._pkg.current_ver
144 cand_ver = self._depcache.get_candidate_ver(pkg._pkg)
145 # init matcher with candidate.version
146 update_origin = matcher[(None, None)]
147 verFileIter = None
148 for (verFileIter, index) in cand_ver.file_list:
149 if (verFileIter.archive, verFileIter.origin) in matcher:
150 indexfile = pkg._pcache._list.find_index(verFileIter)
151 if indexfile: # and indexfile.IsTrusted:
152 match = matcher[verFileIter.archive, verFileIter.origin]
153 update_origin = match
154 break
155 else:
156 # add a node for each origin/archive combination
157 if verFileIter and verFileIter.origin and verFileIter.archive:
158 matcher[verFileIter.archive, verFileIter.origin] = \
159 UpdateOrigin(_("Other updates (%s)") % verFileIter.origin,
160 0)
161 update_origin = matcher[verFileIter.archive,
162 verFileIter.origin]
163 # if the candidate comes from a unknown source (e.g. a PPA) skip
164 # skip the shadow logic below as it would put e.g. a PPA package
165 # in "Recommended updates" when the version in the PPA
166 # is higher than the one in %s-updates
167 if update_origin.importance <= 0:
168 return update_origin
169 # for known packages, check if we have higher versions that
170 # "shadow" this one
171 for ver in pkg._pkg.version_list:
172 # discard is < than installed ver
173 if (inst_ver and
174 apt_pkg.version_compare(ver.ver_str,
175 inst_ver.ver_str) <= 0):
176 #print("skipping '%s' " % ver.ver_str)
177 continue
178 # check if we have a match
179 for (verFileIter, index) in ver.file_list:
180 if (verFileIter.archive, verFileIter.origin) in matcher:
181 indexfile = pkg._pcache._list.find_index(verFileIter)
182 if indexfile: # and indexfile.IsTrusted:
183 match = matcher[verFileIter.archive,
184 verFileIter.origin]
185 if match.importance > update_origin.importance:
186 update_origin = match
187 return update_origin
188
189 def _strip_epoch(self, verstr):135 def _strip_epoch(self, verstr):
190 " strip of the epoch "136 " strip of the epoch "
191 l = verstr.split(":")137 l = verstr.split(":")
@@ -356,9 +302,10 @@
356 def get_changelog(self, name):302 def get_changelog(self, name):
357 " get the changelog file from the changelog location "303 " get the changelog file from the changelog location "
358 origins = self[name].candidate.origins304 origins = self[name].candidate.origins
359 self.all_changes[name] = _("Changes for the versions:\n"305 self.all_changes[name] = _("Changes for %s versions:\n"
360 "Installed version: %s\n"306 "Installed version: %s\n"
361 "Available version: %s\n\n") % (307 "Available version: %s\n\n") % (
308 name,
362 getattr(self[name].installed, "version",309 getattr(self[name].installed, "version",
363 None),310 None),
364 self[name].candidate.version)311 self[name].candidate.version)
365312
=== modified file 'UpdateManager/Core/UpdateList.py'
--- UpdateManager/Core/UpdateList.py 2013-01-22 17:59:35 +0000
+++ UpdateManager/Core/UpdateList.py 2013-01-23 15:48:20 +0000
@@ -1,9 +1,11 @@
1# UpdateList.py1# UpdateList.py
2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
3#3#
4# Copyright (c) 2004-2012 Canonical4# Copyright (c) 2004-2013 Canonical
5#5#
6# Author: Michael Vogt <mvo@debian.org>6# Author: Michael Vogt <mvo@debian.org>
7# Dylan McCall <dylanmccall@ubuntu.com>
8# Michael Terry <michael.terry@canonical.com>
7#9#
8# This program is free software; you can redistribute it and/or10# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License as11# modify it under the terms of the GNU General Public License as
@@ -22,36 +24,131 @@
2224
23from __future__ import print_function25from __future__ import print_function
2426
27import warnings
28warnings.filterwarnings("ignore", "Accessed deprecated property",
29 DeprecationWarning)
30
25from gettext import gettext as _31from gettext import gettext as _
26
27import apt32import apt
28import logging33import logging
29import operator34import operator
35import itertools
36import platform
37import subprocess
38import os
30import random39import random
31import subprocess40import glob
3241
3342from gi.repository import Gio
34class UpdateOrigin(object):43
44from UpdateManager.Core import utils
45
46
47class UpdateItem():
48 def __init__(self, pkg, name, icon):
49 self.icon = icon
50 self.name = name
51 self.pkg = pkg
52
53
54class UpdateGroup(UpdateItem):
55 def __init__(self, pkg, name, icon):
56 UpdateItem.__init__(self, pkg, name, icon)
57 self._items = set()
58 self.core_item = None
59 if pkg is not None:
60 self.core_item = UpdateItem(pkg, name, icon)
61 self._items.add(self.core_item)
62
63 @property
64 def items(self):
65 all_items = []
66 all_items.extend(self._items)
67 return sorted(all_items, key=lambda a: a.name.lower())
68
69 def add(self, pkg):
70 name = utils.get_package_label(pkg)
71 icon = Gio.ThemedIcon.new("package")
72 self._items.add(UpdateItem(pkg, name, icon))
73
74 def contains(self, item):
75 return item in self._items
76
77 def _is_dependency_helper(self, cache, pkg, dep, seen=set()):
78 if pkg is None or pkg.candidate is None or pkg in seen:
79 return False
80 elif pkg.name == dep.name:
81 return True
82 seen.add(pkg)
83 candidate = pkg.candidate
84 dependencies = candidate.get_dependencies('Depends', 'Recommends')
85 for dependency_pkg in itertools.chain.from_iterable(dependencies):
86 if dependency_pkg.name in cache and \
87 self._is_dependency_helper(cache, cache[dependency_pkg.name],
88 dep, seen):
89 return True
90 return False
91
92 def is_dependency(self, cache, maybe_dep):
93 # This is a recursive dependency check. TODO: We do this many times
94 # when grouping packages, and it could be made more efficient.
95 seen = set()
96 for item in self._items:
97 if self._is_dependency_helper(cache, item.pkg, maybe_dep,
98 seen=seen):
99 return True
100 return False
101
102 def packages_are_selected(self):
103 for item in self.items:
104 if item.pkg.marked_install or item.pkg.marked_upgrade:
105 return True
106 return False
107
108 def selection_is_inconsistent(self):
109 pkgs_installing = [item for item in self.items
110 if item.pkg.marked_install or item.pkg.marked_upgrade]
111 return (len(pkgs_installing) > 0 and
112 len(pkgs_installing) < len(self.items))
113
114 def get_total_size(self):
115 size = 0
116 for item in self.items:
117 size += getattr(item.pkg.candidate, "size", 0)
118 return size
119
120
121class UpdateApplicationGroup(UpdateGroup):
122 def __init__(self, pkg, application):
123 name = application.get_display_name()
124 icon = application.get_icon()
125 super(UpdateApplicationGroup, self).__init__(pkg, name, icon)
126
127
128class UpdatePackageGroup(UpdateGroup):
129 def __init__(self, pkg):
130 name = utils.get_package_label(pkg)
131 icon = Gio.ThemedIcon.new("package")
132 super(UpdatePackageGroup, self).__init__(pkg, name, icon)
133
134
135class UpdateSystemGroup(UpdateGroup):
136 def __init__(self, cache):
137 # Translators: the %s is a distro name, like 'Ubuntu' and 'base' as in
138 # the core components and packages.
139 name = _("%s base") % utils.get_ubuntu_flavor_name(cache=cache)
140 icon = Gio.ThemedIcon.new("distributor-logo")
141 super(UpdateSystemGroup, self).__init__(None, name, icon)
142
143
144class UpdateOrigin():
35 def __init__(self, desc, importance):145 def __init__(self, desc, importance):
36 self.packages = []146 self.packages = []
37 self.importance = importance147 self.importance = importance
38 self.description = desc148 self.description = desc
39149
40150
41class OriginsImportance:151class UpdateList():
42 # filled in by us
43 SECURITY = 10
44 UPDATES = 9
45 PROPOSED = 8
46 BACKPORTS = 7
47 ARCHIVE = 6
48 # this is filled in by MyCache
49 OTHER = 0
50 # this is used by us
51 OTHER_UNKNOWN = -1
52
53
54class UpdateList(object):
55 """152 """
56 class that contains the list of available updates in153 class that contains the list of available updates in
57 self.pkgs[origin] where origin is the user readable string154 self.pkgs[origin] where origin is the user readable string
@@ -64,6 +161,8 @@
64 # the file that contains the uniq machine id161 # the file that contains the uniq machine id
65 UNIQ_MACHINE_ID_FILE = "/var/lib/dbus/machine-id"162 UNIQ_MACHINE_ID_FILE = "/var/lib/dbus/machine-id"
66163
164 APP_INSTALL_PATH = "/usr/share/app-install/desktop"
165
67 # the configuration key to turn phased-updates always on166 # the configuration key to turn phased-updates always on
68 ALWAYS_INCLUDE_PHASED_UPDATES = (167 ALWAYS_INCLUDE_PHASED_UPDATES = (
69 "Update-Manager::Always-Include-Phased-Updates")168 "Update-Manager::Always-Include-Phased-Updates")
@@ -71,47 +170,111 @@
71 NEVER_INCLUDE_PHASED_UPDATES = (170 NEVER_INCLUDE_PHASED_UPDATES = (
72 "Update-Manager::Never-Include-Phased-Updates")171 "Update-Manager::Never-Include-Phased-Updates")
73172
74 def __init__(self, parent):173 def __init__(self, parent, dist=None):
75 # a map of packages under their origin174 self.dist = dist if dist else platform.dist()[2]
76 try:
77 dist = subprocess.check_output(
78 ["lsb_release", "-c", "-s"], universal_newlines=True).strip()
79 except subprocess.CalledProcessError as e:
80 print("Error in lsb_release: %s" % e)
81 dist = None
82 self.distUpgradeWouldDelete = 0175 self.distUpgradeWouldDelete = 0
83 self.pkgs = {}176 self.update_groups = []
177 self.security_groups = []
84 self.num_updates = 0178 self.num_updates = 0
85 self.matcher = self.initMatcher(dist)
86 self.random = random.Random()179 self.random = random.Random()
87 # a stable machine uniq id180 # a stable machine uniq id
88 with open(self.UNIQ_MACHINE_ID_FILE) as f:181 with open(self.UNIQ_MACHINE_ID_FILE) as f:
89 self.machine_uniq_id = f.read()182 self.machine_uniq_id = f.read()
90183
91 def initMatcher(self, dist):184 if 'XDG_DATA_DIRS' in os.environ and os.environ['XDG_DATA_DIRS']:
92 # (origin, archive, description, importance)185 data_dirs = os.environ['XDG_DATA_DIRS']
93 matcher_templates = [186 else:
94 (None, None, _("Other updates"), OriginsImportance.OTHER_UNKNOWN)187 data_dirs= '/usr/local/share/:/usr/share/'
95 ]188 self.application_dirs = [os.path.join(base, 'applications')
96 if dist:189 for base in data_dirs.split(':')]
97 matcher_templates += [190
98 ("%s-security" % dist, "Ubuntu",191 if 'XDG_CURRENT_DESKTOP' in os.environ:
99 _("Important security updates"), OriginsImportance.SECURITY),192 self.current_desktop = os.environ.get('XDG_CURRENT_DESKTOP')
100 ("%s-updates" % dist, "Ubuntu",193 else:
101 _("Recommended updates"), OriginsImportance.UPDATES),194 self.current_desktop = ''
102 ("%s-proposed" % dist, "Ubuntu",195
103 _("Proposed updates"), OriginsImportance.PROPOSED),196 def _file_is_application(self, file_path):
104 ("%s-backports" % dist, "Ubuntu",197 file_path = os.path.abspath(file_path)
105 _("Backports"), OriginsImportance.BACKPORTS),198 is_application = False
106 (dist, "Ubuntu",199 for app_dir in self.application_dirs:
107 _("Distribution updates"), OriginsImportance.ARCHIVE),200 is_application = is_application or file_path.startswith(app_dir)
108 ]201 extension = os.path.splitext(file_path)[1]
109 matcher = {}202 is_application = is_application and (extension == '.desktop')
110 for (origin, archive, desc, importance) in matcher_templates:203 return is_application
111 matcher[(origin, archive)] = UpdateOrigin(desc, importance)204
112 return matcher205 def _rate_application_for_package(self, application, pkg):
113206 score = 0
114 def is_ignored_phased_update(self, pkg):207 desktop_file = os.path.basename(application.get_filename())
208 application_id = os.path.splitext(desktop_file)[0]
209
210 if application.should_show():
211 score += 1
212
213 if application_id == pkg.name:
214 score += 5
215
216 return score
217
218 def _get_application_for_package(self, pkg):
219 desktop_files = []
220 rated_applications = []
221
222 for installed_file in pkg.installed_files:
223 if self._file_is_application(installed_file):
224 desktop_files.append(installed_file)
225
226 app_install_pattern = os.path.join(self.APP_INSTALL_PATH,
227 '%s:*' % pkg.name)
228 for desktop_file in glob.glob(app_install_pattern):
229 desktop_files.append(desktop_file)
230
231 for desktop_file in desktop_files:
232 try:
233 application = Gio.DesktopAppInfo.new_from_filename(
234 desktop_file)
235 application.set_desktop_env(self.current_desktop)
236 except Exception as e:
237 print("Error loading .desktop file %s: %s" %
238 (installed_file, e))
239 continue
240 score = self._rate_application_for_package(application, pkg)
241 if score > 0:
242 rated_applications.append((score, application))
243
244 rated_applications.sort(key=lambda app: app[0], reverse=True)
245 if len(rated_applications) > 0:
246 return rated_applications[0][1]
247 else:
248 return None
249
250 def _is_security_update(self, pkg):
251 """ This will test if the pkg is a security update.
252 This includes if there is a newer version in -updates, but also
253 an older update available in -security. For example, if
254 installed pkg A v1.0 is available in both -updates (as v1.2) and
255 -security (v1.1). we want to display it as a security update.
256
257 :return: True if the update comes from the security pocket
258 """
259 if not self.dist:
260 return False
261 inst_ver = pkg._pkg.current_ver
262 for ver in pkg._pkg.version_list:
263 # discard is < than installed ver
264 if (inst_ver and
265 apt.apt_pkg.version_compare(ver.ver_str,
266 inst_ver.ver_str) <= 0):
267 continue
268 # check if we have a match
269 for (verFileIter, index) in ver.file_list:
270 if verFileIter.archive == "%s-security" % self.dist and \
271 verFileIter.origin == "Ubuntu":
272 indexfile = pkg._pcache._list.find_index(verFileIter)
273 if indexfile: # and indexfile.IsTrusted:
274 return True
275 return False
276
277 def _is_ignored_phased_update(self, pkg):
115 """ This will test if the pkg is a phased update and if278 """ This will test if the pkg is a phased update and if
116 it needs to get installed or ignored.279 it needs to get installed or ignored.
117280
@@ -142,15 +305,88 @@
142 return True305 return True
143 return False306 return False
144307
308 def _get_linux_packages(self):
309 "Return all binary packages made by the linux-meta source package"
310 # Hard code this rather than generate from source info in cache because
311 # that might only be available if we have deb-src lines. I think we
312 # could also generate it by iterating over all the binary package info
313 # we have, but that is costly. These don't change often.
314 return ['linux', 'linux-image', 'linux-headers-generic',
315 'linux-image-generic', 'linux-generic',
316 'linux-headers-generic-pae', 'linux-image-generic-pae',
317 'linux-generic-pae', 'linux-headers-omap', 'linux-image-omap',
318 'linux-omap', 'linux-headers-server', 'linux-image-server',
319 'linux-server', 'linux-signed-image-generic',
320 'linux-signed-generic', 'linux-headers-virtual',
321 'linux-image-virtual', 'linux-virtual',
322 'linux-image-extra-virtual']
323
324 def _make_groups(self, cache, pkgs):
325 pkgs_by_source = {}
326 ungrouped_pkgs = []
327 app_groups = []
328 pkg_groups = []
329
330 # Index packages by source package name
331 for pkg in pkgs:
332 srcpkg = pkg.candidate.source_name
333 pkgs_by_source.setdefault(srcpkg, []).append(pkg)
334
335 for srcpkg, pkgs in pkgs_by_source.items():
336 for pkg in pkgs:
337 app = self._get_application_for_package(pkg)
338 if app is not None:
339 app_group = UpdateApplicationGroup(pkg, app)
340 app_groups.append(app_group)
341 else:
342 ungrouped_pkgs.append(pkg)
343
344 # Stick together applications and their immediate dependencies
345 for pkg in list(ungrouped_pkgs):
346 dep_groups = []
347 for group in app_groups:
348 if group.is_dependency(cache, pkg):
349 dep_groups.append(group)
350 if len(dep_groups) > 1:
351 break
352 if len(dep_groups) == 1:
353 dep_groups[0].add(pkg)
354 ungrouped_pkgs.remove(pkg)
355
356 # Separate out system base packages
357 system_group = None
358 meta_group = UpdateGroup(None, None, None)
359 flavor_package = utils.get_ubuntu_flavor_package(cache=cache)
360 meta_pkgs = [flavor_package, "ubuntu-standard", "ubuntu-minimal"]
361 meta_pkgs.extend(self._get_linux_packages())
362 for pkg in meta_pkgs:
363 if pkg in cache:
364 meta_group.add(cache[pkg])
365 for pkg in ungrouped_pkgs:
366 if meta_group.contains(pkg) or meta_group.is_dependency(cache, pkg):
367 if system_group is None:
368 system_group = UpdateSystemGroup(cache)
369 system_group.add(pkg)
370 else:
371 pkg_groups.append(UpdatePackageGroup(pkg))
372
373 app_groups.sort(key=lambda a: a.name.lower())
374 pkg_groups.sort(key=lambda a: a.name.lower())
375 if system_group:
376 pkg_groups.append(system_group)
377
378 return app_groups + pkg_groups
379
145 def update(self, cache):380 def update(self, cache):
146 self.held_back = []381 self.held_back = []
147382
148 # do the upgrade383 # do the upgrade
149 self.distUpgradeWouldDelete = cache.saveDistUpgrade()384 self.distUpgradeWouldDelete = cache.saveDistUpgrade()
150385
151 #dselect_upgrade_origin = UpdateOrigin(_("Previous selected"), 1)386 security_pkgs = []
387 upgrade_pkgs = []
152388
153 # sort by origin389 # Find all upgradable packages
154 for pkg in cache:390 for pkg in cache:
155 if pkg.is_upgradable or pkg.marked_install:391 if pkg.is_upgradable or pkg.marked_install:
156 if getattr(pkg.candidate, "origins", None) is None:392 if getattr(pkg.candidate, "origins", None) is None:
@@ -159,22 +395,22 @@
159 print("WARNING: upgradable but no candidate.origins?!?: ",395 print("WARNING: upgradable but no candidate.origins?!?: ",
160 pkg.name)396 pkg.name)
161 continue397 continue
162 # check where the package belongs
163 origin_node = cache.match_package_origin(pkg, self.matcher)
164398
165 # see if its a phased update and *not* a security update399 # see if its a phased update and *not* a security update
166 # or shadowing a security update400 is_security_update = self._is_security_update(pkg)
167 if (origin_node.importance != OriginsImportance.SECURITY and401 if (not is_security_update and
168 self.is_ignored_phased_update(pkg)):402 self._is_ignored_phased_update(pkg)):
169 continue403 continue
170404
171 if origin_node not in self.pkgs:405 if is_security_update:
172 self.pkgs[origin_node] = []406 security_pkgs.append(pkg)
173 self.pkgs[origin_node].append(pkg)407 else:
408 upgrade_pkgs.append(pkg)
174 self.num_updates = self.num_updates + 1409 self.num_updates = self.num_updates + 1
410
175 if pkg.is_upgradable and not (pkg.marked_upgrade or411 if pkg.is_upgradable and not (pkg.marked_upgrade or
176 pkg.marked_install):412 pkg.marked_install):
177 self.held_back.append(pkg.name)413 self.held_back.append(pkg.name)
178 for l in self.pkgs.keys():414
179 self.pkgs[l].sort(key=operator.attrgetter("name"))415 self.update_groups = self._make_groups(cache, upgrade_pkgs)
180 self.keepcount = cache._depcache.keep_count416 self.security_groups = self._make_groups(cache, security_pkgs)
181417
=== modified file 'UpdateManager/Core/utils.py'
--- UpdateManager/Core/utils.py 2012-12-14 23:03:19 +0000
+++ UpdateManager/Core/utils.py 2013-01-23 15:48:20 +0000
@@ -1,9 +1,10 @@
1# utils.py1# utils.py
2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
3#3#
4# Copyright (c) 2004-2008 Canonical4# Copyright (c) 2004-2013 Canonical
5#5#
6# Author: Michael Vogt <mvo@debian.org>6# Authors: Michael Vogt <mvo@debian.org>
7# Michael Terry <michael.terry@canonical.com>
7#8#
8# This program is free software; you can redistribute it and/or9# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License as10# modify it under the terms of the GNU General Public License as
@@ -27,6 +28,7 @@
27from stat import (S_IMODE, ST_MODE, S_IXUSR)28from stat import (S_IMODE, ST_MODE, S_IXUSR)
28from math import ceil29from math import ceil
2930
31import apt
30import apt_pkg32import apt_pkg
31apt_pkg.init_config()33apt_pkg.init_config()
3234
@@ -58,6 +60,7 @@
58 from urlparse import urlsplit60 from urlparse import urlsplit
5961
60from copy import copy62from copy import copy
63from gi.repository import GLib
6164
6265
63class ExecutionTime(object):66class ExecutionTime(object):
@@ -402,36 +405,56 @@
402 return None405 return None
403406
404407
405def get_ubuntu_flavor():408def get_ubuntu_flavor(cache=None):
406 """ try to guess the flavor based on the running desktop """409 """ try to guess the flavor based on the running desktop """
407 # this will (of course) not work in a server environment,410 # this will (of course) not work in a server environment,
408 # but the main use case for this is to show the right411 # but the main use case for this is to show the right
409 # release notes412 # release notes.
410 # TODO: actually examine which meta packages are installed, like413 pkg = get_ubuntu_flavor_package(cache=cache)
411 # DistUpgrade/DistUpgradeCache.py does and use that to choose a flavor.414 return pkg.split('-', 1)[0]
412 denv = os.environ.get("DESKTOP_SESSION", "")415
413 if "gnome" in denv:416
414 return "ubuntu"417def _load_meta_pkg_list():
415 elif "kde" in denv:418 # This could potentially introduce a circular dependency, but the config
416 return "kubuntu"419 # parser logic is simple, and doesn't rely on any UpdateManager code.
417 elif "xfce" in denv or "xubuntu" in denv:420 from DistUpgrade.DistUpgradeConfigParser import DistUpgradeConfig
418 return "xubuntu"421 parser = DistUpgradeConfig('/usr/share/ubuntu-release-upgrader')
419 elif "LXDE" in denv or "Lubuntu" in denv:422 return parser.getlist('Distro', 'MetaPkgs')
420 return "lubuntu"423
421 # default to ubuntu if nothing more specific is found424
422 return "ubuntu"425def get_ubuntu_flavor_package(cache=None):
423426 """ try to guess the flavor metapackage based on the running desktop """
424427 # From spec, first if ubuntu-desktop is installed, use that.
425def get_ubuntu_flavor_name():428 # Second, grab first installed one from DistUpgrade.cfg.
426 flavor = get_ubuntu_flavor()429 # Lastly, fallback to ubuntu-desktop again.
427 if flavor == "kubuntu":430 meta_pkgs = ['ubuntu-desktop']
428 return "Kubuntu"431
429 elif flavor == "xubuntu":432 try:
430 return "Xubuntu"433 meta_pkgs.extend(sorted(_load_meta_pkg_list()))
431 elif flavor == "lubuntu":434 except Exception as e:
432 return "Lubuntu"435 print('Could not load list of meta packages:', e)
436
437 if cache is None:
438 cache = apt.Cache()
439 for meta_pkg in meta_pkgs:
440 cache_pkg = cache[meta_pkg] if meta_pkg in cache else None
441 if cache_pkg and cache_pkg.is_installed:
442 return meta_pkg
443 return 'ubuntu-desktop'
444
445
446def get_ubuntu_flavor_name(cache=None):
447 """ try to guess the flavor name based on the running desktop """
448 pkg = get_ubuntu_flavor_package(cache=cache)
449 lookup = {'ubuntustudio-desktop': 'Ubuntu Studio'}
450 if pkg in lookup:
451 return lookup[pkg]
452 elif pkg.endswith('-desktop'):
453 return capitalize_first_word(pkg.rsplit('-desktop', 1)[0])
454 elif pkg.endswith('-netbook'):
455 return capitalize_first_word(pkg.rsplit('-netbook', 1)[0])
433 else:456 else:
434 return "Ubuntu"457 return 'Ubuntu'
435458
436459
437# Unused by update-manager, but still used by ubuntu-release-upgrader460# Unused by update-manager, but still used by ubuntu-release-upgrader
@@ -523,6 +546,22 @@
523 return True546 return True
524547
525548
549def capitalize_first_word(string):
550 """ this uppercases the first word's first letter
551 """
552 if len(string) > 1 and string[0].isalpha() and not string[0].isupper():
553 return string[0].capitalize() + string[1:]
554 return string
555
556
557def get_package_label(pkg):
558 """ this takes a package synopsis and uppercases the first word's
559 first letter
560 """
561 name = GLib.markup_escape_text(getattr(pkg.candidate, "summary", ""))
562 return capitalize_first_word(name)
563
564
526if __name__ == "__main__":565if __name__ == "__main__":
527 #print(mirror_from_sources_list())566 #print(mirror_from_sources_list())
528 #print(on_battery())567 #print(on_battery())
529568
=== modified file 'UpdateManager/UpdatesAvailable.py'
--- UpdateManager/UpdatesAvailable.py 2012-12-13 20:39:37 +0000
+++ UpdateManager/UpdatesAvailable.py 2013-01-23 15:48:20 +0000
@@ -1,7 +1,7 @@
1# UpdatesAvailable.py1# UpdatesAvailable.py
2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
3#3#
4# Copyright (c) 2004-2012 Canonical4# Copyright (c) 2004-2013 Canonical
5# 2004 Michiel Sikkes5# 2004 Michiel Sikkes
6# 2005 Martin Willemoes Hansen6# 2005 Martin Willemoes Hansen
7# 2010 Mohamed Amine IL Idrissi7# 2010 Mohamed Amine IL Idrissi
@@ -12,6 +12,7 @@
12# Mohamed Amine IL Idrissi <ilidrissiamine@gmail.com>12# Mohamed Amine IL Idrissi <ilidrissiamine@gmail.com>
13# Alex Launi <alex.launi@canonical.com>13# Alex Launi <alex.launi@canonical.com>
14# Michael Terry <michael.terry@canonical.com>14# Michael Terry <michael.terry@canonical.com>
15# Dylan McCall <dylanmccall@ubuntu.com>
15#16#
16# This program is free software; you can redistribute it and/or17# This program is free software; you can redistribute it and/or
17# modify it under the terms of the GNU General Public License as18# modify it under the terms of the GNU General Public License as
@@ -30,6 +31,7 @@
3031
31from __future__ import absolute_import, print_function32from __future__ import absolute_import, print_function
3233
34from gi.repository import GLib
33from gi.repository import Gtk35from gi.repository import Gtk
34from gi.repository import Gdk36from gi.repository import Gdk
35from gi.repository import GObject37from gi.repository import GObject
@@ -46,18 +48,17 @@
46import os48import os
47import re49import re
48import logging50import logging
49import operator
50import subprocess51import subprocess
51import time52import time
52import threading53import threading
53import xml.sax.saxutils
5454
55from gettext import gettext as _55from gettext import gettext as _
56from gettext import ngettext56from gettext import ngettext
5757
5858
59from .Core.utils import humanize_size59from .Core.utils import (get_package_label, humanize_size)
60from .Core.AlertWatcher import AlertWatcher60from .Core.AlertWatcher import AlertWatcher
61from .Core.UpdateList import UpdateSystemGroup
6162
62from DistUpgrade.DistUpgradeCache import NotEnoughFreeSpaceError63from DistUpgrade.DistUpgradeCache import NotEnoughFreeSpaceError
6364
@@ -70,15 +71,126 @@
7071
71# FIXME:72# FIXME:
72# - kill "all_changes" and move the changes into the "Update" class73# - kill "all_changes" and move the changes into the "Update" class
74# - screen reader does not read update toggle state
75# - screen reader does not say "Downloaded" for downloaded updates
7376
74# list constants77# list constants
75(LIST_CONTENTS, LIST_NAME, LIST_PKG,78(LIST_NAME, LIST_UPDATE_DATA, LIST_SIZE, LIST_TOGGLE_ACTIVE) = range(4)
76 LIST_ORIGIN, LIST_TOGGLE_CHECKED) = range(5)
7779
78# NetworkManager enums80# NetworkManager enums
79from .Core.roam import NetworkManagerHelper81from .Core.roam import NetworkManagerHelper
8082
8183
84class UpdateData():
85 def __init__(self, groups, group, item):
86 self.groups = groups if groups else []
87 self.group = group
88 self.item = item
89
90
91class CellAreaPackage(Gtk.CellAreaBox):
92 """This CellArea lays our package cells side by side, without allocating
93 width for a cell if it isn't present (like icons for header labels).
94 """
95
96 def __init__(self, indent_toplevel=False):
97 Gtk.CellAreaBox.__init__(self)
98 self.indent_toplevel = indent_toplevel
99 self.column = None
100 self.toggle_size = None
101 self.pixbuf_size = None
102 self.cached_cell_start = {}
103
104 def do_foreach_alloc(self, context, widget, cell_area_in, bg_area_in,
105 callback):
106 # First, gather cell renderers and make sure they are what we expect
107 cells = []
108 def gather(cell, data):
109 cells.append(cell)
110 self.foreach(gather, None)
111 if (len(cells) != 3 or
112 not isinstance(cells[0], Gtk.CellRendererToggle) or
113 not isinstance(cells[1], Gtk.CellRendererPixbuf) or
114 not isinstance(cells[2], Gtk.CellRendererText)):
115 return
116 toggle = cells[0]
117 pixbuf = cells[1]
118 text = cells[2]
119
120 # Now just grab some size info
121 cell_area = cell_area_in.copy()
122 bg_area = bg_area_in.copy()
123 spacing = self.get_property("spacing")
124 gicon = pixbuf.get_property("gicon")
125 cell_start = self.get_cell_start(widget)
126 orig_end = cell_area.width + cell_area.x
127 if self.toggle_size is None:
128 toggle_min, self.toggle_size = toggle.get_preferred_width(widget)
129 if gicon and self.pixbuf_size is None:
130 pixbuf_min, self.pixbuf_size = pixbuf.get_preferred_width(widget)
131
132 # And finally, start handling each cell
133
134 cur_path = self.get_current_path_string()
135 depth = Gtk.TreePath.new_from_string(cur_path).get_depth()
136 if gicon is not None and self.indent_toplevel:
137 # if not a header, align with header rows
138 depth = depth + 1
139 if depth == 1:
140 cell_area.x = cell_start
141 elif depth == 2:
142 cell_area.x = cell_start + self.toggle_size + spacing
143 elif depth == 3:
144 # Oddly, cells don't line up if we simply use spacing * 2
145 cell_area.x = cell_start + self.toggle_size * 2 + spacing + 1
146 cell_area.width = self.toggle_size
147 if callback(cells[0], cell_area.copy(), bg_area.copy()):
148 return
149
150 cell_area.x = cell_area.x + cell_area.width + spacing
151 if gicon is None:
152 cell_area.width = 0
153 else:
154 cell_area.width = self.pixbuf_size
155 if callback(cells[1], cell_area.copy(), bg_area.copy()):
156 return
157
158 if gicon is not None:
159 cell_area.x = cell_area.x + cell_area.width + spacing
160 cell_area.width = orig_end - cell_area.x
161 if callback(cells[2], cell_area.copy(), bg_area.copy()):
162 return
163
164 def do_event(self, context, widget, event, cell_area, flags):
165 # This override is just to trick our parent implementation into
166 # allowing clicks on toggle cells when they are where the expanders
167 # usually are. It doesn't expect that, so we expand the cell_area
168 # here to be equivalent to bg_area.
169 cell_start = self.get_cell_start(widget)
170 cell_area.width = cell_area.width + cell_area.x - cell_start
171 cell_area.x = cell_start
172 return Gtk.CellAreaBox.do_event(self, context, widget, event,
173 cell_area, flags)
174
175 def get_cell_start(self, widget):
176 if not self.column:
177 return 0
178 else:
179 val = GObject.Value()
180 val.init(int)
181 widget.style_get_property("horizontal-separator", val)
182 h_sep = val.get_int()
183 widget.style_get_property("grid-line-width", val)
184 line_width = val.get_int()
185 cell_start = self.column.get_x_offset() - h_sep - line_width
186 if not self.indent_toplevel: # i.e. if no headers
187 widget.style_get_property("expander-size", val)
188 spacing = self.get_property("spacing")
189 # Hardcode 4 because GTK+ hardcodes 4 internally
190 cell_start = cell_start + val.get_int() + 4 + spacing
191 return cell_start
192
193
82class UpdatesAvailable(SimpleGtkbuilderApp):194class UpdatesAvailable(SimpleGtkbuilderApp):
83195
84 def __init__(self, app, header=None, desc=None):196 def __init__(self, app, header=None, desc=None):
@@ -134,38 +246,71 @@
134 self.update_close_button()246 self.update_close_button()
135247
136 # the treeview (move into it's own code!)248 # the treeview (move into it's own code!)
137 self.store = Gtk.ListStore(str, str, GObject.TYPE_PYOBJECT,249 self.store = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT, str, bool)
138 GObject.TYPE_PYOBJECT, bool)
139 self.treeview_update.set_model(self.store)250 self.treeview_update.set_model(self.store)
140 self.treeview_update.set_headers_clickable(True)251
252 restart_icon_renderer = Gtk.CellRendererPixbuf()
253 restart_icon_renderer.set_property("xpad", 4)
254 restart_icon_renderer.set_property("ypad", 2)
255 restart_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
256 restart_icon_renderer.set_property("follow-state", True)
257 restart_column = Gtk.TreeViewColumn(None, restart_icon_renderer)
258 restart_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
259 restart_column.set_fixed_width(20)
260 self.treeview_update.append_column(restart_column)
261 restart_column.set_cell_data_func(restart_icon_renderer,
262 self.restart_icon_renderer_data_func)
263
264 pkg_area = CellAreaPackage(bool(self.list.security_groups))
265 pkg_column = Gtk.TreeViewColumn.new_with_area(pkg_area)
266 pkg_area.column = pkg_column
267 pkg_column.set_title(_("Install"))
268 pkg_column.set_property("spacing", 4)
269 pkg_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
270 pkg_column.set_expand(True)
271 self.treeview_update.append_column(pkg_column)
272
273 pkg_toggle_renderer = Gtk.CellRendererToggle()
274 pkg_toggle_renderer.set_property("ypad", 2)
275 pkg_toggle_renderer.connect("toggled", self.on_update_toggled)
276 pkg_column.pack_start(pkg_toggle_renderer, False)
277 pkg_column.add_attribute(pkg_toggle_renderer,
278 'active', LIST_TOGGLE_ACTIVE)
279 pkg_column.set_cell_data_func(pkg_toggle_renderer,
280 self.pkg_toggle_renderer_data_func)
281
282 pkg_icon_renderer = Gtk.CellRendererPixbuf()
283 pkg_icon_renderer.set_property("ypad", 2)
284 pkg_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
285 pkg_column.pack_start(pkg_icon_renderer, False)
286 pkg_column.set_cell_data_func(pkg_icon_renderer,
287 self.pkg_icon_renderer_data_func)
288
289 pkg_label_renderer = Gtk.CellRendererText()
290 pkg_label_renderer.set_property("ypad", 2)
291 pkg_column.pack_start(pkg_label_renderer, True)
292 pkg_column.set_cell_data_func(pkg_label_renderer,
293 self.pkg_label_renderer_data_func)
294
295 size_renderer = Gtk.CellRendererText()
296 size_renderer.set_property("xpad", 6)
297 size_renderer.set_property("ypad", 0)
298 size_renderer.set_property("xalign", 1)
299 # 1.0/1.2 == PANGO.Scale.SMALL. Constant is not (yet) introspected.
300 size_renderer.set_property("scale", 1.0 / 1.2)
301 size_column = Gtk.TreeViewColumn(_("Download"), size_renderer,
302 text=LIST_SIZE)
303 size_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
304 self.treeview_update.append_column(size_column)
305
306 self.treeview_update.set_headers_visible(True)
307 self.treeview_update.set_headers_clickable(False)
141 self.treeview_update.set_direction(Gtk.TextDirection.LTR)308 self.treeview_update.set_direction(Gtk.TextDirection.LTR)
142
143 tr = Gtk.CellRendererText()
144 tr.set_property("xpad", 6)
145 tr.set_property("ypad", 6)
146 cr = Gtk.CellRendererToggle()
147 cr.set_property("activatable", True)
148 cr.set_property("xpad", 6)
149 cr.connect("toggled", self.toggled)
150
151 column_install = Gtk.TreeViewColumn(_("Install"), cr,
152 active=LIST_TOGGLE_CHECKED)
153 column_install.set_cell_data_func(cr, self.install_column_view_func)
154 column = Gtk.TreeViewColumn(_("Name"), tr, markup=LIST_CONTENTS)
155 column.set_resizable(True)
156
157 column_install.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
158 column_install.set_fixed_width(30)
159 column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
160 column.set_fixed_width(100)
161 self.treeview_update.set_fixed_height_mode(False)309 self.treeview_update.set_fixed_height_mode(False)
162310 self.treeview_update.set_expander_column(pkg_column)
163 self.treeview_update.append_column(column_install)
164 column_install.set_visible(True)
165 self.treeview_update.append_column(column)
166 self.treeview_update.set_search_column(LIST_NAME)311 self.treeview_update.set_search_column(LIST_NAME)
167 self.treeview_update.connect("button-press-event",312 self.treeview_update.connect("button-press-event",
168 self.show_context_menu)313 self.on_treeview_button_press)
169314
170 # setup the help viewer and disable the help button if there315 # setup the help viewer and disable the help button if there
171 # is no viewer available316 # is no viewer available
@@ -210,26 +355,97 @@
210 self.button_close.set_use_underline(False)355 self.button_close.set_use_underline(False)
211356
212 def install_all_updates(self, menu, menuitem, data):357 def install_all_updates(self, menu, menuitem, data):
213 self.select_all_updgrades(None)358 self.select_all_upgrades(None)
214 self.on_button_install_clicked(None)359 self.on_button_install_clicked(None)
215360
216 def install_column_view_func(self, cell_layout, renderer, model, iter,361 def restart_icon_renderer_data_func(self, cell_layout, renderer, model,
217 data):362 iter, data):
218 pkg = model.get_value(iter, LIST_PKG)363 def pkg_requires_restart(pkg):
219 if pkg is None:364 restart_condition = pkg.candidate.record.get('XB-Restart-Required')
220 renderer.set_property("activatable", True)365 return restart_condition == 'system'
221 return366
222 current_state = renderer.get_property("active")367 data = model.get_value(iter, LIST_UPDATE_DATA)
223 to_install = pkg.marked_install or pkg.marked_upgrade368 path = model.get_path(iter)
224 renderer.set_property("active", to_install)369
225 # we need to update the store as well to ensure orca knowns370 requires_restart = False
226 # about state changes (it will not read view_func changes)371 if data.item:
227 if to_install != current_state:372 requires_restart = pkg_requires_restart(data.item.pkg)
228 self.store[iter][LIST_TOGGLE_CHECKED] = to_install373 elif data.group:
229 if pkg.name in self.list.held_back:374 if not self.treeview_update.row_expanded(path):
230 renderer.set_property("activatable", False)375 # A package in the group requires restart
376 for group_item in data.group.items:
377 if pkg_requires_restart(group_item.pkg):
378 requires_restart = True
379 break
380
381 # FIXME: Non-standard, incorrect icon name (from app category).
382 # Theme support for what we want seems to be lacking.
383 if requires_restart:
384 restart_icon_names = ['view-refresh-symbolic',
385 'system-restart',
386 'system-reboot']
387 gicon = Gio.ThemedIcon.new_from_names(restart_icon_names)
231 else:388 else:
232 renderer.set_property("activatable", True)389 gicon = None
390 renderer.set_property("gicon", gicon)
391
392 def pkg_toggle_renderer_data_func(self, cell_layout, renderer, model, iter,
393 data):
394 data = model.get_value(iter, LIST_UPDATE_DATA)
395
396 activatable = False
397 inconsistent = False
398 if data.item:
399 activatable = data.item.name not in self.list.held_back
400 inconsistent = False
401 elif data.group:
402 activatable = True
403 inconsistent = data.group.selection_is_inconsistent()
404 elif data.groups:
405 activatable = True
406 inconsistent = False
407 saw_install = None
408 for group in data.groups:
409 for item in group.items:
410 pkg = item.pkg
411 this_install = pkg.marked_install or pkg.marked_upgrade
412 if saw_install is not None and saw_install != this_install:
413 inconsistent = True
414 break
415 saw_install = this_install
416 if inconsistent:
417 break
418
419 # The "active" attribute is already set via LIST_TOGGLE_ACTIVE in the
420 # tree model, so we don't set it here.
421 renderer.set_property("activatable", activatable)
422 renderer.set_property("inconsistent", inconsistent)
423
424 def pkg_icon_renderer_data_func(self, cell_layout, renderer, model, iter,
425 data):
426 data = model.get_value(iter, LIST_UPDATE_DATA)
427
428 gicon = None
429 if data.group:
430 gicon = data.group.icon
431 elif data.item:
432 gicon = data.item.icon
433
434 renderer.set_property("gicon", gicon)
435
436 def pkg_label_renderer_data_func(self, cell_layout, renderer, model, iter,
437 data):
438 data = model.get_value(iter, LIST_UPDATE_DATA)
439 name = model.get_value(iter, LIST_NAME)
440
441 if data.group:
442 markup = name
443 elif data.item:
444 markup = name
445 else: # header
446 markup = "<b>%s</b>" % name
447
448 renderer.set_property("markup", markup)
233449
234 def set_changes_buffer(self, changes_buffer, text, name, srcpkg):450 def set_changes_buffer(self, changes_buffer, text, name, srcpkg):
235 changes_buffer.set_text("")451 changes_buffer.set_text("")
@@ -264,16 +480,21 @@
264 iter = model.get_iter(path)480 iter = model.get_iter(path)
265481
266 # set descr482 # set descr
267 pkg = model.get_value(iter, LIST_PKG)483 data = model.get_value(iter, LIST_UPDATE_DATA)
268 if (pkg is None or pkg.candidate is None or484 item = data.item
269 pkg.candidate.description is None):485 if (item is None and data.group is not None and
486 data.group.core_item is not None):
487 item = data.group.core_item
488 if (item is None or item.pkg is None or
489 item.pkg.candidate is None or
490 item.pkg.candidate.description is None):
270 changes_buffer = self.textview_changes.get_buffer()491 changes_buffer = self.textview_changes.get_buffer()
271 changes_buffer.set_text("")492 changes_buffer.set_text("")
272 desc_buffer = self.textview_descr.get_buffer()493 desc_buffer = self.textview_descr.get_buffer()
273 desc_buffer.set_text("")494 desc_buffer.set_text("")
274 self.notebook_details.set_sensitive(False)495 self.notebook_details.set_sensitive(False)
275 return496 return
276 long_desc = pkg.candidate.description497 long_desc = item.pkg.candidate.description
277 self.notebook_details.set_sensitive(True)498 self.notebook_details.set_sensitive(True)
278 # do some regular expression magic on the description499 # do some regular expression magic on the description
279 # Add a newline before each bullet500 # Add a newline before each bullet
@@ -290,7 +511,7 @@
290 desc_buffer.set_text(long_desc)511 desc_buffer.set_text(long_desc)
291512
292 # now do the changelog513 # now do the changelog
293 name = model.get_value(iter, LIST_NAME)514 name = item.pkg.name
294 if name is None:515 if name is None:
295 return516 return
296517
@@ -334,11 +555,10 @@
334 button.disconnect(id)555 button.disconnect(id)
335 # check if we still are in the right pkg (the download may have taken556 # check if we still are in the right pkg (the download may have taken
336 # some time and the user may have clicked on a new pkg)557 # some time and the user may have clicked on a new pkg)
337 path = widget.get_cursor()[0]558 now_path = widget.get_cursor()[0]
338 if path is None:559 if now_path is None:
339 return560 return
340 now_name = widget.get_model()[path][LIST_NAME]561 if path != now_path:
341 if name != now_name:
342 return562 return
343 # display NEWS.Debian first, then the changelog563 # display NEWS.Debian first, then the changelog
344 changes = ""564 changes = ""
@@ -350,7 +570,7 @@
350 if changes:570 if changes:
351 self.set_changes_buffer(changes_buffer, changes, name, srcpkg)571 self.set_changes_buffer(changes_buffer, changes, name, srcpkg)
352572
353 def show_context_menu(self, widget, event):573 def on_treeview_button_press(self, widget, event):
354 """574 """
355 Show a context menu if a right click was performed on an update entry575 Show a context menu if a right click was performed on an update entry
356 """576 """
@@ -361,10 +581,13 @@
361 self.menu = menu = Gtk.Menu()581 self.menu = menu = Gtk.Menu()
362 item_select_none = \582 item_select_none = \
363 Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))583 Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))
364 item_select_none.connect("activate", self.select_none_updgrades)584 item_select_none.connect("activate", self.select_none_upgrades)
365 menu.append(item_select_none)585 menu.append(item_select_none)
586 num_updates = self.cache.install_count
587 if num_updates == 0:
588 item_select_none.set_property("sensitive", False)
366 item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))589 item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))
367 item_select_all.connect("activate", self.select_all_updgrades)590 item_select_all.connect("activate", self.select_all_upgrades)
368 menu.append(item_select_all)591 menu.append(item_select_all)
369 menu.show_all()592 menu.show_all()
370 menu.popup_for_device(593 menu.popup_for_device(
@@ -373,35 +596,36 @@
373 return True596 return True
374597
375 # we need this for select all/unselect all598 # we need this for select all/unselect all
376 def _toggle_origin_headers(self, new_selection_value):599 def _toggle_group_headers(self, new_selection_value):
377 """ small helper that will set/unset the origin headers600 """ small helper that will set/unset the group headers
378 """601 """
379 model = self.treeview_update.get_model()602 model = self.treeview_update.get_model()
380 for row in model:603 for row in model:
381 if not model.get_value(row.iter, LIST_PKG):604 data = model.get_value(row.iter, LIST_UPDATE_DATA)
382 model.set_value(row.iter, LIST_TOGGLE_CHECKED,605 if data.groups is not None or data.group is not None:
606 model.set_value(row.iter, LIST_TOGGLE_ACTIVE,
383 new_selection_value)607 new_selection_value)
384608
385 def select_all_updgrades(self, widget):609 def select_all_upgrades(self, widget):
386 """610 """
387 Select all updates611 Select all updates
388 """612 """
389 self.setBusy(True)613 self.setBusy(True)
390 self.cache.saveDistUpgrade()614 self.cache.saveDistUpgrade()
391 self._toggle_origin_headers(True)615 self._toggle_group_headers(True)
392 self.treeview_update.queue_draw()616 self.treeview_update.queue_draw()
393 self.refresh_updates_count()617 self.updates_changed()
394 self.setBusy(False)618 self.setBusy(False)
395619
396 def select_none_updgrades(self, widget):620 def select_none_upgrades(self, widget):
397 """621 """
398 Select none updates622 Select none updates
399 """623 """
400 self.setBusy(True)624 self.setBusy(True)
401 self.cache.clear()625 self.cache.clear()
402 self._toggle_origin_headers(False)626 self._toggle_group_headers(False)
403 self.treeview_update.queue_draw()627 self.treeview_update.queue_draw()
404 self.refresh_updates_count()628 self.updates_changed()
405 self.setBusy(False)629 self.setBusy(False)
406630
407 def setBusy(self, flag):631 def setBusy(self, flag):
@@ -417,7 +641,21 @@
417 while Gtk.events_pending():641 while Gtk.events_pending():
418 Gtk.main_iteration()642 Gtk.main_iteration()
419643
420 def refresh_updates_count(self):644 def _mark_selected_updates(self):
645 def foreach_cb(model, path, iter, data):
646 data = model.get_value(iter, LIST_UPDATE_DATA)
647 active = False
648 if data.item:
649 active = data.item.pkg.marked_install or \
650 data.item.pkg.marked_upgrade
651 elif data.group:
652 active = data.group.packages_are_selected()
653 elif data.groups:
654 active = any([g.packages_are_selected() for g in data.groups])
655 model.set_value(iter, LIST_TOGGLE_ACTIVE, active)
656 self.store.foreach(foreach_cb, None)
657
658 def _refresh_updates_count(self):
421 self.button_install.set_sensitive(self.cache.install_count)659 self.button_install.set_sensitive(self.cache.install_count)
422 try:660 try:
423 inst_count = self.cache.install_count661 inst_count = self.cache.install_count
@@ -458,10 +696,14 @@
458 self.hbox_downsize.show()696 self.hbox_downsize.show()
459 self.vbox_alerts.show()697 self.vbox_alerts.show()
460698
699 def updates_changed(self):
700 self._mark_selected_updates()
701 self._refresh_updates_count()
702
461 def update_count(self):703 def update_count(self):
462 """activate or disable widgets and show dialog texts correspoding to704 """activate or disable widgets and show dialog texts correspoding to
463 the number of available updates"""705 the number of available updates"""
464 self.refresh_updates_count()706 self.updates_changed()
465707
466 text_header = None708 text_header = None
467 text_desc = None709 text_desc = None
@@ -486,8 +728,6 @@
486 self.notebook_details.set_sensitive(True)728 self.notebook_details.set_sensitive(True)
487 self.treeview_update.set_sensitive(True)729 self.treeview_update.set_sensitive(True)
488 self.button_install.grab_default()730 self.button_install.grab_default()
489 self.treeview_update.set_cursor(Gtk.TreePath.new_from_string("1"),
490 None, False)
491 self.label_header.set_label(text_header)731 self.label_header.set_label(text_header)
492732
493 if text_desc is not None:733 if text_desc is not None:
@@ -573,14 +813,14 @@
573 # can deal with dialup connections properly813 # can deal with dialup connections properly
574 if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:814 if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:
575 self.label_offline.set_text(_("Connecting..."))815 self.label_offline.set_text(_("Connecting..."))
576 self.refresh_updates_count()816 self.updates_changed()
577 self.hbox_offline.show()817 self.hbox_offline.show()
578 self.vbox_alerts.show()818 self.vbox_alerts.show()
579 self.connected = False819 self.connected = False
580 # in doubt (STATE_UNKNOWN), assume connected820 # in doubt (STATE_UNKNOWN), assume connected
581 elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST or821 elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST or
582 state == NetworkManagerHelper.NM_STATE_UNKNOWN):822 state == NetworkManagerHelper.NM_STATE_UNKNOWN):
583 self.refresh_updates_count()823 self.updates_changed()
584 self.hbox_offline.hide()824 self.hbox_offline.hide()
585 self.connected = True825 self.connected = True
586 # trigger re-showing the current app to get changelog info (if826 # trigger re-showing the current app to get changelog info (if
@@ -590,7 +830,7 @@
590 self.connected = False830 self.connected = False
591 self.label_offline.set_text(_("You may not be able to check for "831 self.label_offline.set_text(_("You may not be able to check for "
592 "updates or download new updates."))832 "updates or download new updates."))
593 self.refresh_updates_count()833 self.updates_changed()
594 self.hbox_offline.show()834 self.hbox_offline.show()
595 self.vbox_alerts.show()835 self.vbox_alerts.show()
596836
@@ -613,63 +853,37 @@
613 self.hbox_on_3g.hide()853 self.hbox_on_3g.hide()
614 self.hbox_roaming.hide()854 self.hbox_roaming.hide()
615855
616 def row_activated(self, treeview, path, column):856 def on_update_toggled(self, renderer, path):
617 iter = self.store.get_iter(path)
618
619 pkg = self.store.get_value(iter, LIST_PKG)
620 origin = self.store.get_value(iter, LIST_ORIGIN)
621 if pkg is not None:
622 return
623 self.toggle_from_origin(pkg, origin, True)
624
625 def toggle_from_origin(self, pkg, origin, select_all=True):
626 self.setBusy(True)
627 actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
628 for pkg in self.list.pkgs[origin]:
629 if pkg.marked_install or pkg.marked_upgrade:
630 #print("marking keep: ", pkg.name)
631 pkg.mark_keep()
632 elif not (pkg.name in self.list.held_back):
633 #print("marking install: ", pkg.name)
634 pkg.mark_install(auto_fix=False, auto_inst=False)
635 # check if we left breakage
636 if self.cache._depcache.broken_count:
637 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
638 Fix.resolve_by_keep()
639 self.refresh_updates_count()
640 self.treeview_update.queue_draw()
641 del actiongroup
642 self.setBusy(False)
643
644 def toggled(self, renderer, path):
645 """ a toggle button in the listview was toggled """857 """ a toggle button in the listview was toggled """
646 iter = self.store.get_iter(path)858 iter = self.store.get_iter(path)
647 pkg = self.store.get_value(iter, LIST_PKG)859 data = self.store.get_value(iter, LIST_UPDATE_DATA)
648 origin = self.store.get_value(iter, LIST_ORIGIN)
649 # make sure that we don't allow to toggle deactivated updates860 # make sure that we don't allow to toggle deactivated updates
650 # this is needed for the call by the row activation callback861 # this is needed for the call by the row activation callback
651 if pkg is None:862 if data.groups or data.group:
652 toggled_value = not self.store.get_value(iter, LIST_TOGGLE_CHECKED)863 if data.groups:
653 self.toggle_from_origin(pkg, origin, toggled_value)864 self.toggle_from_groups(data.groups)
654 self.store.set_value(iter, LIST_TOGGLE_CHECKED, toggled_value)865 elif data.group:
866 self.toggle_from_groups([data.group])
867 self.updates_changed()
655 self.treeview_update.queue_draw()868 self.treeview_update.queue_draw()
656 return869 return
657 if pkg is None or pkg.name in self.list.held_back:870 if (data.item is None or data.item.pkg is None or
871 data.item.pkg.name in self.list.held_back):
658 return False872 return False
659 self.setBusy(True)873 self.setBusy(True)
660 # update the cache874 # update the cache
661 if pkg.marked_install or pkg.marked_upgrade:875 if data.item.pkg.marked_install or data.item.pkg.marked_upgrade:
662 pkg.mark_keep()876 data.item.pkg.mark_keep()
663 if self.cache._depcache.broken_count:877 if self.cache._depcache.broken_count:
664 Fix = apt_pkg.ProblemResolver(self.cache._depcache)878 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
665 Fix.resolve_by_keep()879 Fix.resolve_by_keep()
666 else:880 else:
667 try:881 try:
668 pkg.mark_install()882 data.item.pkg.mark_install()
669 except SystemError:883 except SystemError:
670 pass884 pass
885 self.updates_changed()
671 self.treeview_update.queue_draw()886 self.treeview_update.queue_draw()
672 self.refresh_updates_count()
673 self.setBusy(False)887 self.setBusy(False)
674888
675 def on_treeview_update_row_activated(self, treeview, path, column, *args):889 def on_treeview_update_row_activated(self, treeview, path, column, *args):
@@ -677,7 +891,29 @@
677 If an update row was activated (by pressing space), toggle the891 If an update row was activated (by pressing space), toggle the
678 install check box892 install check box
679 """893 """
680 self.toggled(None, path)894 self.on_update_toggled(None, path)
895
896 def toggle_from_groups(self, groups):
897 self.setBusy(True)
898 actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
899
900 # Deselect all updates if any in group are selected
901 keep_packages = any([g.packages_are_selected() for g in groups])
902 for group in groups:
903 for item in group.items:
904 if keep_packages:
905 item.pkg.mark_keep()
906 elif not item.pkg.name in self.list.held_back:
907 item.pkg.mark_install(auto_fix=False, auto_inst=False)
908
909 # check if we left breakage
910 if self.cache._depcache.broken_count:
911 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
912 Fix.resolve_by_keep()
913 self.updates_changed()
914 self.treeview_update.queue_draw()
915 del actiongroup
916 self.setBusy(False)
681917
682 def save_state(self):918 def save_state(self):
683 """ save the state (window-size for now) """919 """ save the state (window-size for now) """
@@ -693,9 +929,58 @@
693 expanded = self.expander_details.get_expanded()929 expanded = self.expander_details.get_expanded()
694 self.window_main.set_resizable(expanded)930 self.window_main.set_resizable(expanded)
695 if w > 0 and h > 0 and expanded:931 if w > 0 and h > 0 and expanded:
696 self.window_main.resize(w, h)932 # There is a race here. If we immediately resize, it often doesn't
933 # take. Using idle_add helps, but we *still* occasionally don't
934 # restore the size correctly. Help needed to track this down!
935 GLib.idle_add(lambda: self.window_main.resize(w, h))
697 return False936 return False
698937
938 def _add_header(self, name, groups):
939 total_size = 0
940 for group in groups:
941 total_size = total_size + group.get_total_size()
942 header_row = [
943 name,
944 UpdateData(groups, None, None),
945 humanize_size(total_size),
946 True
947 ]
948 return self.store.append(None, header_row)
949
950 def _add_groups(self, groups):
951 # Each row contains:
952 # row label (for screen reader),
953 # update data tuple (is_toplevel, group object, package object),
954 # update size,
955 # update selection state
956 for group in groups:
957 if not group.items:
958 continue
959
960 group_is_item = None
961 if not isinstance(group, UpdateSystemGroup) and \
962 len(group.items) == 1:
963 group_is_item = group.items[0]
964
965 group_row = [
966 group.name,
967 UpdateData(None, group, group_is_item),
968 humanize_size(group.get_total_size()),
969 True
970 ]
971 group_iter = self.store.append(None, group_row)
972
973 if group_is_item:
974 continue
975 for item in group.items:
976 item_row = [
977 item.name,
978 UpdateData(None, None, item),
979 humanize_size(getattr(item.pkg.candidate, "size", 0)),
980 True
981 ]
982 self.store.append(group_iter, item_row)
983
699 def fillstore(self):984 def fillstore(self):
700 # use the watch cursor985 # use the watch cursor
701 self.setBusy(True)986 self.setBusy(True)
@@ -706,46 +991,22 @@
706 self.dl_size = 0991 self.dl_size = 0
707992
708 self.scrolledwindow_update.show()993 self.scrolledwindow_update.show()
709 origin_list = sorted(994
710 self.list.pkgs, key=operator.attrgetter("importance"),995 # add security and update groups to self.store
711 reverse=True)996 if self.list.security_groups:
712 for origin in origin_list:997 self._add_header(_("Security updates"), self.list.security_groups)
713 self.store.append(['<b><big>%s</big></b>' % origin.description,998 self._add_groups(self.list.security_groups)
714 origin.description, None, origin, True])999 if self.list.security_groups and self.list.update_groups:
715 for pkg in self.list.pkgs[origin]:1000 self._add_header(_("Other updates"), self.list.update_groups)
716 name = xml.sax.saxutils.escape(pkg.name)1001 if self.list.update_groups:
717 if not pkg.is_installed:1002 self._add_groups(self.list.update_groups)
718 name += _(" (New install)")1003
719 summary = xml.sax.saxutils.escape(getattr(pkg.candidate,
720 "summary", ""))
721 if self.summary_before_name:
722 contents = "%s\n<small>%s</small>" % (summary, name)
723 else:
724 contents = "<b>%s</b>\n<small>%s</small>" % (name, summary)
725 #TRANSLATORS: the b stands for Bytes
726 size = _("(Size: %s)") % humanize_size(getattr(pkg.candidate,
727 "size", 0))
728 installed_version = getattr(pkg.installed, "version", None)
729 candidate_version = getattr(pkg.candidate, "version", None)
730 if installed_version is not None:
731 version = _("From version %(old_version)s "
732 "to %(new_version)s") % {
733 "old_version": installed_version,
734 "new_version": candidate_version}
735 else:
736 version = _("Version %s") % candidate_version
737 if self.show_versions:
738 contents = "%s\n<small>%s %s</small>" % (contents, version,
739 size)
740 else:
741 contents = "%s <small>%s</small>" % (contents, size)
742 self.store.append([contents, pkg.name, pkg, None, True])
743 self.treeview_update.set_model(self.store)1004 self.treeview_update.set_model(self.store)
744 self.update_count()1005 self.update_count()
745 self.setBusy(False)1006 self.setBusy(False)
746 while Gtk.events_pending():1007 while Gtk.events_pending():
747 Gtk.main_iteration()1008 Gtk.main_iteration()
748 self.refresh_updates_count()1009 self.updates_changed()
749 return False1010 return False
7501011
751 def main(self):1012 def main(self):
7521013
=== modified file 'data/com.ubuntu.update-manager.gschema.xml.in'
--- data/com.ubuntu.update-manager.gschema.xml.in 2012-05-31 22:28:41 +0000
+++ data/com.ubuntu.update-manager.gschema.xml.in 2013-01-23 15:48:20 +0000
@@ -6,12 +6,12 @@
6 <description>Stores the state of the expander that contains the list of changes and the description</description>6 <description>Stores the state of the expander that contains the list of changes and the description</description>
7 </key>7 </key>
8 <key name="window-width" type="i">8 <key name="window-width" type="i">
9 <default>0</default>9 <default>1</default>
10 <summary>The window width</summary>10 <summary>The window width</summary>
11 <description>Stores the width of the update-manager dialog</description>11 <description>Stores the width of the update-manager dialog</description>
12 </key>12 </key>
13 <key name="window-height" type="i">13 <key name="window-height" type="i">
14 <default>0</default>14 <default>400</default>
15 <summary>The window height</summary>15 <summary>The window height</summary>
16 <description>Stores the height of the update-manager dialog</description>16 <description>Stores the height of the update-manager dialog</description>
17 </key>17 </key>
1818
=== modified file 'data/gtkbuilder/UpdateManager.ui'
--- data/gtkbuilder/UpdateManager.ui 2012-12-14 22:11:25 +0000
+++ data/gtkbuilder/UpdateManager.ui 2013-01-23 15:48:20 +0000
@@ -112,11 +112,13 @@
112 <property name="visible">True</property>112 <property name="visible">True</property>
113 <property name="can_focus">True</property>113 <property name="can_focus">True</property>
114 <property name="shadow_type">in</property>114 <property name="shadow_type">in</property>
115 <property name="min_content_height">100</property>
115 <child>116 <child>
116 <object class="GtkTreeView" id="treeview_update">117 <object class="GtkTreeView" id="treeview_update">
117 <property name="visible">True</property>118 <property name="visible">True</property>
118 <property name="can_focus">True</property>119 <property name="can_focus">True</property>
119 <property name="headers_visible">False</property>120 <property name="headers_visible">False</property>
121 <property name="headers_clickable">False</property>
120 <property name="rules_hint">True</property>122 <property name="rules_hint">True</property>
121 <child internal-child="accessible">123 <child internal-child="accessible">
122 <object class="AtkObject" id="treeview_update-atkobject">124 <object class="AtkObject" id="treeview_update-atkobject">
@@ -470,11 +472,9 @@
470 <child>472 <child>
471 <object class="GtkButton" id="button_settings">473 <object class="GtkButton" id="button_settings">
472 <property name="label" translatable="yes">_Settings...</property>474 <property name="label" translatable="yes">_Settings...</property>
473 <property name="use_action_appearance">False</property>
474 <property name="visible">True</property>475 <property name="visible">True</property>
475 <property name="can_focus">True</property>476 <property name="can_focus">True</property>
476 <property name="receives_default">True</property>477 <property name="receives_default">True</property>
477 <property name="use_action_appearance">False</property>
478 <property name="use_underline">True</property>478 <property name="use_underline">True</property>
479 <signal name="clicked" handler="on_button_settings_clicked" swapped="no"/>479 <signal name="clicked" handler="on_button_settings_clicked" swapped="no"/>
480 </object>480 </object>
@@ -488,15 +488,13 @@
488 <child>488 <child>
489 <object class="GtkButton" id="button_close">489 <object class="GtkButton" id="button_close">
490 <property name="label">gtk-cancel</property>490 <property name="label">gtk-cancel</property>
491 <property name="use_action_appearance">False</property>
492 <property name="visible">True</property>491 <property name="visible">True</property>
493 <property name="can_focus">True</property>492 <property name="can_focus">True</property>
494 <property name="can_default">True</property>493 <property name="can_default">True</property>
495 <property name="receives_default">True</property>494 <property name="receives_default">True</property>
496 <property name="use_action_appearance">False</property>
497 <property name="use_stock">True</property>495 <property name="use_stock">True</property>
496 <accelerator key="W" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
498 <accelerator key="Q" signal="clicked" modifiers="GDK_CONTROL_MASK"/>497 <accelerator key="Q" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
499 <accelerator key="W" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
500 </object>498 </object>
501 <packing>499 <packing>
502 <property name="expand">False</property>500 <property name="expand">False</property>
@@ -507,13 +505,11 @@
507 <child>505 <child>
508 <object class="GtkButton" id="button_install">506 <object class="GtkButton" id="button_install">
509 <property name="label" translatable="yes">_Install Now</property>507 <property name="label" translatable="yes">_Install Now</property>
510 <property name="use_action_appearance">False</property>
511 <property name="visible">True</property>508 <property name="visible">True</property>
512 <property name="can_focus">True</property>509 <property name="can_focus">True</property>
513 <property name="can_default">True</property>510 <property name="can_default">True</property>
514 <property name="has_default">True</property>511 <property name="has_default">True</property>
515 <property name="receives_default">True</property>512 <property name="receives_default">True</property>
516 <property name="use_action_appearance">False</property>
517 <property name="image">image-apply</property>513 <property name="image">image-apply</property>
518 <property name="use_underline">True</property>514 <property name="use_underline">True</property>
519 <signal name="clicked" handler="on_button_install_clicked" swapped="no"/>515 <signal name="clicked" handler="on_button_install_clicked" swapped="no"/>
520516
=== added directory 'tests/aptroot-grouping-test'
=== added directory 'tests/aptroot-grouping-test/etc'
=== added directory 'tests/aptroot-grouping-test/etc/apt'
=== added file 'tests/aptroot-grouping-test/etc/apt/sources.list'
--- tests/aptroot-grouping-test/etc/apt/sources.list 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/etc/apt/sources.list 2013-01-23 15:48:20 +0000
@@ -0,0 +1,2 @@
1deb http://archive.ubuntu.com/ubuntu lucid main
2deb http://archive.ubuntu.com/ubuntu lucid-security main
03
=== added symlink 'tests/aptroot-grouping-test/etc/apt/trusted.gpg'
=== target is u'/etc/apt/trusted.gpg'
=== added directory 'tests/aptroot-grouping-test/var'
=== added directory 'tests/aptroot-grouping-test/var/cache'
=== added directory 'tests/aptroot-grouping-test/var/lib'
=== added directory 'tests/aptroot-grouping-test/var/lib/apt'
=== added directory 'tests/aptroot-grouping-test/var/lib/apt/lists'
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release'
--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release 2013-01-23 15:48:20 +0000
@@ -0,0 +1,9 @@
1Origin: Ubuntu
2Label: Ubuntu
3Suite: lucid-security
4Version: 10.04
5Codename: lucid
6Date: Thu, 29 Apr 2010 17:24:55 UTC
7Architectures: amd64 armel i386 ia64 powerpc sparc
8Components: main restricted universe multiverse
9Description: Ubuntu Lucid 10.04
010
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg'
--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_Release.gpg 2013-01-23 15:48:20 +0000
@@ -0,0 +1,7 @@
1-----BEGIN PGP SIGNATURE-----
2Version: GnuPG v1.4.6 (GNU/Linux)
3
4iD8DBQBL2cDzQJdur0N9BbURAmk2AJ9ungOjKn0ektAH87KhRIHht+1cDQCfck7P
5ZoIb2P0v2PEqa4Az8KnIIW4=
6=b/mY
7-----END PGP SIGNATURE-----
08
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages'
--- 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
+++ 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
@@ -0,0 +1,10 @@
1Package: base-pkg
2Priority: optional
3Section: admin
4Installed-Size: 1
5Maintainer: Foo <foo@bar.com>
6Architecture: all
7Version: 2
8Size: 1
9Description: a base package
10Origin: Ubuntu
011
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release'
--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release 2013-01-23 15:48:20 +0000
@@ -0,0 +1,9 @@
1Origin: Ubuntu
2Label: Ubuntu
3Suite: lucid
4Version: 10.04
5Codename: lucid
6Date: Thu, 29 Apr 2010 17:24:55 UTC
7Architectures: amd64 armel i386 ia64 powerpc sparc
8Components: main restricted universe multiverse
9Description: Ubuntu Lucid 10.04
010
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg'
--- tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_Release.gpg 2013-01-23 15:48:20 +0000
@@ -0,0 +1,7 @@
1-----BEGIN PGP SIGNATURE-----
2Version: GnuPG v1.4.6 (GNU/Linux)
3
4iD8DBQBL2cDzQJdur0N9BbURAmk2AJ9ungOjKn0ektAH87KhRIHht+1cDQCfck7P
5ZoIb2P0v2PEqa4Az8KnIIW4=
6=b/mY
7-----END PGP SIGNATURE-----
08
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid_main_binary-amd64_Packages'
--- 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
+++ 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
@@ -0,0 +1,80 @@
1Package: installed-app
2Priority: optional
3Section: admin
4Installed-Size: 1
5Maintainer: Foo <foo@bar.com>
6Architecture: all
7Version: 2
8Size: 1
9Depends: installed-pkg-multiple-deps
10Suggests: installed-pkg-single-dep
11Description: just an app
12Origin: Ubuntu
13
14Package: installed-app-with-subitems
15Priority: optional
16Section: admin
17Installed-Size: 1
18Maintainer: Foo <foo@bar.com>
19Architecture: all
20Version: 2
21Size: 1
22Recommends: intermediate, installed-pkg-multiple-deps
23Description: app with subitems
24Origin: Ubuntu
25
26Package: intermediate
27Priority: optional
28Section: admin
29Installed-Size: 1
30Maintainer: Foo <foo@bar.com>
31Architecture: all
32Version: 0
33Size: 1
34Description: intermediate pkg
35Origin: Ubuntu
36
37Package: installed-pkg
38Priority: optional
39Section: admin
40Installed-Size: 1
41Maintainer: Foo <foo@bar.com>
42Architecture: all
43Version: 2
44Size: 1
45Description: just a pkg
46Origin: Ubuntu
47
48Package: installed-pkg-multiple-deps
49Priority: optional
50Section: admin
51Installed-Size: 1
52Maintainer: Foo <foo@bar.com>
53Architecture: all
54Version: 2
55Size: 1
56Description: pkg with multiple deps
57Origin: Ubuntu
58
59Package: installed-pkg-single-dep
60Priority: optional
61Section: admin
62Installed-Size: 1
63Maintainer: Foo <foo@bar.com>
64Architecture: all
65Version: 2
66Size: 1
67Description: pkg with single dep
68Origin: Ubuntu
69
70Package: ubuntu-minimal
71Priority: optional
72Section: admin
73Installed-Size: 1
74Maintainer: Foo <foo@bar.com>
75Architecture: all
76Version: 2
77Size: 1
78Recommends: base-pkg
79Description: meta package
80Origin: Ubuntu
081
=== added file 'tests/aptroot-grouping-test/var/lib/apt/lists/lock'
=== added directory 'tests/aptroot-grouping-test/var/lib/apt/lists/partial'
=== added directory 'tests/aptroot-grouping-test/var/lib/dpkg'
=== added file 'tests/aptroot-grouping-test/var/lib/dpkg/status'
--- tests/aptroot-grouping-test/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
+++ tests/aptroot-grouping-test/var/lib/dpkg/status 2013-01-23 15:48:20 +0000
@@ -0,0 +1,82 @@
1Package: base-pkg
2Status: install ok installed
3Priority: optional
4Section: admin
5Installed-Size: 1
6Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
7Architecture: all
8Version: 0.1
9Description: a base package
10
11Package: installed-app
12Status: install ok installed
13Priority: optional
14Section: admin
15Installed-Size: 1
16Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
17Architecture: all
18Version: 0.1
19Description: an installed app
20
21Package: installed-app-with-subitems
22Status: install ok installed
23Priority: optional
24Section: admin
25Installed-Size: 1
26Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
27Architecture: all
28Version: 0.1
29Description: an installed app with subitems
30
31Package: intermediate
32Status: install ok installed
33Priority: optional
34Section: admin
35Installed-Size: 1
36Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
37Architecture: all
38Version: 0.1
39Depends: installed-pkg-single-dep
40Description: an intermediate pkg
41
42Package: installed-pkg
43Status: install ok installed
44Priority: optional
45Section: admin
46Installed-Size: 1
47Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
48Architecture: all
49Version: 0.1
50Description: an installed package
51
52Package: not-updatable
53Status: install ok installed
54Priority: optional
55Section: admin
56Installed-Size: 1
57Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
58Architecture: all
59Version: 0.1
60Depends: installed-pkg-single-dep
61Description: extra pkg
62 to confirm we only look at updatable pkgs when calculating groupings
63
64Package: installed-pkg-multiple-deps
65Status: install ok installed
66Priority: optional
67Section: admin
68Installed-Size: 1
69Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
70Architecture: all
71Version: 0.1
72Description: an installed package that multiple apps depend on
73
74Package: installed-pkg-single-dep
75Status: install ok installed
76Priority: optional
77Section: admin
78Installed-Size: 1
79Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
80Architecture: all
81Version: 0.1
82Description: an installed package that only one app depends on
083
=== added directory 'tests/aptroot-grouping-test/var/lib/dpkg/updates'
=== modified file 'tests/test_update_list.py'
--- tests/test_update_list.py 2013-01-16 12:45:38 +0000
+++ tests/test_update_list.py 2013-01-23 15:48:20 +0000
@@ -6,15 +6,16 @@
6import apt6import apt
7import unittest7import unittest
88
9from UpdateManager.Core.UpdateList import UpdateList9from UpdateManager.Core import UpdateList
10from UpdateManager.Core.MyCache import MyCache10from UpdateManager.Core.MyCache import MyCache
1111
12from mock import patch12from gi.repository import Gio
13from mock import patch, PropertyMock, MagicMock
1314
14CURDIR = os.path.dirname(os.path.abspath(__file__))15CURDIR = os.path.dirname(os.path.abspath(__file__))
1516
1617
17class UpdateListTestCase(unittest.TestCase):18class PhasedTestCase(unittest.TestCase):
1819
19 def setUp(self):20 def setUp(self):
20 # mangle the arch21 # mangle the arch
@@ -30,11 +31,10 @@
30 self.cache._listsLock = 031 self.cache._listsLock = 0
31 self.cache.update()32 self.cache.update()
32 self.cache.open()33 self.cache.open()
33 self.updates_list = UpdateList(parent=None)34 self.updates_list = UpdateList.UpdateList(parent=None)
3435
35 def assertUpdatesListLen(self, nr):36 def assertUpdatesListLen(self, nr):
36 origin = list(self.updates_list.pkgs.keys())[0]37 self.assertEqual(self.updates_list.num_updates, nr)
37 self.assertEqual(len(self.updates_list.pkgs[origin]), nr)
3838
39 def test_phased_percentage_not_included(self):39 def test_phased_percentage_not_included(self):
40 """ Test that updates above the threshold are not included"""40 """ Test that updates above the threshold are not included"""
@@ -76,16 +76,96 @@
76 self.updates_list.update(self.cache)76 self.updates_list.update(self.cache)
77 self.assertUpdatesListLen(1)77 self.assertUpdatesListLen(1)
7878
79 @patch('UpdateManager.Core.UpdateList.OriginsImportance')79 @patch('UpdateManager.Core.UpdateList.UpdateList._is_security_update')
80 def test_phased_percentage_from_security(self, mock_origin_importance):80 def test_phased_percentage_from_security(self, mock_security):
81 """ Test that updates from the security node go in"""81 """ Test that updates from the security node go in"""
82 # pretend all updates come from security for the sake of this test82 # pretend all updates come from security for the sake of this test
83 mock_origin_importance.SECURITY = 083 mock_security.return_value = True
84 with patch.object(self.updates_list.random, "randint") as mock_randint:84 with patch.object(self.updates_list.random, "randint") as mock_randint:
85 mock_randint.return_value = 10085 mock_randint.return_value = 100
86 self.updates_list.update(self.cache)86 self.updates_list.update(self.cache)
87 self.assertUpdatesListLen(2)87 self.assertUpdatesListLen(2)
8888
89class GroupingTestCase(unittest.TestCase):
90 # installed_files does not respect aptroot, so we have to patch it
91 @patch('apt.package.Package.installed_files', new_callable=PropertyMock)
92 @patch('gi.repository.Gio.DesktopAppInfo.new_from_filename')
93 def setUp(self, mock_desktop, mock_installed):
94 # mangle the arch
95 real_arch = apt.apt_pkg.config.find("APT::Architecture")
96 apt.apt_pkg.config.set("APT::Architecture", "amd64")
97 self.addCleanup(
98 lambda: apt.apt_pkg.config.set("APT::Architecture", real_arch))
99 self.aptroot = os.path.join(CURDIR,
100 "aptroot-grouping-test")
101 self.cache = MyCache(apt.progress.base.OpProgress(),
102 rootdir=self.aptroot)
103 mock_installed.__get__ = self.fake_installed_files
104 mock_desktop.side_effect = self.fake_desktop
105 self.updates_list = UpdateList.UpdateList(parent=None, dist='lucid')
106 self.updates_list.update(self.cache)
107
108 def fake_installed_files(self, mock_prop, pkg, pkg_class):
109 if pkg.name == 'installed-app':
110 return ['/usr/share/applications/installed-app.desktop']
111 elif pkg.name == 'installed-app-with-subitems':
112 return ['/usr/share/applications/installed-app2.desktop']
113 else:
114 return []
115
116 def fake_desktop(self, path):
117 # These can all be the same for our purposes
118 app = MagicMock()
119 app.get_filename.return_value = path
120 app.get_display_name.return_value = 'App ' + os.path.basename(path)
121 app.get_icon.return_value = Gio.ThemedIcon.new("package")
122 return app
123
124 def test_app(self):
125 self.assertGreater(len(self.updates_list.update_groups), 0)
126 group = self.updates_list.update_groups[0]
127 self.assertIsInstance(group, UpdateList.UpdateApplicationGroup)
128 self.assertIsNotNone(group.core_item)
129 self.assertEqual(group.core_item.pkg.name, 'installed-app')
130 self.assertListEqual([x.pkg.name for x in group.items],
131 ['installed-app'])
132
133 def test_app_with_subitems(self):
134 self.assertGreater(len(self.updates_list.update_groups), 1)
135 group = self.updates_list.update_groups[1]
136 self.assertIsInstance(group, UpdateList.UpdateApplicationGroup)
137 self.assertIsNotNone(group.core_item)
138 self.assertEqual(group.core_item.pkg.name,
139 'installed-app-with-subitems')
140 self.assertListEqual([x.pkg.name for x in group.items],
141 ['installed-app-with-subitems',
142 'installed-pkg-single-dep'])
143
144 def test_pkg(self):
145 self.assertGreater(len(self.updates_list.update_groups), 2)
146 group = self.updates_list.update_groups[2]
147 self.assertIsInstance(group, UpdateList.UpdatePackageGroup)
148 self.assertIsNotNone(group.core_item)
149 self.assertEqual(group.core_item.pkg.name, 'installed-pkg')
150 self.assertListEqual([x.pkg.name for x in group.items],
151 ['installed-pkg'])
152
153 def test_pkg_multiple_deps(self):
154 self.assertEqual(len(self.updates_list.update_groups), 4)
155 group = self.updates_list.update_groups[3]
156 self.assertIsInstance(group, UpdateList.UpdatePackageGroup)
157 self.assertIsNotNone(group.core_item)
158 self.assertEqual(group.core_item.pkg.name,
159 'installed-pkg-multiple-deps')
160 self.assertListEqual([x.pkg.name for x in group.items],
161 ['installed-pkg-multiple-deps'])
162
163 def test_security(self):
164 self.assertEqual(len(self.updates_list.security_groups), 1)
165 group = self.updates_list.security_groups[0]
166 self.assertIsInstance(group, UpdateList.UpdateSystemGroup)
167 self.assertIsNone(group.core_item)
168 self.assertListEqual([x.pkg.name for x in group.items], ['base-pkg'])
89169
90if __name__ == "__main__":170if __name__ == "__main__":
91 unittest.main()171 unittest.main()
92172
=== modified file 'tests/test_update_origin.py'
--- tests/test_update_origin.py 2012-11-19 16:20:30 +0000
+++ tests/test_update_origin.py 2013-01-23 15:48:20 +0000
@@ -48,15 +48,11 @@
48 if l.archive == "lucid-security"]:48 if l.archive == "lucid-security"]:
49 test_pkgs.add(pkg.name)49 test_pkgs.add(pkg.name)
50 self.assertTrue(len(test_pkgs) > 0)50 self.assertTrue(len(test_pkgs) > 0)
51 ul = UpdateList(None)51 ul = UpdateList(None, dist="lucid")
52 matcher = ul.initMatcher("lucid")
53 for pkgname in test_pkgs:52 for pkgname in test_pkgs:
54 pkg = self.cache[pkgname]53 pkg = self.cache[pkgname]
55 origin = self.cache.match_package_origin(pkg, matcher)54 self.assertTrue(ul._is_security_update(pkg),
56 self.assertEqual(self.cache.match_package_origin(pkg, matcher),55 "pkg '%s' is not in lucid-security" % pkg.name)
57 matcher[("lucid-security", "Ubuntu")],
58 "pkg '%s' is not in lucid-security but in '%s' "
59 "instead" % (pkg.name, origin.description))
6056
61 def testOriginMatcherWithVersionInUpdatesAndSecurity(self):57 def testOriginMatcherWithVersionInUpdatesAndSecurity(self):
62 # empty dpkg status58 # empty dpkg status
@@ -90,15 +86,13 @@
90 "newer")86 "newer")
9187
92 # now test if versions in -security are detected88 # now test if versions in -security are detected
93 ul = UpdateList(None)89 ul = UpdateList(None, dist="lucid")
94 matcher = ul.initMatcher("lucid")
95 for pkgname in test_pkgs:90 for pkgname in test_pkgs:
96 pkg = self.cache[pkgname]91 pkg = self.cache[pkgname]
97 self.assertEqual(self.cache.match_package_origin(pkg, matcher),92 self.assertTrue(ul._is_security_update(pkg),
98 matcher[("lucid-security", "Ubuntu")],93 "package '%s' from lucid-updates contains also a "
99 "package '%s' from lucid-updates contains also a "94 "(not yet installed) security updates, but it is "
100 "(not yet installed) security updates, but it is "95 "not labeled as such" % pkg.name)
101 "not labeled as such" % pkg.name)
10296
103 # now check if it marks the version with -update if the -security97 # now check if it marks the version with -update if the -security
104 # version is installed98 # version is installed
@@ -118,17 +112,14 @@
118 self.cache.open()112 self.cache.open()
119 for pkgname in test_pkgs:113 for pkgname in test_pkgs:
120 pkg = self.cache[pkgname]114 pkg = self.cache[pkgname]
121 self.assertNotEqual(None, pkg._pkg.current_ver,115 self.assertIsNotNone(pkg._pkg.current_ver,
122 "no package '%s' installed" % pkg.name)116 "no package '%s' installed" % pkg.name)
123 candidate_version = getattr(pkg.candidate, "version", None)117 candidate_version = getattr(pkg.candidate, "version", None)
124 origin = self.cache.match_package_origin(pkg, matcher)118 self.assertFalse(ul._is_security_update(pkg),
125 self.assertEqual(self.cache.match_package_origin(pkg, matcher),
126 matcher[("lucid-updates", "Ubuntu")],
127 "package '%s' (%s) from lucid-updates is "119 "package '%s' (%s) from lucid-updates is "
128 "labelled '%s' even though we have marked this "120 "labelled as a security update even though we "
129 "version as installed already" %121 "have marked this version as installed already" %
130 (pkg.name, candidate_version,122 (pkg.name, candidate_version))
131 origin.description))
132123
133124
134if __name__ == "__main__":125if __name__ == "__main__":
135126
=== modified file 'tests/test_utils.py'
--- tests/test_utils.py 2012-08-17 21:11:57 +0000
+++ tests/test_utils.py 2013-01-23 15:48:20 +0000
@@ -3,14 +3,11 @@
33
4import logging4import logging
5import glob5import glob
6import mock
6import sys7import sys
7import unittest8import unittest
89
9from UpdateManager.Core.utils import (10from UpdateManager.Core import utils
10 is_child_of_process_name,
11 get_string_with_no_auth_from_source_entry,
12 humanize_size,
13 estimate_kernel_size_in_boot)
1411
1512
16class TestUtils(unittest.TestCase):13class TestUtils(unittest.TestCase):
@@ -18,27 +15,27 @@
18 def test_humanize_size(self):15 def test_humanize_size(self):
19 # humanize size is a bit funny, it rounds up to kB as the meaningful16 # humanize size is a bit funny, it rounds up to kB as the meaningful
20 # unit for users17 # unit for users
21 self.assertEqual(humanize_size(1000), "1 kB")18 self.assertEqual(utils.humanize_size(1000), "1 kB")
22 self.assertEqual(humanize_size(10), "1 kB")19 self.assertEqual(utils.humanize_size(10), "1 kB")
23 self.assertEqual(humanize_size(1200), "2 kB")20 self.assertEqual(utils.humanize_size(1200), "2 kB")
24 # but not for MB as well21 # but not for MB as well
25 self.assertEqual(humanize_size(1200 * 1000), "1.2 MB")22 self.assertEqual(utils.humanize_size(1200 * 1000), "1.2 MB")
26 self.assertEqual(humanize_size(1478 * 1000), "1.5 MB")23 self.assertEqual(utils.humanize_size(1478 * 1000), "1.5 MB")
27 # and we don't go to Gb just yet (as its not really needed24 # and we don't go to Gb just yet (as its not really needed
28 # in a upgrade context most of the time25 # in a upgrade context most of the time
29 self.assertEqual(humanize_size(1000 * 1000 * 1000), "1000.0 MB")26 self.assertEqual(utils.humanize_size(1000 * 1000 * 1000), "1000.0 MB")
3027
31 @unittest.skipIf(not glob.glob("/boot/*"), "inside chroot")28 @unittest.skipIf(not glob.glob("/boot/*"), "inside chroot")
32 def test_estimate_kernel_size(self):29 def test_estimate_kernel_size(self):
33 estimate = estimate_kernel_size_in_boot()30 estimate = utils.estimate_kernel_size_in_boot()
34 self.assertTrue(estimate > 0)31 self.assertTrue(estimate > 0)
3532
36 def test_is_child_of_process_name(self):33 def test_is_child_of_process_name(self):
37 self.assertTrue(is_child_of_process_name("init"))34 self.assertTrue(utils.is_child_of_process_name("init"))
38 self.assertFalse(is_child_of_process_name("mvo"))35 self.assertFalse(utils.is_child_of_process_name("mvo"))
39 for e in glob.glob("/proc/[0-9]*"):36 for e in glob.glob("/proc/[0-9]*"):
40 pid = int(e[6:])37 pid = int(e[6:])
41 is_child_of_process_name("gdm", pid)38 utils.is_child_of_process_name("gdm", pid)
4239
43 def test_is_port_listening(self):40 def test_is_port_listening(self):
44 from UpdateManager.Core.utils import is_port_already_listening41 from UpdateManager.Core.utils import is_port_already_listening
@@ -49,16 +46,70 @@
49 # entry with PW46 # entry with PW
50 s = SourceEntry("deb http://user:pass@some-ppa/ ubuntu main")47 s = SourceEntry("deb http://user:pass@some-ppa/ ubuntu main")
51 self.assertTrue(48 self.assertTrue(
52 not "user" in get_string_with_no_auth_from_source_entry(s))49 not "user" in utils.get_string_with_no_auth_from_source_entry(s))
53 self.assertTrue(50 self.assertTrue(
54 not "pass" in get_string_with_no_auth_from_source_entry(s))51 not "pass" in utils.get_string_with_no_auth_from_source_entry(s))
55 self.assertEqual(get_string_with_no_auth_from_source_entry(s),52 self.assertEqual(utils.get_string_with_no_auth_from_source_entry(s),
56 "deb http://hidden-u:hidden-p@some-ppa/ ubuntu main")53 "deb http://hidden-u:hidden-p@some-ppa/ ubuntu main")
57 # no pw54 # no pw
58 s = SourceEntry("deb http://some-ppa/ ubuntu main")55 s = SourceEntry("deb http://some-ppa/ ubuntu main")
59 self.assertEqual(get_string_with_no_auth_from_source_entry(s),56 self.assertEqual(utils.get_string_with_no_auth_from_source_entry(s),
60 "deb http://some-ppa/ ubuntu main")57 "deb http://some-ppa/ ubuntu main")
6158
59 @mock.patch('UpdateManager.Core.utils._load_meta_pkg_list')
60 def test_flavor_package_ubuntu_first(self, mock_load):
61 cache = {'ubuntu-desktop': mock.MagicMock(),
62 'other-desktop': mock.MagicMock()}
63 cache['ubuntu-desktop'].is_installed = True
64 cache['other-desktop'].is_installed = True
65 mock_load.return_value = ['other-desktop']
66 self.assertEqual(utils.get_ubuntu_flavor_package(cache=cache),
67 'ubuntu-desktop')
68
69 @mock.patch('UpdateManager.Core.utils._load_meta_pkg_list')
70 def test_flavor_package_match(self, mock_load):
71 cache = {'a': mock.MagicMock(),
72 'b': mock.MagicMock(),
73 'c': mock.MagicMock()}
74 cache['a'].is_installed = True
75 cache['b'].is_installed = True
76 cache['c'].is_installed = True
77 mock_load.return_value = ['c', 'a', 'b']
78 # Must pick alphabetically first
79 self.assertEqual(utils.get_ubuntu_flavor_package(cache=cache), 'a')
80
81 def test_flavor_package_default(self):
82 self.assertEqual(utils.get_ubuntu_flavor_package(cache={}),
83 'ubuntu-desktop')
84
85 def test_flavor_default(self):
86 self.assertEqual(utils.get_ubuntu_flavor(cache={}), 'ubuntu')
87
88 @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
89 def test_flavor_simple(self, mock_package):
90 mock_package.return_value = 'd'
91 self.assertEqual(utils.get_ubuntu_flavor(), 'd')
92
93 @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
94 def test_flavor_chop(self, mock_package):
95 mock_package.return_value = 'd-pkg'
96 self.assertEqual(utils.get_ubuntu_flavor(), 'd')
97
98 @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
99 def test_flavor_name_desktop(self, mock_package):
100 mock_package.return_value = 'something-desktop'
101 self.assertEqual(utils.get_ubuntu_flavor_name(), 'Something')
102
103 @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
104 def test_flavor_name_netbook(self, mock_package):
105 mock_package.return_value = 'something-netbook'
106 self.assertEqual(utils.get_ubuntu_flavor_name(), 'Something')
107
108 @mock.patch('UpdateManager.Core.utils.get_ubuntu_flavor_package')
109 def test_flavor_name_studio(self, mock_package):
110 mock_package.return_value = 'ubuntustudio-desktop'
111 self.assertEqual(utils.get_ubuntu_flavor_name(), 'Ubuntu Studio')
112
62113
63if __name__ == '__main__':114if __name__ == '__main__':
64 if len(sys.argv) > 1 and sys.argv[1] == "-v":115 if len(sys.argv) > 1 and sys.argv[1] == "-v":

Subscribers

People subscribed via source and target branches

to status/vote changes: