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