~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/registry/browser/distroseries.py

Merge db-devel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
    'DistroSeriesView',
22
22
    ]
23
23
 
 
24
import apt_pkg
24
25
from lazr.restful.interface import copy_field
25
26
from zope.component import getUtility
26
27
from zope.event import notify
103
104
from lp.services.worlddata.interfaces.language import ILanguageSet
104
105
from lp.soyuz.browser.archive import PackageCopyingMixin
105
106
from lp.soyuz.browser.packagesearch import PackageSearchViewBase
 
107
from lp.soyuz.interfaces.distributionjob import (
 
108
    IDistroSeriesDifferenceJobSource,
 
109
    )
 
110
from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
106
111
from lp.soyuz.interfaces.queue import IPackageUploadSet
 
112
from lp.soyuz.model.packagecopyjob import specify_dsd_package
 
113
from lp.soyuz.model.queue import PackageUploadQueue
107
114
from lp.translations.browser.distroseries import (
108
115
    check_distroseries_translations_viewable,
109
116
    )
110
117
 
 
118
# DistroSeries statuses that benefit from mass package upgrade support.
 
119
UPGRADABLE_SERIES_STATUSES = [
 
120
    SeriesStatus.FUTURE,
 
121
    SeriesStatus.EXPERIMENTAL,
 
122
    SeriesStatus.DEVELOPMENT,
 
123
    ]
 
124
 
111
125
 
112
126
class DistroSeriesNavigation(GetitemNavigation, BugTargetTraversalMixin,
113
127
    StructuralSubscriptionTargetTraversalMixin):
171
185
    def traverse_queue(self, id):
172
186
        return getUtility(IPackageUploadSet).get(id)
173
187
 
174
 
    @stepthrough('+difference')
175
 
    def traverse_difference(self, name):
176
 
        dsd_source = getUtility(IDistroSeriesDifferenceSource)
177
 
        return dsd_source.getByDistroSeriesAndName(self.context, name)
178
 
 
179
188
 
180
189
class DistroSeriesBreadcrumb(Breadcrumb):
181
190
    """Builds a breadcrumb for an `IDistroSeries`."""
349
358
            self.context.datereleased = UTC_NOW
350
359
 
351
360
 
352
 
class DistroSeriesView(LaunchpadView, MilestoneOverlayMixin):
 
361
class DerivedDistroSeriesMixin:
 
362
 
 
363
    @cachedproperty
 
364
    def has_unique_parent(self):
 
365
        return len(self.context.getParentSeries()) == 1
 
366
 
 
367
    @cachedproperty
 
368
    def unique_parent(self):
 
369
        if self.has_unique_parent:
 
370
            return self.context.getParentSeries()[0]
 
371
        else:
 
372
            None
 
373
 
 
374
    @cachedproperty
 
375
    def number_of_parents(self):
 
376
        return len(self.context.getParentSeries())
 
377
 
 
378
    def getParentName(self, multiple_parent_default=None):
 
379
        if self.has_unique_parent:
 
380
            return ("parent series '%s'" %
 
381
                self.unique_parent.displayname)
 
382
        else:
 
383
            if multiple_parent_default is not None:
 
384
                return multiple_parent_default
 
385
            else:
 
386
                return 'a parent series'
 
387
 
 
388
 
 
389
class DistroSeriesView(LaunchpadView, MilestoneOverlayMixin,
 
390
                       DerivedDistroSeriesMixin):
353
391
 
354
392
    def initialize(self):
355
393
        super(DistroSeriesView, self).initialize()
620
658
 
621
659
    @property
622
660
    def is_derived_series_feature_enabled(self):
623
 
        return getFeatureFlag("soyuz.derived-series-ui.enabled") is not None
 
661
        return getFeatureFlag("soyuz.derived_series_ui.enabled") is not None
624
662
 
625
663
    @property
626
664
    def next_url(self):
663
701
        higher_term = SimpleTerm(
664
702
            HIGHER_VERSION_THAN_PARENT,
665
703
            HIGHER_VERSION_THAN_PARENT,
666
 
            "Ignored packages with a higher version than in '%s'"
 
704
            "Ignored packages with a higher version than in %s"
667
705
                % parent_name)
668
706
        voc.insert(2, higher_term)
669
707
    return SimpleVocabulary(tuple(voc))
696
734
 
697
735
 
698
736
class DistroSeriesDifferenceBaseView(LaunchpadFormView,
699
 
                                     PackageCopyingMixin):
 
737
                                     PackageCopyingMixin,
 
738
                                     DerivedDistroSeriesMixin):
700
739
    """Base class for all pages presenting differences between
701
740
    a derived series and its parent."""
702
741
    schema = IDifferencesFormSchema
717
756
 
718
757
    def initialize(self):
719
758
        """Redirect to the derived series if the feature is not enabled."""
720
 
        if not getFeatureFlag('soyuz.derived-series-ui.enabled'):
 
759
        if not getFeatureFlag('soyuz.derived_series_ui.enabled'):
721
760
            self.request.response.redirect(canonical_url(self.context))
722
761
            return
723
762
 
731
770
        return NotImplementedError()
732
771
 
733
772
    def setupPackageFilterRadio(self):
 
773
        if self.has_unique_parent:
 
774
            parent_name = "'%s'" % self.unique_parent.displayname
 
775
        else:
 
776
            parent_name = 'parent'
734
777
        return form.Fields(Choice(
735
778
            __name__='package_type',
736
779
            vocabulary=make_package_type_vocabulary(
737
 
                self.context.previous_series.displayname,
 
780
                parent_name,
738
781
                self.search_higher_parent_option),
739
782
            default=DEFAULT_PACKAGE_TYPE,
740
783
            required=True))
751
794
            self.form_fields)
752
795
        check_permission('launchpad.Edit', self.context)
753
796
        terms = [
754
 
            SimpleTerm(diff, diff.source_package_name.name,
755
 
                diff.source_package_name.name)
756
 
                for diff in self.cached_differences.batch]
 
797
            SimpleTerm(diff, diff.id)
 
798
                    for diff in self.cached_differences.batch]
757
799
        diffs_vocabulary = SimpleVocabulary(terms)
758
800
        choice = self.form_fields['selected_differences'].field.value_type
759
801
        choice.vocabulary = diffs_vocabulary
814
856
        return (has_perm and
815
857
                self.cached_differences.batch.total() > 0)
816
858
 
 
859
    @cachedproperty
 
860
    def pending_syncs(self):
 
861
        """Pending synchronization jobs for this distroseries.
 
862
 
 
863
        :return: A dict mapping (name, version) package specifications to
 
864
            pending sync jobs.
 
865
        """
 
866
        job_source = getUtility(IPlainPackageCopyJobSource)
 
867
        return job_source.getPendingJobsPerPackage(self.context)
 
868
 
 
869
    @cachedproperty
 
870
    def pending_dsd_updates(self):
 
871
        """Pending `DistroSeriesDifference` update jobs.
 
872
 
 
873
        :return: A `set` of `DistroSeriesDifference`s that have pending
 
874
            `DistroSeriesDifferenceJob`s.
 
875
        """
 
876
        job_source = getUtility(IDistroSeriesDifferenceJobSource)
 
877
        return job_source.getPendingJobsForDifferences(
 
878
            self.context, self.cached_differences.batch)
 
879
 
 
880
    def hasPendingDSDUpdate(self, dsd):
 
881
        """Have there been changes that `dsd` is still being updated for?"""
 
882
        return dsd in self.pending_dsd_updates
 
883
 
 
884
    def hasPendingSync(self, dsd):
 
885
        """Is there a package-copying job pending to resolve `dsd`?"""
 
886
        return self.pending_syncs.get(specify_dsd_package(dsd)) is not None
 
887
 
 
888
    def isNewerThanParent(self, dsd):
 
889
        """Is the child's version of this package newer than the parent's?
 
890
 
 
891
        If it is, there's no point in offering to sync it.
 
892
 
 
893
        Any version is considered "newer" than a missing version.
 
894
        """
 
895
        # This is trickier than it looks: versions are not totally
 
896
        # ordered.  Two non-identical versions may compare as equal.
 
897
        # Only consider cases where the child's version is conclusively
 
898
        # newer, not where the relationship is in any way unclear.
 
899
        if dsd.parent_source_version is None:
 
900
            # There is nothing to sync; the child is up to date and if
 
901
            # anything needs updating, it's the parent.
 
902
            return True
 
903
        if dsd.source_version is None:
 
904
            # The child doesn't have this package.  Treat that as the
 
905
            # parent being newer.
 
906
            return False
 
907
        comparison = apt_pkg.VersionCompare(
 
908
            dsd.parent_source_version, dsd.source_version)
 
909
        return comparison < 0
 
910
 
 
911
    def canRequestSync(self, dsd):
 
912
        """Does it make sense to request a sync for this difference?"""
 
913
        # There are two conditions for this: it doesn't make sense to
 
914
        # sync if the child's version of the package is newer than the
 
915
        # parent's version, or if there is already a sync pending.
 
916
        return (
 
917
            not self.isNewerThanParent(dsd) and not self.hasPendingSync(dsd))
 
918
 
 
919
    def describeJobs(self, dsd):
 
920
        """Describe any jobs that may be pending for `dsd`.
 
921
 
 
922
        Shows "synchronizing..." if the entry is being synchronized, and
 
923
        "updating..." if the DSD is being updated with package changes.
 
924
 
 
925
        :param dsd: A `DistroSeriesDifference` on the page.
 
926
        :return: An HTML text describing work that is pending or in
 
927
            progress for `dsd`; or None.
 
928
        """
 
929
        has_pending_dsd_update = self.hasPendingDSDUpdate(dsd)
 
930
        has_pending_sync = self.hasPendingSync(dsd)
 
931
        if not has_pending_dsd_update and not has_pending_sync:
 
932
            return None
 
933
 
 
934
        description = []
 
935
        if has_pending_dsd_update:
 
936
            description.append("updating")
 
937
        if has_pending_sync:
 
938
            description.append("synchronizing")
 
939
        return " and ".join(description) + "&hellip;"
 
940
 
817
941
    @property
818
942
    def specified_name_filter(self):
819
943
        """If specified, return the name filter from the GET form data."""
840
964
    @cachedproperty
841
965
    def cached_differences(self):
842
966
        """Return a batch navigator of filtered results."""
843
 
        if self.specified_package_type == NON_IGNORED:
844
 
            status=(
845
 
                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,)
846
 
            child_version_higher = False
847
 
        elif self.specified_package_type == IGNORED:
848
 
            status=(
849
 
                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
850
 
            child_version_higher = False
851
 
        elif self.specified_package_type == HIGHER_VERSION_THAN_PARENT:
852
 
            status=(
853
 
                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
854
 
            child_version_higher = True
855
 
        elif self.specified_package_type == RESOLVED:
856
 
            status=DistroSeriesDifferenceStatus.RESOLVED
857
 
            child_version_higher = False
858
 
        else:
859
 
            raise AssertionError('specified_package_type unknown')
 
967
        package_type_dsd_status = {
 
968
            NON_IGNORED: (
 
969
                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,),
 
970
            IGNORED: DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
 
971
            HIGHER_VERSION_THAN_PARENT: (
 
972
                DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT),
 
973
            RESOLVED: DistroSeriesDifferenceStatus.RESOLVED,
 
974
        }
 
975
 
 
976
        status = package_type_dsd_status[self.specified_package_type]
 
977
        child_version_higher = (
 
978
            self.specified_package_type == HIGHER_VERSION_THAN_PARENT)
860
979
 
861
980
        differences = getUtility(
862
981
            IDistroSeriesDifferenceSource).getForDistroSeries(
863
 
                self.context,
864
 
                difference_type = self.differences_type,
 
982
                self.context, difference_type=self.differences_type,
865
983
                source_package_name_filter=self.specified_name_filter,
866
 
                status=status,
867
 
                child_version_higher=child_version_higher)
 
984
                status=status, child_version_higher=child_version_higher)
868
985
        return BatchNavigator(differences, self.request)
869
986
 
870
987
    @cachedproperty
882
999
            differences = getUtility(
883
1000
                IDistroSeriesDifferenceSource).getForDistroSeries(
884
1001
                    self.context,
885
 
                    difference_type = self.differences_type,
 
1002
                    difference_type=self.differences_type,
886
1003
                    status=(
887
1004
                        DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
888
1005
                        DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT))
901
1018
 
902
1019
    def initialize(self):
903
1020
        # Update the label for sync action.
 
1021
        if self.has_unique_parent:
 
1022
            parent_name = "'%s'" % self.unique_parent.displayname
 
1023
        else:
 
1024
            parent_name = 'Parent'
904
1025
        self.initialize_sync_label(
905
1026
            "Sync Selected %s Versions into %s" % (
906
 
                self.context.previous_series.displayname,
 
1027
                parent_name,
907
1028
                self.context.displayname,
908
1029
                ))
909
1030
        super(DistroSeriesLocalDifferencesView, self).initialize()
912
1033
    def explanation(self):
913
1034
        return structured(
914
1035
            "Source packages shown here are present in both %s "
915
 
            "and the parent series, %s, but are different somehow. "
 
1036
            "and %s, but are different somehow. "
916
1037
            "Changes could be in either or both series so check the "
917
 
            "versions (and the diff if necessary) before syncing the %s "
 
1038
            "versions (and the diff if necessary) before syncing the parent "
918
1039
            'version (<a href="/+help/soyuz/derived-series-syncing.html" '
919
 
            'target="help">Read more about syncing from the parent series'
 
1040
            'target="help">Read more about syncing from a parent series'
920
1041
            '</a>).',
921
1042
            self.context.displayname,
922
 
            self.context.previous_series.fullseriesname,
923
 
            self.context.previous_series.displayname)
 
1043
            self.getParentName())
924
1044
 
925
1045
    @property
926
1046
    def label(self):
927
1047
        return (
928
 
            "Source package differences between '%s' and "
929
 
            "parent series '%s'" % (
 
1048
            "Source package differences between '%s' and"
 
1049
            " %s" % (
930
1050
                self.context.displayname,
931
 
                self.context.previous_series.displayname,
 
1051
                self.getParentName(multiple_parent_default='parent series'),
932
1052
                ))
933
1053
 
934
 
    @action(_("Update"), name="update")
935
 
    def update_action(self, action, data):
936
 
        """Simply re-issue the form with the new values."""
937
 
        pass
938
 
 
939
1054
    @action(_("Sync Sources"), name="sync", validator='validate_sync',
940
1055
            condition='canPerformSync')
941
1056
    def sync_sources(self, action, data):
942
1057
        self._sync_sources(action, data)
943
1058
 
 
1059
    def getUpgrades(self):
 
1060
        """Find straightforward package upgrades.
 
1061
 
 
1062
        These are updates for packages that this distroseries shares
 
1063
        with a parent series, for which there have been updates in the
 
1064
        parent, and which do not have any changes in this series that
 
1065
        might complicate a sync.
 
1066
 
 
1067
        :return: A result set of `DistroSeriesDifference`s.
 
1068
        """
 
1069
        return getUtility(IDistroSeriesDifferenceSource).getSimpleUpgrades(
 
1070
            self.context)
 
1071
 
 
1072
    @action(_("Upgrade Packages"), name="upgrade", condition='canUpgrade')
 
1073
    def upgrade(self, action, data):
 
1074
        """Request synchronization of straightforward package upgrades."""
 
1075
        self.requestUpgrades()
 
1076
 
 
1077
    def requestUpgrades(self):
 
1078
        """Request sync of packages that can be easily upgraded."""
 
1079
        target_distroseries = self.context
 
1080
        target_archive = target_distroseries.main_archive
 
1081
        differences_by_archive = (
 
1082
            getUtility(IDistroSeriesDifferenceSource)
 
1083
                .collateDifferencesByParentArchive(self.getUpgrades()))
 
1084
        for source_archive, differences in differences_by_archive.iteritems():
 
1085
            source_package_info = [
 
1086
                (difference.source_package_name.name,
 
1087
                 difference.parent_source_version)
 
1088
                for difference in differences]
 
1089
            getUtility(IPlainPackageCopyJobSource).create(
 
1090
                source_package_info, source_archive, target_archive,
 
1091
                target_distroseries, PackagePublishingPocket.UPDATES)
 
1092
        self.request.response.addInfoNotification(
 
1093
            (u"Upgrades of {context.displayname} packages have been "
 
1094
             u"requested. Please give Launchpad some time to complete "
 
1095
             u"these.").format(context=self.context))
 
1096
 
 
1097
    def canUpgrade(self, action=None):
 
1098
        """Should the form offer a packages upgrade?"""
 
1099
        if getFeatureFlag("soyuz.derived_series_sync.enabled") is None:
 
1100
            return False
 
1101
        elif self.context.status not in UPGRADABLE_SERIES_STATUSES:
 
1102
            # A feature freeze precludes blanket updates.
 
1103
            return False
 
1104
        elif self.getUpgrades().is_empty():
 
1105
            # There are no simple updates to perform.
 
1106
            return False
 
1107
        else:
 
1108
            queue = PackageUploadQueue(self.context, None)
 
1109
            return check_permission("launchpad.Edit", queue)
 
1110
 
944
1111
 
945
1112
class DistroSeriesMissingPackagesView(DistroSeriesDifferenceBaseView,
946
1113
                                      LaunchpadFormView):
956
1123
    def initialize(self):
957
1124
        # Update the label for sync action.
958
1125
        self.initialize_sync_label(
959
 
            "Include Selected packages into into %s" % (
 
1126
            "Include Selected packages into %s" % (
960
1127
                self.context.displayname,
961
1128
                ))
962
1129
        super(DistroSeriesMissingPackagesView, self).initialize()
965
1132
    def explanation(self):
966
1133
        return structured(
967
1134
            "Packages that are listed here are those that have been added to "
968
 
            "the specific packages %s that were used to create %s. They are "
969
 
            "listed here so you can consider including them in %s.",
970
 
            self.context.previous_series.displayname,
 
1135
            "the specific packages in %s that were used to create %s. "
 
1136
            "They are listed here so you can consider including them in %s.",
 
1137
            self.getParentName(),
971
1138
            self.context.displayname,
972
1139
            self.context.displayname)
973
1140
 
974
1141
    @property
975
1142
    def label(self):
976
1143
        return (
977
 
            "Packages in parent series '%s' but not in '%s'" % (
978
 
                self.context.previous_series.displayname,
 
1144
            "Packages in %s but not in '%s'" % (
 
1145
                self.getParentName(),
979
1146
                self.context.displayname,
980
1147
                ))
981
1148
 
982
 
    @action(_("Update"), name="update")
983
 
    def update_action(self, action, data):
984
 
        """Simply re-issue the form with the new values."""
985
 
        pass
986
 
 
987
1149
    @action(_("Sync Sources"), name="sync", validator='validate_sync',
988
1150
            condition='canPerformSync')
989
1151
    def sync_sources(self, action, data):
1008
1170
    def explanation(self):
1009
1171
        return structured(
1010
1172
            "Packages that are listed here are those that have been added to "
1011
 
            "%s but are not yet part of the parent series %s.",
 
1173
            "%s but are not yet part of %s.",
1012
1174
            self.context.displayname,
1013
 
            self.context.previous_series.displayname)
 
1175
            self.getParentName())
1014
1176
 
1015
1177
    @property
1016
1178
    def label(self):
1017
1179
        return (
1018
 
            "Packages in '%s' but not in parent series '%s'" % (
 
1180
            "Packages in '%s' but not in %s" % (
1019
1181
                self.context.displayname,
1020
 
                self.context.previous_series.displayname,
 
1182
                self.getParentName(),
1021
1183
                ))
1022
1184
 
1023
 
    @action(_("Update"), name="update")
1024
 
    def update_action(self, action, data):
1025
 
        """Simply re-issue the form with the new values."""
1026
 
        pass
1027
 
 
1028
1185
    def canPerformSync(self, *args):
1029
1186
        return False