launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #16930
Re: [Merge] lp:~cjwatson/launchpad/livefs-browser into lp:launchpad
Review: Approve code
Diff comments:
> === modified file 'lib/lp/app/browser/configure.zcml'
> --- lib/lp/app/browser/configure.zcml 2013-04-17 11:07:52 +0000
> +++ lib/lp/app/browser/configure.zcml 2014-06-17 15:19:20 +0000
> @@ -1,4 +1,4 @@
> -<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +<!-- Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> GNU Affero General Public License version 3 (see the file LICENSE).
> -->
>
> @@ -564,6 +564,12 @@
> factory="lp.app.browser.tales.BuildImageDisplayAPI"
> name="image"
> />
> + <adapter
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + provides="zope.traversing.interfaces.IPathAdapter"
> + factory="lp.app.browser.tales.BuildImageDisplayAPI"
> + name="image"
> + />
>
> <adapter
> for="lp.soyuz.interfaces.archive.IArchive"
> @@ -815,6 +821,12 @@
> name="fmt"
> />
> <adapter
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + provides="zope.traversing.interfaces.IPathAdapter"
> + factory="lp.app.browser.tales.LiveFSFormatterAPI"
> + name="fmt"
> + />
> + <adapter
> for="lp.blueprints.interfaces.specification.ISpecification"
> provides="zope.traversing.interfaces.IPathAdapter"
> factory="lp.app.browser.tales.SpecificationFormatterAPI"
>
> === modified file 'lib/lp/app/browser/tales.py'
> --- lib/lp/app/browser/tales.py 2013-06-22 08:37:21 +0000
> +++ lib/lp/app/browser/tales.py 2014-06-17 15:19:20 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Implementation of the lp: htmlform: fmt: namespaces in TALES."""
> @@ -1830,6 +1830,18 @@
> 'owner': self._context.owner.displayname}
>
>
> +class LiveFSFormatterAPI(CustomizableFormatter):
> + """Adapter providing fmt support for ILiveFS objects."""
> +
> + _link_summary_template = _(
> + 'Live filesystem %(distroseries)s %(name)s for %(owner)s')
> +
> + def _link_summary_values(self):
> + return {'distroseries': self._context.distro_series.name,
> + 'name': self._context.name,
> + 'owner': self._context.owner.displayname}
> +
> +
> class SpecificationFormatterAPI(CustomizableFormatter):
> """Adapter providing fmt support for Specification objects"""
>
>
> === modified file 'lib/lp/soyuz/browser/configure.zcml'
> --- lib/lp/soyuz/browser/configure.zcml 2014-06-17 15:19:18 +0000
> +++ lib/lp/soyuz/browser/configure.zcml 2014-06-17 15:19:20 +0000
> @@ -733,10 +733,57 @@
> path_expression="string:+livefs/${distro_series/distribution/name}/${distro_series/name}/${name}"
> attribute_to_parent="owner"
> />
> + <browser:defaultView
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + name="+index"
> + />
> + <browser:page
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + class="lp.soyuz.browser.livefs.LiveFSView"
> + permission="launchpad.View"
> + name="+index"
> + template="../templates/livefs-index.pt"
> + />
> + <browser:menus
> + module="lp.soyuz.browser.livefs"
> + classes="LiveFSNavigationMenu"
> + />
> <browser:navigation
> module="lp.soyuz.browser.livefs"
> classes="LiveFSNavigation"
> />
> + <browser:page
> + for="lp.registry.interfaces.person.IPerson"
> + class="lp.soyuz.browser.livefs.LiveFSAddView"
> + permission="launchpad.Edit"
> + name="+new-livefs"
> + template="../templates/livefs-new.pt"
> + />
> + <browser:page
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + class="lp.soyuz.browser.livefs.LiveFSAdminView"
> + permission="launchpad.Admin"
> + name="+admin"
> + template="../../app/templates/generic-edit.pt"/>
> + <browser:page
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + class="lp.soyuz.browser.livefs.LiveFSEditView"
> + permission="launchpad.Edit"
> + name="+edit"
> + template="../../app/templates/generic-edit.pt"/>
> + <browser:page
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + class="lp.soyuz.browser.livefs.LiveFSHierarchy"
> + permission="zope.Public"
> + name="+hierarchy"
> + template="../../app/templates/launchpad-hierarchy.pt"
> + />
> + <adapter
> + provides="lp.services.webapp.interfaces.IBreadcrumb"
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + factory="lp.services.webapp.breadcrumb.NameBreadcrumb"
> + permission="zope.Public"
> + />
> <browser:url
> for="lp.soyuz.interfaces.livefs.ILiveFSSet"
> path_expression="string:livefses"
> @@ -747,9 +794,44 @@
> path_expression="string:+build/${id}"
> attribute_to_parent="livefs"
> />
> + <browser:menus
> + module="lp.soyuz.browser.livefsbuild"
> + classes="LiveFSBuildContextMenu"
> + />
> <browser:navigation
> module="lp.soyuz.browser.livefsbuild"
> classes="LiveFSBuildNavigation"
> />
> + <browser:defaultView
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + name="+index"
> + />
> + <browser:page
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + class="lp.soyuz.browser.livefsbuild.LiveFSBuildView"
> + permission="launchpad.View"
> + name="+index"
> + template="../templates/livefsbuild-index.pt"
> + />
> + <browser:page
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + class="lp.soyuz.browser.livefsbuild.LiveFSBuildCancelView"
> + permission="launchpad.Edit"
> + name="+cancel"
> + template="../../app/templates/generic-edit.pt"
> + />
> + <browser:page
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + class="lp.soyuz.browser.livefsbuild.LiveFSBuildRescoreView"
> + permission="launchpad.Admin"
> + name="+rescore"
> + template="../../app/templates/generic-edit.pt"
> + />
> + <adapter
> + provides="lp.services.webapp.interfaces.IBreadcrumb"
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
> + permission="zope.Public"
> + />
> </facet>
> </configure>
>
> === modified file 'lib/lp/soyuz/browser/livefs.py'
> --- lib/lp/soyuz/browser/livefs.py 2014-06-17 15:19:18 +0000
> +++ lib/lp/soyuz/browser/livefs.py 2014-06-17 15:19:20 +0000
> @@ -1,17 +1,73 @@
> # Copyright 2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> +"""LiveFS views."""
> +
> __metaclass__ = type
> __all__ = [
> + 'LiveFSAddView',
> + 'LiveFSEditView',
> 'LiveFSNavigation',
> + 'LiveFSNavigationMenu',
> + 'LiveFSView',
> ]
>
> +import json
> +
> +from lazr.lifecycle.event import ObjectModifiedEvent
> +from lazr.lifecycle.snapshot import Snapshot
> +from lazr.restful import ResourceJSONEncoder
> +from lazr.restful.interface import (
> + copy_field,
> + use_template,
> + )
> +from zope.component import getUtility
> +from zope.event import notify
> +from zope.interface import (
> + implements,
> + Interface,
> + providedBy,
> + )
> +from zope.schema import (
> + Choice,
> + Text,
> + )
> +
> +from lp.app.browser.launchpad import Hierarchy
> +from lp.app.browser.launchpadform import (
> + action,
> + custom_widget,
> + LaunchpadEditFormView,
> + LaunchpadFormView,
> + )
> +from lp.app.browser.lazrjs import (
> + InlinePersonEditPickerWidget,
> + TextLineEditorWidget,
> + )
> +from lp.app.browser.tales import format_link
> +from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
> +from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries
> +from lp.registry.interfaces.series import SeriesStatus
> +from lp.services.features import getFeatureFlag
> from lp.services.webapp import (
> + canonical_url,
> + enabled_with_permission,
> + LaunchpadView,
> + Link,
> Navigation,
> + NavigationMenu,
> stepthrough,
> )
> +from lp.services.webapp.authorization import check_permission
> +from lp.services.webapp.breadcrumb import Breadcrumb
> from lp.soyuz.browser.build import get_build_by_id_str
> -from lp.soyuz.interfaces.livefs import ILiveFS
> +from lp.soyuz.interfaces.livefs import (
> + ILiveFS,
> + ILiveFSSet,
> + LIVEFS_FEATURE_FLAG,
> + LiveFSFeatureDisabled,
> + NoSuchLiveFS,
> + )
> from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
>
>
> @@ -24,3 +80,285 @@
> if build is None or build.livefs != self.context:
> return None
> return build
> +
> +
> +class ILiveFSesForPerson(Interface):
> + """A marker interface for live filesystem sets."""
> +
> +
> +class LiveFSesForPersonBreadcrumb(Breadcrumb):
> + """A Breadcrumb to handle the "Live filesystems" link."""
> +
> + rootsite = None
> + text = 'Live filesystems'
> +
> + implements(ILiveFSesForPerson)
> +
> + @property
> + def url(self):
> + return canonical_url(self.context, view_name="+livefs")
> +
> +
> +class LiveFSHierarchy(Hierarchy):
> + """Hierarchy for live filesystems."""
> +
> + @property
> + def objects(self):
> + """See `Hierarchy`."""
> + traversed = list(self.request.traversed_objects)
> + # Pop the root object.
> + yield traversed.pop(0)
> + # Pop until we find the live filesystem.
> + livefs = traversed.pop(0)
> + while not ILiveFS.providedBy(livefs):
> + yield livefs
> + livefs = traversed.pop(0)
> + # Pop in the "Live filesystems" link.
> + yield LiveFSesForPersonBreadcrumb(livefs.owner)
> + yield livefs
> + for item in traversed:
> + yield item
> +
> +
> +class LiveFSNavigationMenu(NavigationMenu):
> + """Navigation menu for live filesystems."""
> +
> + usedfor = ILiveFS
> +
> + facet = 'overview'
> +
> + links = ('admin', 'edit',)
> +
> + @enabled_with_permission('launchpad.Admin')
> + def admin(self):
> + return Link('+admin', 'Administer live filesystem', icon='edit')
> +
> + @enabled_with_permission('launchpad.Edit')
> + def edit(self):
> + return Link('+edit', 'Edit live filesystem', icon='edit')
> +
> +
> +class LiveFSView(LaunchpadView):
> + """Default view of a LiveFS."""
> +
> + @property
> + def page_title(self):
> + return "%(name)s's %(livefs_name) live filesystem" % {
> + 'name': self.context.owner.displayname,
> + 'livefs_name': self.context.name,
> + }
This might want to include the distroseries.
> +
> + label = page_title
> +
> + @property
> + def builds(self):
> + return builds_for_livefs(self.context)
> +
> + @property
> + def person_picker(self):
> + field = copy_field(
> + ILiveFS['owner'],
> + vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
> + return InlinePersonEditPickerWidget(
> + self.context, field, format_link(self.context.owner),
> + header='Change owner', step_title='Select a new owner')
> +
> + @property
> + def name_widget(self):
> + name = ILiveFS['name']
> + title = "Edit the live filesystem name"
> + return TextLineEditorWidget(
> + self.context, name, title, 'h1', max_width='95%', truncate_lines=1)
> +
> + @property
> + def sorted_metadata_items(self):
> + if self.context.metadata is None:
> + return []
> + return sorted(self.context.metadata.items())
> +
> +
> +def builds_for_livefs(livefs):
> + """A list of interesting builds.
> +
> + All pending builds are shown, as well as 1-10 recent builds. Recent
> + builds are ordered by date finished (if completed) or date_started (if
> + date finished is not set due to an error building or other circumstance
> + which resulted in the build not being completed). This allows started
> + but unfinished builds to show up in the view but be discarded as more
> + recent builds become available.
> +
> + Builds that the user does not have permission to see are excluded.
> + """
> + builds = [
> + build for build in livefs.pending_builds
> + if check_permission('launchpad.View', build)]
> + for build in livefs.completed_builds:
> + if not check_permission('launchpad.View', build):
> + continue
> + builds.append(build)
> + if len(builds) >= 10:
> + break
> + return builds
> +
> +
> +class ILiveFSEditSchema(Interface):
> + """Schema for adding or editing a live filesystem."""
> +
> + use_template(ILiveFS, include=[
> + 'owner',
> + 'name',
> + 'require_virtualized',
> + ])
> + distro_series = Choice(
> + vocabulary='BuildableDistroSeries', title=u'Distribution series')
This will need to be revisited with derived distros, but it'll do for now.
> + metadata = Text(
> + title=u'Live filesystem build metadata',
> + description=(
> + u'A JSON dictionary of data about the image. Entries here will '
> + 'be passed to the builder slave.'))
> +
> +
> +class LiveFSMetadataValidatorMixin:
> + """Class to validate that live filesystem properties are valid."""
> +
> + def validate(self, data):
> + if data['metadata']:
> + try:
> + json.loads(data['metadata'])
> + except Exception as e:
> + self.setFieldError('metadata', str(e))
> +
> +
> +class LiveFSAddView(LiveFSMetadataValidatorMixin, LaunchpadFormView):
> + """View for creating live filesystems."""
> +
> + title = label = 'Create a new live filesystem'
> +
> + schema = ILiveFSEditSchema
> + field_names = ['owner', 'name', 'distro_series', 'metadata']
> + custom_widget('distro_series', LaunchpadRadioWidget)
> +
> + def initialize(self):
> + """See `LaunchpadView`."""
> + if not getFeatureFlag(LIVEFS_FEATURE_FLAG):
> + raise LiveFSFeatureDisabled
> + super(LiveFSAddView, self).initialize()
> +
> + @property
> + def initial_values(self):
> + series = [
> + term.value for term in BuildableDistroSeries()
> + if term.value.status in (
> + SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)][0]
> + return {
> + 'owner': self.user,
> + 'distro_series': series,
> + 'metadata': '{}',
> + }
> +
> + @property
> + def cancel_url(self):
> + return canonical_url(self.context)
> +
> + @action('Create live filesystem', name='create')
> + def request_action(self, action, data):
> + livefs = getUtility(ILiveFSSet).new(
> + self.user, data['owner'], data['distro_series'], data['name'],
> + json.loads(data['metadata']))
> + self.next_url = canonical_url(livefs)
> +
> + def validate(self, data):
> + super(LiveFSAddView, self).validate(data)
> + owner = data.get('owner', None)
> + distro_series = data['distro_series']
> + name = data.get('name', None)
> + if owner and name:
> + if getUtility(ILiveFSSet).exists(owner, distro_series, name):
> + self.setFieldError(
> + 'name',
> + 'There is already a live filesystem for %s owned by %s '
> + 'with this name.' % (
> + distro_series.displayname, owner.displayname))
> +
> +
> +class BaseLiveFSEditView(LaunchpadEditFormView):
> +
> + schema = ILiveFSEditSchema
> +
> + @property
> + def cancel_url(self):
> + return canonical_url(self.context)
> +
> + @action('Update live filesystem', name='update')
> + def request_action(self, action, data):
> + self.updateContextFromData(data)
> + self.next_url = canonical_url(self.context)
> +
> + @property
> + def adapters(self):
> + """See `LaunchpadFormView`."""
> + return {ILiveFSEditSchema: self.context}
> +
> +
> +class LiveFSAdminView(BaseLiveFSEditView):
> + """View for administering live filesystems."""
> +
> + @property
> + def title(self):
> + return 'Administer %s live filesystem' % self.context.name
> +
> + label = title
> +
> + field_names = ['require_virtualized']
> +
> + @property
> + def initial_values(self):
> + return {'require_virtualized': self.context.require_virtualized}
> +
> +
> +
> +class LiveFSEditView(LiveFSMetadataValidatorMixin, BaseLiveFSEditView):
> + """View for editing live filesystems."""
> +
> + @property
> + def title(self):
> + return 'Edit %s live filesystem' % self.context.name
> +
> + label = title
> +
> + field_names = ['owner', 'name', 'distro_series', 'metadata']
> + custom_widget('distro_series', LaunchpadRadioWidget)
> +
> + @property
> + def initial_values(self):
> + return {
> + 'distro_series': self.context.distro_series,
> + 'metadata': json.dumps(
> + self.context.metadata, ensure_ascii=False,
> + cls=ResourceJSONEncoder),
> + }
> +
> + def updateContextFromData(self, data, context=None, notify_modified=True):
> + """See `LaunchpadEditFormView`."""
> + if 'metadata' in data:
> + data['metadata'] = json.loads(data['metadata'])
> + super(LiveFSEditView, self).updateContextFromData(
> + data, context=context, notify_modified=notify_modified)
> +
> + def validate(self, data):
> + super(LiveFSEditView, self).validate(data)
> + owner = data.get('owner', None)
> + distro_series = data['distro_series']
> + name = data.get('name', None)
> + if owner and name:
> + try:
> + livefs = getUtility(ILiveFSSet).getByName(
> + owner, distro_series, name)
> + if livefs != self.context:
> + self.setFieldError(
> + 'name',
> + 'There is already a live filesystem for %s owned by '
> + '%s with this name.' % (
> + distro_series.displayname, owner.displayname))
> + except NoSuchLiveFS:
> + pass
>
> === modified file 'lib/lp/soyuz/browser/livefsbuild.py'
> --- lib/lp/soyuz/browser/livefsbuild.py 2014-06-17 15:19:18 +0000
> +++ lib/lp/soyuz/browser/livefsbuild.py 2014-06-17 15:19:20 +0000
> @@ -1,15 +1,173 @@
> # Copyright 2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> +"""LiveFSBuild views."""
> +
> __metaclass__ = type
> __all__ = [
> + 'LiveFSBuildContextMenu',
> 'LiveFSBuildNavigation',
> + 'LiveFSBuildView',
> ]
>
> -from lp.services.librarian.browser import FileNavigationMixin
> -from lp.services.webapp import Navigation
> +from zope.interface import Interface
> +
> +from lp.app.browser.launchpadform import (
> + action,
> + LaunchpadFormView,
> + )
> +from lp.buildmaster.enums import BuildQueueStatus
> +from lp.services.librarian.browser import (
> + FileNavigationMixin,
> + ProxiedLibraryFileAlias,
> + )
> +from lp.services.propertycache import cachedproperty
> +from lp.services.webapp import (
> + canonical_url,
> + ContextMenu,
> + enabled_with_permission,
> + LaunchpadView,
> + Link,
> + Navigation,
> + )
> +from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
> from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
>
>
> class LiveFSBuildNavigation(Navigation, FileNavigationMixin):
> usedfor = ILiveFSBuild
> +
> +
> +class LiveFSBuildContextMenu(ContextMenu):
> + """Context menu for live filesystem builds."""
> +
> + usedfor = ILiveFSBuild
> +
> + facet = 'overview'
> +
> + links = ('cancel', 'rescore')
> +
> + @enabled_with_permission('launchpad.Edit')
> + def cancel(self):
> + return Link(
> + '+cancel', 'Cancel build', icon='remove',
> + enabled=self.context.can_be_cancelled)
> +
> + @enabled_with_permission('launchpad.Admin')
> + def rescore(self):
> + return Link(
> + '+rescore', 'Rescore build', icon='edit',
> + enabled=self.context.can_be_rescored)
> +
> +
> +class LiveFSBuildView(LaunchpadView):
> + """Default view of a LiveFSBuild."""
> +
> + @property
> + def label(self):
> + return self.context.title
> +
> + page_title = label
> +
> + @cachedproperty
> + def eta(self):
> + """The datetime when the build job is estimated to complete.
> +
> + This is the BuildQueue.estimated_duration plus the
> + Job.date_started or BuildQueue.getEstimatedJobStartTime.
> + """
> + if self.context.buildqueue_record is None:
> + return None
> + queue_record = self.context.buildqueue_record
> + if queue_record.status == BuildQueueStatus.WAITING:
> + start_time = queue_record.getEstimatedJobStartTime()
> + else:
> + start_time = queue_record.date_started
> + if start_time is None:
> + return None
> + duration = queue_record.estimated_duration
> + return start_time + duration
> +
> + @cachedproperty
> + def estimate(self):
> + """If true, the date value is an estimate."""
> + if self.context.date_finished is not None:
> + return False
> + return self.eta is not None
> +
> + @cachedproperty
> + def date(self):
> + """The date when the build completed or is estimated to complete."""
> + if self.estimate:
> + return self.eta
> + return self.context.date_finished
> +
> + @cachedproperty
> + def files(self):
> + """Return `LibraryFileAlias`es for files produced by this build."""
> + if not self.context.was_built:
> + return None
> +
> + return [
> + ProxiedLibraryFileAlias(alias, self.context)
> + for _, alias, _ in self.context.getFiles() if not alias.deleted]
> +
> + @cachedproperty
> + def has_files(self):
> + return bool(self.files)
> +
> + @property
> + def sorted_metadata_override_items(self):
> + if self.context.metadata_override is None:
> + return []
> + return sorted(self.context.metadata_override.items())
> +
> +
> +class LiveFSBuildCancelView(LaunchpadFormView):
> + """View for cancelling a live filesystem build."""
> +
> + class schema(Interface):
> + """Schema for cancelling a build."""
> +
> + page_title = label = 'Cancel build'
> +
> + @property
> + def cancel_url(self):
> + return canonical_url(self.context)
> + next_url = cancel_url
> +
> + @action('Cancel build', name='cancel')
> + def request_action(self, action, data):
> + """Cancel the build."""
> + self.context.cancel()
> +
> +
> +class LiveFSBuildRescoreView(LaunchpadFormView):
> + """View for rescoring a live filesystem build."""
> +
> + schema = IBuildRescoreForm
> +
> + page_title = label = 'Rescore build'
> +
> + def __call__(self):
> + if self.context.can_be_rescored:
> + return super(LiveFSBuildRescoreView, self).__call__()
> + self.request.response.addWarningNotification(
> + "Cannot rescore this build because it is not queued.")
> + self.request.response.redirect(canonical_url(self.context))
> +
> + @property
> + def cancel_url(self):
> + return canonical_url(self.context)
> + next_url = cancel_url
> +
> + @action('Rescore build', name='rescore')
> + def request_action(self, action, data):
> + """Rescore the build."""
> + score = data.get('priority')
> + self.context.rescore(score)
> + self.request.response.addNotification('Build rescored to %s.' % score)
> +
> + @property
> + def initial_values(self):
> + return {'score': str(self.context.buildqueue_record.lastscore)}
>
> === modified file 'lib/lp/soyuz/browser/tests/test_livefs.py'
> --- lib/lp/soyuz/browser/tests/test_livefs.py 2014-06-17 15:19:18 +0000
> +++ lib/lp/soyuz/browser/tests/test_livefs.py 2014-06-17 15:19:20 +0000
> @@ -1,15 +1,63 @@
> # Copyright 2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> -"""Test live filesystem navigation."""
> +"""Test live filesystem views."""
>
> __metaclass__ = type
>
> +from datetime import (
> + datetime,
> + timedelta,
> + )
> +import json
> +
> +from fixtures import FakeLogger
> +from mechanize import LinkNotFoundError
> +import pytz
> +from zope.component import getUtility
> +from zope.security.interfaces import Unauthorized
> +
> +from lp.app.interfaces.launchpad import ILaunchpadCelebrities
> +from lp.buildmaster.enums import BuildStatus
> +from lp.registry.interfaces.series import SeriesStatus
> +from lp.services.database.constants import UTC_NOW
> from lp.services.features.testing import FeatureFixture
> -from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
> -from lp.testing import TestCaseWithFactory
> -from lp.testing.layers import DatabaseFunctionalLayer
> +from lp.services.webapp import canonical_url
> +from lp.services.webapp.servers import LaunchpadTestRequest
> +from lp.soyuz.browser.livefs import (
> + LiveFSAdminView,
> + LiveFSEditView,
> + LiveFSView,
> + )
> +from lp.soyuz.interfaces.livefs import (
> + LIVEFS_FEATURE_FLAG,
> + LiveFSFeatureDisabled,
> + )
> +from lp.soyuz.interfaces.processor import IProcessorSet
> +from lp.testing import (
> + BrowserTestCase,
> + login,
> + login_person,
> + person_logged_in,
> + TestCaseWithFactory,
> + time_counter,
> + )
> +from lp.testing.layers import (
> + DatabaseFunctionalLayer,
> + LaunchpadFunctionalLayer,
> + )
> +from lp.testing.matchers import (
> + MatchesPickerText,
> + MatchesTagText,
> + )
> +from lp.testing.pages import (
> + extract_text,
> + find_main_content,
> + find_tags_by_class,
> + get_feedback_messages,
> + )
> from lp.testing.publication import test_traverse
> +from lp.testing.views import create_initialized_view
>
>
> class TestLiveFSNavigation(TestCaseWithFactory):
> @@ -20,10 +68,368 @@
> super(TestLiveFSNavigation, self).setUp()
> self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
>
> + def test_canonical_url(self):
> + owner = self.factory.makePerson(name="person")
> + distribution = self.factory.makeDistribution(
> + name="distro", owner=owner)
> + distroseries = self.factory.makeDistroSeries(
> + distribution=distribution, name="unstable")
> + livefs = self.factory.makeLiveFS(
> + registrant=owner, owner=owner, distroseries=distroseries,
> + name=u"livefs")
> + self.assertEqual(
> + "http://launchpad.dev/~person/+livefs/distro/unstable/livefs",
> + canonical_url(livefs))
> +
> def test_livefs(self):
> livefs = self.factory.makeLiveFS()
> obj, _, _ = test_traverse(
> - "http://api.launchpad.dev/devel/~%s/+livefs/%s/%s/%s" % (
> + "http://launchpad.dev/~%s/+livefs/%s/%s/%s" % (
> livefs.owner.name, livefs.distro_series.distribution.name,
> livefs.distro_series.name, livefs.name))
> self.assertEqual(livefs, obj)
> +
> +
> +class TestLiveFSViewsFeatureFlag(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def test_feature_flag_disabled(self):
> + # Without a feature flag, we will not create new LiveFSes.
> + person = self.factory.makePerson()
> + self.assertRaises(
> + LiveFSFeatureDisabled, create_initialized_view,
> + person, "+new-livefs")
> +
> +
> +class TestLiveFSAddView(BrowserTestCase):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSAddView, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.useFixture(FakeLogger())
> + self.person = self.factory.makePerson(
> + name="test-person", displayname="Test Person")
> +
> + def test_initial_distroseries(self):
> + # The initial distroseries is the newest that is current or in
> + # development.
> + archive = self.factory.makeArchive(owner=self.person)
> + self.factory.makeDistroSeries(
> + distribution=archive.distribution, version="14.04",
> + status=SeriesStatus.DEVELOPMENT)
> + development = self.factory.makeDistroSeries(
> + distribution=archive.distribution, version="14.10",
> + status=SeriesStatus.DEVELOPMENT)
> + self.factory.makeDistroSeries(
> + distribution=archive.distribution, version="15.04",
> + status=SeriesStatus.EXPERIMENTAL)
> + with person_logged_in(self.person):
> + view = create_initialized_view(self.person, "+new-livefs")
> + self.assertEqual(development, view.initial_values["distro_series"])
> +
> + def test_create_new_livefs_not_logged_in(self):
> + self.assertRaises(
> + Unauthorized, self.getViewBrowser, self.person,
> + view_name="+new-livefs", no_login=True)
> +
> + def test_create_new_livefs(self):
> + archive = self.factory.makeArchive()
> + distroseries = self.factory.makeDistroSeries(
> + distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
> + browser = self.getViewBrowser(
> + self.person, view_name="+new-livefs", user=self.person)
> + browser.getControl("Name").value = "ubuntu-core"
> + browser.getControl("Live filesystem build metadata").value = (
> + '{"product": "ubuntu-core", "image_format": "plain"}')
> + browser.getControl("Create live filesystem").click()
> +
> + content = find_main_content(browser.contents)
> + self.assertEqual("ubuntu-core\nEdit", extract_text(content.h1))
> + self.assertThat(
> + "Test Person", MatchesPickerText(content, "edit-owner"))
> + self.assertThat(
> + "Distribution series:\n%s\nEdit live filesystem" %
> + distroseries.fullseriesname,
> + MatchesTagText(content, "distro_series"))
> + self.assertThat(
> + "Metadata:\nimage_format\nplain\nproduct\nubuntu-core",
> + MatchesTagText(content, "metadata"))
> +
> + def test_create_new_livefs_users_teams_as_owner_options(self):
> + # Teams that the user is in are options for the live filesystem owner.
> + self.factory.makeTeam(
> + name="test-team", displayname="Test Team", members=[self.person])
> + browser = self.getViewBrowser(
> + self.person, view_name="+new-livefs", user=self.person)
> + options = browser.getControl("Owner").displayOptions
> + self.assertEqual(
> + ["Test Person (test-person)", "Test Team (test-team)"],
> + sorted(str(option) for option in options))
> +
> + def test_create_new_livefs_invalid_metadata(self):
> + # The metadata field must contain valid JSON.
> + browser = self.getViewBrowser(
> + self.person, view_name="+new-livefs", user=self.person)
> + browser.getControl("Name").value = "ubuntu-core"
> + browser.getControl("Live filesystem build metadata").value = "{"
> + browser.getControl("Create live filesystem").click()
> + json_error = str(self.assertRaises(ValueError, json.loads, "{"))
> + self.assertEqual(
> + json_error, get_feedback_messages(browser.contents)[1])
> +
> +
> +class TestLiveFSAdminView(BrowserTestCase):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSAdminView, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.useFixture(FakeLogger())
> + self.person = self.factory.makePerson(
> + name="test-person", displayname="Test Person")
> +
> + def test_unauthorized(self):
> + # A non-admin user cannot administer a live filesystem.
> + login_person(self.person)
> + livefs = self.factory.makeLiveFS(registrant=self.person)
> + livefs_url = canonical_url(livefs)
> + browser = self.getViewBrowser(livefs, user=self.person)
> + self.assertRaises(
> + LinkNotFoundError, browser.getLink, "Administer live filesystem")
> + self.assertRaises(
> + Unauthorized, self.getUserBrowser, livefs_url + "/+admin",
> + user=self.person)
> +
> + def test_admin_livefs(self):
> + # Admins can change require_virtualized.
> + login("admin@xxxxxxxxxxxxx")
> + admins = getUtility(ILaunchpadCelebrities).commercial_admin
> + admins.addMember(self.person, admins)
> + login_person(self.person)
> + livefs = self.factory.makeLiveFS(registrant=self.person)
> + self.assertTrue(livefs.require_virtualized)
> + browser = self.getViewBrowser(livefs, user=self.person)
> + browser.getLink("Administer live filesystem").click()
> + browser.getControl("Require virtualized builders").selected = False
> + browser.getControl("Update live filesystem").click()
> + login_person(self.person)
> + self.assertFalse(livefs.require_virtualized)
> +
> + def test_admin_livefs_sets_date_last_modified(self):
> + # Administering a live filesystem sets the date_last_modified property.
> + login("admin@xxxxxxxxxxxxx")
> + admins = getUtility(ILaunchpadCelebrities).commercial_admin
> + admins.addMember(self.person, admins)
> + login_person(self.person)
> + date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
> + livefs = self.factory.makeLiveFS(
> + registrant=self.person, date_created=date_created)
> + view = LiveFSAdminView(livefs, LaunchpadTestRequest())
> + view.initialize()
> + view.request_action.success({"require_virtualized": False})
> + self.assertSqlAttributeEqualsDate(
> + livefs, "date_last_modified", UTC_NOW)
> +
> +
> +class TestLiveFSEditView(BrowserTestCase):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSEditView, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.useFixture(FakeLogger())
> + self.person = self.factory.makePerson(
> + name="test-person", displayname="Test Person")
> +
> + def test_edit_livefs(self):
> + archive = self.factory.makeArchive()
> + old_series = self.factory.makeDistroSeries(
> + distribution=archive.distribution, status=SeriesStatus.CURRENT)
> + livefs = self.factory.makeLiveFS(
> + registrant=self.person, owner=self.person, distroseries=old_series)
> + self.factory.makeTeam(
> + name="new-team", displayname="New Team", members=[self.person])
> + new_series = self.factory.makeDistroSeries(
> + distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
> +
> + browser = self.getViewBrowser(livefs, user=self.person)
> + browser.getLink("Edit live filesystem").click()
> + browser.getControl("Owner").value = ["new-team"]
> + browser.getControl("Name").value = "new-name"
> + browser.getControl(name="field.distro_series").value = [
> + str(new_series.id)]
> + browser.getControl("Live filesystem build metadata").value = (
> + '{"product": "new-name"}')
> + browser.getControl("Update live filesystem").click()
> +
> + content = find_main_content(browser.contents)
> + self.assertEqual("new-name\nEdit", extract_text(content.h1))
> + self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
> + self.assertThat(
> + "Distribution series:\n%s\nEdit live filesystem" %
> + new_series.fullseriesname,
> + MatchesTagText(content, "distro_series"))
> + self.assertThat(
> + "Metadata:\nproduct\nnew-name",
> + MatchesTagText(content, "metadata"))
> +
> + def test_edit_livefs_sets_date_last_modified(self):
> + # Editing a live filesystem sets the date_last_modified property.
> + date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
> + livefs = self.factory.makeLiveFS(
> + registrant=self.person, date_created=date_created)
> + with person_logged_in(self.person):
> + view = LiveFSEditView(livefs, LaunchpadTestRequest())
> + view.initialize()
> + view.request_action.success({
> + "owner": livefs.owner,
> + "name": u"changed",
> + "distro_series": livefs.distro_series,
> + "metadata": "{}",
> + })
> + self.assertSqlAttributeEqualsDate(
> + livefs, "date_last_modified", UTC_NOW)
> +
> + def test_edit_livefs_already_exists(self):
> + distroseries = self.factory.makeDistroSeries(
> + distribution=getUtility(ILaunchpadCelebrities).ubuntu,
> + displayname="Grumpy")
> + livefs = self.factory.makeLiveFS(
> + registrant=self.person, owner=self.person,
> + distroseries=distroseries, name=u"one")
> + self.factory.makeLiveFS(
> + registrant=self.person, owner=self.person,
> + distroseries=distroseries, name=u"two")
> + browser = self.getViewBrowser(livefs, user=self.person)
> + browser.getLink("Edit live filesystem").click()
> + browser.getControl("Name").value = "two"
> + browser.getControl("Update live filesystem").click()
> + self.assertEqual(
> + "There is already a live filesystem for Grumpy owned by "
> + "Test Person with this name.",
> + extract_text(find_tags_by_class(browser.contents, "message")[1]))
> +
> +
> +class TestLiveFSView(BrowserTestCase):
> +
> + layer = LaunchpadFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSView, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
> + self.distroseries = self.factory.makeDistroSeries(
> + distribution=self.ubuntu, name="shiny", displayname="Shiny")
> + processor = getUtility(IProcessorSet).getByName("386")
> + self.distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=self.distroseries, architecturetag="i386",
> + processor=processor)
> + self.person = self.factory.makePerson(
> + name="test-person", displayname="Test Person")
> + self.factory.makeBuilder(virtualized=True)
> +
> + def makeLiveFS(self):
> + return self.factory.makeLiveFS(
> + registrant=self.person, owner=self.person,
> + distroseries=self.distroseries, name=u"livefs-name",
> + metadata={"project": "ubuntu-test"})
> +
> + def makeBuild(self, livefs=None, archive=None, date_created=None,
> + **kwargs):
> + if livefs is None:
> + livefs = self.makeLiveFS()
> + if archive is None:
> + archive = self.ubuntu.main_archive
> + if date_created is None:
> + date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
> + return self.factory.makeLiveFSBuild(
> + requester=self.person, livefs=livefs, archive=archive,
> + distroarchseries=self.distroarchseries, date_created=date_created,
> + **kwargs)
> +
> + def test_index(self):
> + build = self.makeBuild(
> + status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
> + self.assertTextMatchesExpressionIgnoreWhitespace("""\
> + Test Person Live filesystems livefs-name
> + .*
> + Live filesystem information
> + Owner: Test Person
> + Distribution series: Ubuntu Shiny
> + Metadata: project ubuntu-test
> + Latest builds
> + Status When complete Architecture Archive
> + Successfully built 30 minutes ago i386
> + Primary Archive for Ubuntu Linux
> + """, self.getMainText(build.livefs))
> +
> + def test_index_success_with_buildlog(self):
> + # The build log is shown if it is there.
> + build = self.makeBuild(
> + status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
> + build.setLog(self.factory.makeLibraryFileAlias())
> + self.assertTextMatchesExpressionIgnoreWhitespace("""\
> + Latest builds
> + Status When complete Architecture Archive
> + Successfully built 30 minutes ago buildlog \(.*\) i386
> + Primary Archive for Ubuntu Linux
> + """, self.getMainText(build.livefs))
> +
> + def test_index_hides_builds_into_private_archive(self):
> + # The index page hides builds into archives the user can't view.
> + archive = self.factory.makeArchive(private=True)
> + with person_logged_in(archive.owner):
> + livefs = self.makeBuild(archive=archive).livefs
> + self.assertIn(
> + "This live filesystem has not been built yet.",
> + self.getMainText(livefs))
> +
> + def test_index_no_builds(self):
> + # A message is shown when there are no builds.
> + livefs = self.factory.makeLiveFS()
> + self.assertIn(
> + "This live filesystem has not been built yet.",
> + self.getMainText(livefs))
> +
> + def test_index_pending(self):
> + # A pending build is listed as such.
> + build = self.makeBuild()
> + build.queueBuild()
> + self.assertTextMatchesExpressionIgnoreWhitespace("""\
> + Latest builds
> + Status When complete Architecture Archive
> + Needs building in .* \(estimated\) i386
> + Primary Archive for Ubuntu Linux
> + """, self.getMainText(build.livefs))
> +
> + def setStatus(self, build, status):
> + build.updateStatus(
> + BuildStatus.BUILDING, date_started=build.date_created)
> + build.updateStatus(
> + status, date_finished=build.date_started + timedelta(minutes=30))
> +
> + def test_builds(self):
> + # LiveFSView.builds produces reasonable results.
> + livefs = self.makeLiveFS()
> + # Create oldest builds first so that they sort properly by id.
> + date_gen = time_counter(
> + datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
> + builds = [
> + self.makeBuild(livefs=livefs, date_created=next(date_gen))
> + for i in range(11)]
> + view = LiveFSView(livefs, None)
> + self.assertEqual(list(reversed(builds)), view.builds)
> + self.setStatus(builds[10], BuildStatus.FULLYBUILT)
> + self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD)
> + # When there are >= 9 pending builds, only the most recent of any
> + # completed builds is returned.
> + self.assertEqual(
> + list(reversed(builds[:9])) + [builds[10]], view.builds)
> + for build in builds[:9]:
> + self.setStatus(build, BuildStatus.FULLYBUILT)
> + self.assertEqual(list(reversed(builds[1:])), view.builds)
>
> === added file 'lib/lp/soyuz/browser/tests/test_livefsbuild.py'
> --- lib/lp/soyuz/browser/tests/test_livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/browser/tests/test_livefsbuild.py 2014-06-17 15:19:20 +0000
> @@ -0,0 +1,261 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test live filesystem build views."""
> +
> +__metaclass__ = type
> +
> +from fixtures import FakeLogger
> +from mechanize import LinkNotFoundError
> +from storm.locals import Store
> +from testtools.matchers import StartsWith
> +import transaction
> +from zope.component import getUtility
> +from zope.security.interfaces import Unauthorized
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.app.interfaces.launchpad import ILaunchpadCelebrities
> +from lp.buildmaster.enums import BuildStatus
> +from lp.services.features.testing import FeatureFixture
> +from lp.services.webapp import canonical_url
> +from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
> +from lp.testing import (
> + admin_logged_in,
> + ANONYMOUS,
> + BrowserTestCase,
> + login,
> + logout,
> + person_logged_in,
> + TestCaseWithFactory,
> + )
> +from lp.testing.layers import (
> + DatabaseFunctionalLayer,
> + LaunchpadFunctionalLayer,
> + )
> +from lp.testing.pages import (
> + extract_text,
> + find_main_content,
> + find_tags_by_class,
> + setupBrowser,
> + setupBrowserForUser,
> + )
> +from lp.testing.views import create_initialized_view
> +
> +
> +class TestCanonicalUrlForLiveFSBuild(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestCanonicalUrlForLiveFSBuild, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def test_canonical_url(self):
> + owner = self.factory.makePerson(name="person")
> + distribution = self.factory.makeDistribution(
> + name="distro", owner=owner)
> + distroseries = self.factory.makeDistroSeries(
> + distribution=distribution, name="unstable")
> + livefs = self.factory.makeLiveFS(
> + registrant=owner, owner=owner, distroseries=distroseries,
> + name=u"livefs")
> + build = self.factory.makeLiveFSBuild(requester=owner, livefs=livefs)
> + self.assertThat(
> + canonical_url(build),
> + StartsWith(
> + "http://launchpad.dev/~person/+livefs/distro/unstable/livefs/"
> + "+build/"))
> +
> +
> +class TestLiveFSBuildView(TestCaseWithFactory):
> +
> + layer = LaunchpadFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuildView, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def test_files(self):
> + # LiveFSBuildView.files returns all the associated files.
> + build = self.factory.makeLiveFSBuild(status=BuildStatus.FULLYBUILT)
> + livefsfile = self.factory.makeLiveFSFile(livefsbuild=build)
> + build_view = create_initialized_view(build, "+index")
> + self.assertEqual(
> + [livefsfile.libraryfile.filename],
> + [lfa.filename for lfa in build_view.files])
> + # Deleted files won't be included.
> + self.assertFalse(livefsfile.libraryfile.deleted)
> + removeSecurityProxy(livefsfile.libraryfile).content = None
> + self.assertTrue(livefsfile.libraryfile.deleted)
> + build_view = create_initialized_view(build, "+index")
> + self.assertEqual([], build_view.files)
> +
> + def test_eta(self):
> + # LiveFSBuildView.eta returns a non-None value when it should, or
> + # None when there's no start time.
> + build = self.factory.makeLiveFSBuild()
> + build.queueBuild()
> + self.assertIsNone(create_initialized_view(build, "+index").eta)
> + self.factory.makeBuilder(processors=[build.processor])
> + self.assertIsNotNone(create_initialized_view(build, "+index").eta)
> +
> + def test_estimate(self):
> + # LiveFSBuildView.estimate returns True until the job is completed.
> + build = self.factory.makeLiveFSBuild()
> + build.queueBuild()
> + self.factory.makeBuilder(processors=[build.processor])
> + build.updateStatus(BuildStatus.BUILDING)
> + self.assertTrue(create_initialized_view(build, "+index").estimate)
> + build.updateStatus(BuildStatus.FULLYBUILT)
> + self.assertFalse(create_initialized_view(build, "+index").estimate)
> +
> +
> +class TestLiveFSBuildOperations(BrowserTestCase):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuildOperations, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.useFixture(FakeLogger())
> + self.build = self.factory.makeLiveFSBuild()
> + self.build_url = canonical_url(self.build)
> + self.requester = self.build.requester
> + self.buildd_admin = self.factory.makePerson(
> + member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
> +
> + def test_cancel_build(self):
> + # The requester of a build can cancel it.
> + self.build.queueBuild()
> + transaction.commit()
> + browser = self.getViewBrowser(self.build, user=self.requester)
> + browser.getLink("Cancel build").click()
> + self.assertEqual(self.build_url, browser.getLink("Cancel").url)
> + browser.getControl("Cancel build").click()
> + self.assertEqual(self.build_url, browser.url)
> + login(ANONYMOUS)
> + self.assertEqual(BuildStatus.CANCELLED, self.build.status)
> +
> + def test_cancel_build_random_user(self):
> + # An unrelated non-admin user cannot cancel a build.
> + self.build.queueBuild()
> + transaction.commit()
> + user = self.factory.makePerson()
> + browser = self.getViewBrowser(self.build, user=user)
> + self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
> + self.assertRaises(
> + Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
> + user=user)
> +
> + def test_cancel_build_wrong_state(self):
> + # If the build isn't queued, you can't cancel it.
> + browser = self.getViewBrowser(self.build, user=self.requester)
> + self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
> +
> + def test_rescore_build(self):
> + # A buildd admin can rescore a build.
> + self.build.queueBuild()
> + transaction.commit()
> + browser = self.getViewBrowser(self.build, user=self.buildd_admin)
> + browser.getLink("Rescore build").click()
> + self.assertEqual(self.build_url, browser.getLink("Cancel").url)
> + browser.getControl("Priority").value = "1024"
> + browser.getControl("Rescore build").click()
> + self.assertEqual(self.build_url, browser.url)
> + login(ANONYMOUS)
> + self.assertEqual(1024, self.build.buildqueue_record.lastscore)
> +
> + def test_rescore_build_invalid_score(self):
> + # Build scores can only take numbers.
> + self.build.queueBuild()
> + transaction.commit()
> + browser = self.getViewBrowser(self.build, user=self.buildd_admin)
> + browser.getLink("Rescore build").click()
> + self.assertEqual(self.build_url, browser.getLink("Cancel").url)
> + browser.getControl("Priority").value = "tentwentyfour"
> + browser.getControl("Rescore build").click()
> + self.assertEqual(
> + "Invalid integer data",
> + extract_text(find_tags_by_class(browser.contents, "message")[1]))
> +
> + def test_rescore_build_not_admin(self):
> + # A non-admin user cannot cancel a build.
> + self.build.queueBuild()
> + transaction.commit()
> + user = self.factory.makePerson()
> + browser = self.getViewBrowser(self.build, user=user)
> + self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
> + self.assertRaises(
> + Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
> + user=user)
> +
> + def test_rescore_build_wrong_state(self):
> + # If the build isn't NEEDSBUILD, you can't rescore it.
> + self.build.queueBuild()
> + with person_logged_in(self.requester):
> + self.build.cancel()
> + browser = self.getViewBrowser(self.build, user=self.buildd_admin)
> + self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
> +
> + def test_rescore_build_wrong_state_stale_link(self):
> + # An attempt to rescore a non-queued build from a stale link shows a
> + # sensible error message.
> + self.build.queueBuild()
> + with person_logged_in(self.requester):
> + self.build.cancel()
> + browser = self.getViewBrowser(
> + self.build, "+rescore", user=self.buildd_admin)
> + self.assertEqual(self.build_url, browser.url)
> + self.assertIn(
> + "Cannot rescore this build because it is not queued.",
> + browser.contents)
> +
> + def test_builder_history(self):
> + Store.of(self.build).flush()
> + self.build.updateStatus(
> + BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
> + title = self.build.title
> + browser = self.getViewBrowser(self.build.builder, "+history")
> + self.assertTextMatchesExpressionIgnoreWhitespace(
> + "Build history.*%s" % title,
> + extract_text(find_main_content(browser.contents)))
> + self.assertEqual(self.build_url, browser.getLink(title).url)
> +
> + def makeBuildingLiveFS(self, archive=None):
> + builder = self.factory.makeBuilder()
> + build = self.factory.makeLiveFSBuild(archive=archive)
> + build.updateStatus(BuildStatus.BUILDING, builder=builder)
> + build.queueBuild()
> + build.buildqueue_record.builder = builder
> + build.buildqueue_record.logtail = "tail of the log"
> + return build
> +
> + def makeNonRedirectingBrowser(self, url, user=None):
> + browser = setupBrowserForUser(user) if user else setupBrowser()
> + browser.mech_browser.set_handle_equiv(False)
> + browser.open(url)
> + return browser
> +
> + def test_builder_index_public(self):
> + build = self.makeBuildingLiveFS()
> + builder_url = canonical_url(build.builder)
> + logout()
> + browser = self.makeNonRedirectingBrowser(builder_url)
> + self.assertIn("tail of the log", browser.contents)
> +
> + def test_builder_index_private(self):
> + archive = self.factory.makeArchive(private=True)
> + with admin_logged_in():
> + build = self.makeBuildingLiveFS(archive=archive)
> + builder_url = canonical_url(build.builder)
> + user = self.factory.makePerson()
> + logout()
> +
> + # An unrelated user can't see the logtail of a private build.
> + browser = self.makeNonRedirectingBrowser(builder_url, user=user)
> + self.assertNotIn("tail of the log", browser.contents)
> +
> + # But someone who can see the archive can.
> + browser = self.makeNonRedirectingBrowser(
> + builder_url, user=archive.owner)
> + self.assertIn("tail of the log", browser.contents)
>
> === added file 'lib/lp/soyuz/templates/livefs-index.pt'
> --- lib/lp/soyuz/templates/livefs-index.pt 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/templates/livefs-index.pt 2014-06-17 15:19:20 +0000
> @@ -0,0 +1,109 @@
> +<html
> + xmlns="http://www.w3.org/1999/xhtml"
> + xmlns:tal="http://xml.zope.org/namespaces/tal"
> + xmlns:metal="http://xml.zope.org/namespaces/metal"
> + xmlns:i18n="http://xml.zope.org/namespaces/i18n"
> + metal:use-macro="view/macro:page/main_side"
> + i18n:domain="launchpad"
> +>
> +
> +<body>
> + <metal:registering fill-slot="registering">
> + Created by
> + <tal:registrant replace="structure context/registrant/fmt:link"/>
> + on
> + <tal:created-on replace="structure context/date_created/fmt:date"/>
> + and last modified on
> + <tal:last-modified replace="structure context/date_last_modified/fmt:date"/>
> + </metal:registering>
> +
> + <metal:side fill-slot="side">
> + <div tal:replace="structure context/@@+global-actions"/>
> + </metal:side>
> +
> + <metal:heading fill-slot="heading">
> + <h1 tal:replace="structure view/name_widget"/>
> + </metal:heading>
> +
> + <div metal:fill-slot="main">
> + <h2>Live filesystem information</h2>
> + <div class="two-column-list">
> + <dl id="owner">
> + <dt>Owner:</dt>
> + <dd tal:content="structure view/person_picker"/>
> + </dl>
> + <dl id="distro_series">
> + <dt>Distribution series:</dt>
> + <dd tal:define="distro_series context/distro_series">
> + <a tal:attributes="href distro_series/fmt:url"
> + tal:content="distro_series/fullseriesname"/>
> + <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
> + </dd>
> + </dl>
> + <dl id="metadata">
> + <dt>Metadata:</dt>
> + <dd>
> + <table class="listing compressed">
> + <tbody>
> + <tr tal:repeat="pair view/sorted_metadata_items">
> + <td tal:repeat="value pair" tal:content="value"/>
> + </tr>
> + </tbody>
> + </table>
> + </dd>
> + </dl>
> + </div>
> +
> + <h2>Latest builds</h2>
> + <table id="latest-builds-listing" class="listing"
> + style="margin-bottom: 1em;">
> + <thead>
> + <tr>
> + <th>Status</th>
> + <th>When complete</th>
> + <th>Architecture</th>
> + <th>Archive</th>
> + </tr>
> + </thead>
> + <tbody>
> + <tal:livefs-builds repeat="build view/builds">
> + <tal:build-view define="buildview nocall:build/@@+index">
> + <tr tal:attributes="id string:build-${build/id}">
> + <td>
> + <span tal:replace="structure build/image:icon"/>
> + <a tal:content="build/status/title"
> + tal:attributes="href build/fmt:url"/>
> + </td>
> + <td>
> + <tal:date replace="buildview/date/fmt:displaydate"/>
> + <tal:estimate condition="buildview/estimate">
> + (estimated)
> + </tal:estimate>
> +
> + <tal:build-log define="file build/log" tal:condition="file">
> + <a class="sprite download"
> + tal:attributes="href build/log_url">buildlog</a>
> + (<span tal:replace="file/content/filesize/fmt:bytes"/>)
> + </tal:build-log>
> + </td>
> + <td>
> + <a class="sprite distribution"
> + tal:define="archseries build/distro_arch_series"
> + tal:attributes="href archseries/fmt:url"
> + tal:content="archseries/architecturetag"/>
> + </td>
> + <td>
> + <tal:archive replace="structure build/archive/fmt:link"/>
> + </td>
> + </tr>
> + </tal:build-view>
> + </tal:livefs-builds>
> + </tbody>
> + </table>
> + <p tal:condition="not: view/builds">
> + This live filesystem has not been built yet.
> + </p>
> + </div>
> +
> +</body>
> +</html>
>
> === added file 'lib/lp/soyuz/templates/livefs-new.pt'
> --- lib/lp/soyuz/templates/livefs-new.pt 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/templates/livefs-new.pt 2014-06-17 15:19:20 +0000
> @@ -0,0 +1,45 @@
> +<html
> + xmlns="http://www.w3.org/1999/xhtml"
> + xmlns:tal="http://xml.zope.org/namespaces/tal"
> + xmlns:metal="http://xml.zope.org/namespaces/metal"
> + xmlns:i18n="http://xml.zope.org/namespaces/i18n"
> + metal:use-macro="view/macro:page/main_side"
> + i18n:domain="launchpad"
> +>
> +
> +<body>
> + <div metal:fill-slot="main">
> + <div>
> + <p>
> + A live filesystem image is a copy of an operating system that can
> + start from a removable medium such as a DVD or a USB drive, without
> + needing to be installed to a hard disk. It is typically used as part
> + of installation media released by distributions.
> + </p>
> + <p>
> + Launchpad can build a limited variety of live filesystem images
> + using <tt>live-build</tt> and <tt>livecd-rootfs</tt>.
> + </p>
> + </div>
> +
> + <div metal:use-macro="context/@@launchpad_form/form">
> + <metal:formbody fill-slot="widgets">
> + <table class="form">
> + <tal:widget define="widget nocall:view/widgets/name">
> + <metal:block use-macro="context/@@launchpad_form/widget_row"/>
> + </tal:widget>
> + <tal:widget define="widget nocall:view/widgets/owner">
> + <metal:block use-macro="context/@@launchpad_form/widget_row"/>
> + </tal:widget>
> + <tal:widget define="widget nocall:view/widgets/distro_series">
> + <metal:block use-macro="context/@@launchpad_form/widget_row"/>
> + </tal:widget>
> + <tal:widget define="widget nocall:view/widgets/metadata">
> + <metal:block use-macro="context/@@launchpad_form/widget_row"/>
> + </tal:widget>
> + </table>
> + </metal:formbody>
> + </div>
> + </div>
> +</body>
> +</html>
>
> === added file 'lib/lp/soyuz/templates/livefsbuild-index.pt'
> --- lib/lp/soyuz/templates/livefsbuild-index.pt 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/templates/livefsbuild-index.pt 2014-06-17 15:19:20 +0000
> @@ -0,0 +1,217 @@
> +<html
> + xmlns="http://www.w3.org/1999/xhtml"
> + xmlns:tal="http://xml.zope.org/namespaces/tal"
> + xmlns:metal="http://xml.zope.org/namespaces/metal"
> + xmlns:i18n="http://xml.zope.org/namespaces/i18n"
> + metal:use-macro="view/macro:page/main_only"
> + i18n:domain="launchpad"
> +>
> +
> + <body>
> +
> + <tal:registering metal:fill-slot="registering">
> + created
> + <span tal:content="context/date_created/fmt:displaydate"
> + tal:attributes="title context/date_created/fmt:datetime"/>
> + </tal:registering>
> +
> + <div metal:fill-slot="main">
> +
> + <div class="yui-g">
> +
> + <div id="status" class="yui-u first">
> + <div class="portlet">
> + <div metal:use-macro="template/macros/status"/>
> + </div>
> + </div>
> +
> + <div id="details" class="yui-u">
> + <div class="portlet">
> + <div metal:use-macro="template/macros/details"/>
> + </div>
> + </div>
> +
> + </div> <!-- yui-g -->
> +
> + <div id="files" class="portlet" tal:condition="view/has_files">
> + <div metal:use-macro="template/macros/files"/>
> + </div>
> +
> + <div id="buildlog" class="portlet"
> + tal:condition="context/status/enumvalue:BUILDING">
> + <div metal:use-macro="template/macros/buildlog"/>
> + </div>
> +
> + </div> <!-- main -->
> +
> +
> +<metal:macros fill-slot="bogus">
> +
> + <metal:macro define-macro="details">
> + <tal:comment replace="nothing">
> + Details section.
> + </tal:comment>
> + <h2>Build details</h2>
> + <div class="two-column-list">
> + <dl>
> + <dt>Live filesystem:</dt>
> + <dd>
> + <tal:livefs replace="structure context/livefs/fmt:link"/>
> + </dd>
> + </dl>
> + <dl>
> + <dt>Archive:</dt>
> + <dd>
> + <span tal:replace="structure context/archive/fmt:link"/>
> + </dd>
> + </dl>
> + <dl>
> + <dt>Series:</dt>
> + <dd><a class="sprite distribution"
> + tal:define="series context/distro_series"
> + tal:attributes="href series/fmt:url"
> + tal:content="series/displayname"/>
> + </dd>
> + </dl>
> + <dl>
> + <dt>Architecture:</dt>
> + <dd><a class="sprite distribution"
> + tal:define="archseries context/distro_arch_series"
> + tal:attributes="href archseries/fmt:url"
> + tal:content="archseries/architecturetag"/>
> + </dd>
> + </dl>
> + <dl>
> + <dt>Pocket:</dt>
> + <dd><span tal:replace="context/pocket/title"/></dd>
> + </dl>
> + <dl>
> + <dt>Metadata overrides:</dt>
> + <dd>
> + <table class="listing compressed">
> + <tbody>
> + <tr tal:repeat="pair view/sorted_metadata_override_items">
> + <td tal:repeat="value pair" tal:content="value"/>
> + </tr>
> + </tbody>
> + </table>
> + </dd>
> + </dl>
> + </div>
> + </metal:macro>
> +
> + <metal:macro define-macro="status">
> + <tal:comment replace="nothing">
> + Status section.
> + </tal:comment>
> + <h2>Build status</h2>
> + <p>
> + <span tal:replace="structure context/image:icon" />
> + <span tal:attributes="
> + class string:buildstatus${context/status/name};"
> + tal:content="context/status/title"/>
> + <tal:building condition="context/status/enumvalue:BUILDING">
> + on <a tal:content="context/buildqueue_record/builder/title"
> + tal:attributes="href context/buildqueue_record/builder/fmt:url"/>
> + </tal:building>
> + <tal:built condition="context/builder">
> + on <a tal:content="context/builder/title"
> + tal:attributes="href context/builder/fmt:url"/>
> + </tal:built>
> + <tal:cancel define="link context/menu:context/cancel"
> + condition="link/enabled"
> + replace="structure link/fmt:link" />
> + </p>
> +
> + <ul>
> + <li tal:condition="context/dependencies">
> + Missing build dependencies: <em tal:content="context/dependencies"/>
> + </li>
> + <tal:reallypending condition="context/buildqueue_record">
> + <tal:pending condition="context/buildqueue_record/status/enumvalue:WAITING">
> + <li tal:define="eta context/buildqueue_record/getEstimatedJobStartTime">
> + Start <tal:eta replace="eta/fmt:approximatedate"/>
> + (<span tal:replace="context/buildqueue_record/lastscore"/>)
> + <a href="https://help.launchpad.net/Packaging/BuildScores"
> + target="_blank">What's this?</a>
> + </li>
> + </tal:pending>
> + </tal:reallypending>
> + <tal:started condition="context/date_started">
> + <li tal:condition="context/date_started">
> + Started <span
> + tal:define="start context/date_started"
> + tal:attributes="title start/fmt:datetime"
> + tal:content="start/fmt:displaydate"/>
> + </li>
> + </tal:started>
> + <tal:finish condition="not: context/date_finished">
> + <li tal:define="eta view/eta" tal:condition="view/eta">
> + Estimated finish <tal:eta replace="eta/fmt:approximatedate"/>
> + </li>
> + </tal:finish>
> +
> + <li tal:condition="context/date_finished">
> + Finished <span
> + tal:attributes="title context/date_finished/fmt:datetime"
> + tal:content="context/date_finished/fmt:displaydate"/>
> + <tal:duration condition="context/duration">
> + (took <span tal:replace="context/duration/fmt:exactduration"/>)
> + </tal:duration>
> + </li>
> + <li tal:define="file context/log"
> + tal:condition="file">
> + <a class="sprite download"
> + tal:attributes="href context/log_url">buildlog</a>
> + (<span tal:replace="file/content/filesize/fmt:bytes" />)
> + </li>
> + <li tal:define="file context/upload_log"
> + tal:condition="file">
> + <a class="sprite download"
> + tal:attributes="href context/upload_log_url">uploadlog</a>
> + (<span tal:replace="file/content/filesize/fmt:bytes" />)
> + </li>
> + </ul>
> +
> + <div
> + style="margin-top: 1.5em"
> + tal:define="link context/menu:context/rescore"
> + tal:condition="link/enabled"
> + >
> + <a tal:replace="structure link/fmt:link"/>
> + </div>
> + </metal:macro>
> +
> + <metal:macro define-macro="files">
> + <tal:comment replace="nothing">
> + Files section.
> + </tal:comment>
> + <h2>Built files</h2>
> + <p>Files resulting from this build:</p>
> + <ul>
> + <li tal:repeat="file view/files">
> + <a class="sprite download"
> + tal:content="file/filename"
> + tal:attributes="href file/http_url"/>
> + (<span tal:replace="file/content/filesize/fmt:bytes"/>)
> + </li>
> + </ul>
> + </metal:macro>
> +
> + <metal:macro define-macro="buildlog">
> + <tal:comment replace="nothing">
> + Buildlog section.
> + </tal:comment>
> + <h2>Buildlog</h2>
> + <div id="buildlog-tail" class="logtail"
> + tal:define="logtail context/buildqueue_record/logtail"
> + tal:content="structure logtail/fmt:text-to-html"/>
> + <p class="lesser" tal:condition="view/user">
> + Updated on <span tal:replace="structure view/user/fmt:local-time"/>
> + </p>
> + </metal:macro>
> +
> +</metal:macros>
> +
> + </body>
> +</html>
>
--
https://code.launchpad.net/~cjwatson/launchpad/livefs-browser/+merge/219505
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
References