~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/registry/model/distroseriesdifference.py

Merge db-devel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
9
9
    'DistroSeriesDifference',
10
10
    ]
11
11
 
 
12
from collections import defaultdict
12
13
from itertools import chain
13
14
from operator import itemgetter
14
15
 
 
16
import apt_pkg
15
17
from debian.changelog import (
16
18
    Changelog,
17
19
    Version,
18
20
    )
19
21
from lazr.enum import DBItem
20
22
from sqlobject import StringCol
 
23
from storm.exceptions import NotOneError
21
24
from storm.expr import (
22
25
    And,
23
 
    compile as storm_compile,
 
26
    Column,
24
27
    Desc,
25
 
    SQL,
 
28
    Table,
26
29
    )
27
 
from storm.info import ClassAlias
28
30
from storm.locals import (
29
31
    Int,
30
32
    Reference,
40
42
from canonical.launchpad.components.decoratedresultset import (
41
43
    DecoratedResultSet,
42
44
    )
43
 
from lp.services.messages.model.message import Message
44
45
from canonical.launchpad.interfaces.lpstorm import (
45
46
    IMasterStore,
46
47
    IStore,
52
53
    )
53
54
from lp.registry.errors import (
54
55
    DistroSeriesDifferenceError,
 
56
    MultipleParentsForDerivedSeriesError,
55
57
    NotADerivedSeriesError,
56
58
    )
57
59
from lp.registry.interfaces.distroseriesdifference import (
61
63
from lp.registry.interfaces.distroseriesdifferencecomment import (
62
64
    IDistroSeriesDifferenceCommentSource,
63
65
    )
 
66
from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
64
67
from lp.registry.interfaces.person import IPersonSet
65
68
from lp.registry.model.distroseries import DistroSeries
66
69
from lp.registry.model.distroseriesdifferencecomment import (
70
73
from lp.registry.model.sourcepackagename import SourcePackageName
71
74
from lp.services.database import bulk
72
75
from lp.services.database.stormbase import StormBase
 
76
from lp.services.messages.model.message import (
 
77
    Message,
 
78
    MessageChunk,
 
79
    )
73
80
from lp.services.propertycache import (
74
81
    cachedproperty,
75
82
    clear_property_cache,
86
93
from lp.soyuz.model.distroseriessourcepackagerelease import (
87
94
    DistroSeriesSourcePackageRelease,
88
95
    )
 
96
from lp.soyuz.model.packageset import Packageset
89
97
from lp.soyuz.model.publishing import SourcePackagePublishingHistory
90
98
from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
91
99
 
106
114
        )
107
115
    conditions = And(
108
116
        DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds),
109
 
        DistroSeries.id == DistroSeriesDifference.derived_series_id,
110
117
        SourcePackagePublishingHistory.archiveID == Archive.id,
111
118
        SourcePackagePublishingHistory.sourcepackagereleaseID == (
112
119
            SourcePackageRelease.id),
116
123
        )
117
124
    # Check in the parent archive or the child?
118
125
    if in_parent:
119
 
        ParentDistroSeries = ClassAlias(DistroSeries)
120
126
        conditions = And(
121
127
            conditions,
122
 
            ParentDistroSeries.id == DistroSeries.previous_seriesID,
123
 
            Archive.distributionID == ParentDistroSeries.distributionID,
 
128
            DistroSeries.id == DistroSeriesDifference.parent_series_id,
 
129
            Archive.distributionID == DistroSeries.distributionID,
124
130
            Archive.purpose == ArchivePurpose.PRIMARY,
125
131
            )
126
132
    else:
127
133
        conditions = And(
128
134
            conditions,
 
135
            DistroSeries.id == DistroSeriesDifference.derived_series_id,
129
136
            Archive.distributionID == DistroSeries.distributionID,
130
137
            Archive.purpose == ArchivePurpose.PRIMARY,
131
138
            )
182
189
    return DecoratedResultSet(comments, itemgetter(0))
183
190
 
184
191
 
 
192
def packagesets(dsds, in_parent):
 
193
    """Return the packagesets for the given dsds inside the parent or
 
194
    the derived `DistroSeries`.
 
195
 
 
196
    Returns a dict with the corresponding packageset list for each dsd id.
 
197
 
 
198
    :param dsds: An iterable of `DistroSeriesDifference` instances.
 
199
    :param in_parent: A boolean indicating if we should look in the parent
 
200
        series' archive instead of the derived series' archive.
 
201
    """
 
202
    if len(dsds) == 0:
 
203
        return {}
 
204
 
 
205
    PackagesetSources = Table("PackageSetSources")
 
206
    FlatPackagesetInclusion = Table("FlatPackagesetInclusion")
 
207
 
 
208
    tables = IStore(Packageset).using(
 
209
        DistroSeriesDifference, Packageset,
 
210
        PackagesetSources, FlatPackagesetInclusion)
 
211
    results = tables.find(
 
212
        (DistroSeriesDifference.id, Packageset),
 
213
        Column("packageset", PackagesetSources) == (
 
214
            Column("child", FlatPackagesetInclusion)),
 
215
        Packageset.distroseries_id == (
 
216
            DistroSeriesDifference.parent_series_id if in_parent else
 
217
            DistroSeriesDifference.derived_series_id),
 
218
        Column("parent", FlatPackagesetInclusion) == Packageset.id,
 
219
        Column("sourcepackagename", PackagesetSources) == (
 
220
            DistroSeriesDifference.source_package_name_id),
 
221
        DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds))
 
222
    results = results.order_by(
 
223
        Column("sourcepackagename", PackagesetSources),
 
224
        Packageset.name)
 
225
 
 
226
    grouped = defaultdict(list)
 
227
    for dsd_id, packageset in results:
 
228
        grouped[dsd_id].append(packageset)
 
229
    return grouped
 
230
 
 
231
 
 
232
def message_chunks(messages):
 
233
    """Return the message chunks for the given messages.
 
234
 
 
235
    Returns a dict with the list of `MessageChunk` for each message id.
 
236
 
 
237
    :param messages: An iterable of `Message` instances.
 
238
    """
 
239
    store = IStore(MessageChunk)
 
240
    chunks = store.find(MessageChunk,
 
241
        MessageChunk.messageID.is_in(m.id for m in messages))
 
242
 
 
243
    grouped = defaultdict(list)
 
244
    for chunk in chunks:
 
245
        grouped[chunk.messageID].append(chunk)
 
246
    return grouped
 
247
 
 
248
 
185
249
class DistroSeriesDifference(StormBase):
186
250
    """See `DistroSeriesDifference`."""
187
251
    implements(IDistroSeriesDifference)
194
258
    derived_series = Reference(
195
259
        derived_series_id, 'DistroSeries.id')
196
260
 
 
261
    parent_series_id = Int(name='parent_series', allow_none=False)
 
262
    parent_series = Reference(parent_series_id, 'DistroSeries.id')
 
263
 
197
264
    source_package_name_id = Int(
198
265
        name='source_package_name', allow_none=False)
199
266
    source_package_name = Reference(
219
286
    base_version = StringCol(dbName='base_version', notNull=False)
220
287
 
221
288
    @staticmethod
222
 
    def new(derived_series, source_package_name):
 
289
    def new(derived_series, source_package_name, parent_series=None):
223
290
        """See `IDistroSeriesDifferenceSource`."""
224
 
        if not derived_series.is_derived_series:
225
 
            raise NotADerivedSeriesError()
 
291
        if parent_series is None:
 
292
            try:
 
293
                dsps = getUtility(IDistroSeriesParentSet)
 
294
                dsp = dsps.getByDerivedSeries(
 
295
                    derived_series).one()
 
296
            except NotOneError:
 
297
                raise MultipleParentsForDerivedSeriesError()
 
298
            else:
 
299
                if dsp is None:
 
300
                    raise NotADerivedSeriesError()
 
301
                else:
 
302
                    parent_series = dsp.parent_series
226
303
 
227
304
        store = IMasterStore(DistroSeriesDifference)
228
305
        diff = DistroSeriesDifference()
229
306
        diff.derived_series = derived_series
 
307
        diff.parent_series = parent_series
230
308
        diff.source_package_name = source_package_name
231
309
 
232
310
        # The status and type is set to default values - they will be
240
318
    @staticmethod
241
319
    def getForDistroSeries(
242
320
        distro_series,
243
 
        difference_type=DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
 
321
        difference_type=None,
244
322
        source_package_name_filter=None,
245
323
        status=None,
246
 
        child_version_higher=False):
 
324
        child_version_higher=False,
 
325
        parent_series=None):
247
326
        """See `IDistroSeriesDifferenceSource`."""
 
327
        if difference_type is None:
 
328
            difference_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
248
329
        if status is None:
249
330
            status = (
250
331
                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
258
339
            DistroSeriesDifference.status.is_in(status),
259
340
            DistroSeriesDifference.source_package_name ==
260
341
                SourcePackageName.id,
261
 
         ]
 
342
        ]
 
343
 
 
344
        if parent_series:
 
345
            conditions.extend([
 
346
               DistroSeriesDifference.parent_series == parent_series.id])
262
347
 
263
348
        if source_package_name_filter:
264
349
            conditions.extend([
310
395
                    parent_source_pubs_for_release.itervalues()),
311
396
                ("sourcepackagereleaseID",))
312
397
 
 
398
            # Get packagesets and parent_packagesets for each DSD.
 
399
            dsd_packagesets = packagesets(dsds, in_parent=False)
 
400
            dsd_parent_packagesets = packagesets(dsds, in_parent=True)
 
401
 
 
402
            # Cache latest messages contents (MessageChunk).
 
403
            messages = bulk.load_related(
 
404
                Message, latest_comments, ['message_id'])
 
405
            chunks = message_chunks(messages)
 
406
            for msg in messages:
 
407
                cache = get_property_cache(msg)
 
408
                cache.text_contents = Message.chunks_text(
 
409
                    chunks.get(msg.id, []))
 
410
 
313
411
            for dsd in dsds:
314
412
                spn_id = dsd.source_package_name_id
315
413
                cache = get_property_cache(dsd)
316
414
                cache.source_pub = source_pubs.get(spn_id)
317
415
                cache.parent_source_pub = parent_source_pubs.get(spn_id)
 
416
                cache.packagesets = dsd_packagesets.get(dsd.id)
 
417
                cache.parent_packagesets = dsd_parent_packagesets.get(dsd.id)
318
418
                if spn_id in source_pubs_for_release:
319
419
                    spph = source_pubs_for_release[spn_id]
320
420
                    cache.source_package_release = (
327
427
                    spph = parent_source_pubs_for_release[spn_id]
328
428
                    cache.parent_source_package_release = (
329
429
                        DistroSeriesSourcePackageRelease(
330
 
                            dsd.derived_series.previous_series,
331
 
                            spph.sourcepackagerelease))
 
430
                            dsd.parent_series, spph.sourcepackagerelease))
332
431
                else:
333
432
                    cache.parent_source_package_release = None
334
433
                cache.latest_comment = latest_comment_by_dsd_id.get(dsd.id)
361
460
            differences, pre_iter_hook=eager_load)
362
461
 
363
462
    @staticmethod
364
 
    def getByDistroSeriesAndName(distro_series, source_package_name):
 
463
    def getByDistroSeriesNameAndParentSeries(distro_series,
 
464
                                             source_package_name,
 
465
                                             parent_series):
365
466
        """See `IDistroSeriesDifferenceSource`."""
 
467
 
366
468
        return IStore(DistroSeriesDifference).find(
367
469
            DistroSeriesDifference,
368
470
            DistroSeriesDifference.derived_series == distro_series,
 
471
            DistroSeriesDifference.parent_series == parent_series,
369
472
            DistroSeriesDifference.source_package_name == (
370
473
                SourcePackageName.id),
371
474
            SourcePackageName.name == source_package_name).one()
372
475
 
 
476
    @staticmethod
 
477
    def getSimpleUpgrades(distro_series):
 
478
        """See `IDistroSeriesDifferenceSource`.
 
479
 
 
480
        Eager-load related `ISourcePackageName` records.
 
481
        """
 
482
        differences = IStore(DistroSeriesDifference).find(
 
483
            (DistroSeriesDifference, SourcePackageName),
 
484
            DistroSeriesDifference.derived_series == distro_series,
 
485
            DistroSeriesDifference.difference_type ==
 
486
                DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
 
487
            DistroSeriesDifference.status ==
 
488
                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
 
489
            DistroSeriesDifference.parent_source_version !=
 
490
                DistroSeriesDifference.base_version,
 
491
            DistroSeriesDifference.source_version ==
 
492
                DistroSeriesDifference.base_version,
 
493
            SourcePackageName.id ==
 
494
                DistroSeriesDifference.source_package_name_id)
 
495
        return DecoratedResultSet(differences, itemgetter(0))
 
496
 
 
497
    @staticmethod
 
498
    def collateDifferencesByParentArchive(differences):
 
499
        by_archive = dict()
 
500
        for difference in differences:
 
501
            archive = difference.parent_series.main_archive
 
502
            if archive in by_archive:
 
503
                by_archive[archive].append(difference)
 
504
            else:
 
505
                by_archive[archive] = [difference]
 
506
        return by_archive
 
507
 
373
508
    @cachedproperty
374
509
    def source_pub(self):
375
510
        """See `IDistroSeriesDifference`."""
384
519
        """Helper to keep source_pub/parent_source_pub DRY."""
385
520
        distro_series = self.derived_series
386
521
        if for_parent:
387
 
            distro_series = self.derived_series.previous_series
 
522
            distro_series = self.parent_series
388
523
 
389
524
        pubs = distro_series.getPublishedSources(
390
525
            self.source_package_name, include_pending=True)
399
534
    def base_source_pub(self):
400
535
        """See `IDistroSeriesDifference`."""
401
536
        if self.base_version is not None:
402
 
            parent = self.derived_series.previous_series
 
537
            parent = self.parent_series
403
538
            result = parent.main_archive.getPublishedSources(
404
539
                name=self.source_package_name.name,
405
540
                version=self.base_version).first()
421
556
    @property
422
557
    def title(self):
423
558
        """See `IDistroSeriesDifference`."""
424
 
        parent_name = self.derived_series.previous_series.displayname
 
559
        parent_name = self.parent_series.displayname
425
560
        return ("Difference between distroseries '%(parent_name)s' and "
426
561
                "'%(derived_name)s' for package '%(pkg_name)s' "
427
562
                "(%(parent_version)s/%(source_version)s)" % {
466
601
        """See `IDistroSeriesDifference`."""
467
602
        return self._getPackageDiffURL(self.parent_package_diff)
468
603
 
469
 
    def getPackageSets(self):
 
604
    @cachedproperty
 
605
    def packagesets(self):
470
606
        """See `IDistroSeriesDifference`."""
471
607
        if self.derived_series is not None:
472
 
            return getUtility(IPackagesetSet).setsIncludingSource(
473
 
                self.source_package_name, self.derived_series)
 
608
            return list(getUtility(IPackagesetSet).setsIncludingSource(
 
609
                self.source_package_name, self.derived_series))
474
610
        else:
475
611
            return []
476
612
 
477
 
    def getParentPackageSets(self):
 
613
    @cachedproperty
 
614
    def parent_packagesets(self):
478
615
        """See `IDistroSeriesDifference`."""
479
 
        has_previous_series = self.derived_series is not None and (
480
 
            self.derived_series.previous_series is not None)
481
 
        if has_previous_series:
482
 
            return getUtility(IPackagesetSet).setsIncludingSource(
483
 
                self.source_package_name,
484
 
                self.derived_series.previous_series)
485
 
        else:
486
 
            return []
 
616
        return list(getUtility(IPackagesetSet).setsIncludingSource(
 
617
            self.source_package_name, self.parent_series))
487
618
 
488
619
    @property
489
620
    def package_diff_status(self):
504
635
    @cachedproperty
505
636
    def parent_source_package_release(self):
506
637
        return self._package_release(
507
 
            self.derived_series.previous_series,
508
 
            self.parent_source_version)
 
638
            self.parent_series, self.parent_source_version)
509
639
 
510
640
    @cachedproperty
511
641
    def source_package_release(self):
512
642
        return self._package_release(
513
 
            self.derived_series,
514
 
            self.source_version)
 
643
            self.derived_series, self.source_version)
515
644
 
516
645
    def _package_release(self, distro_series, version):
517
646
        statuses = (
533
662
            return DistroSeriesSourcePackageRelease(
534
663
                distro_series, pub.sourcepackagerelease)
535
664
 
536
 
    def update(self):
 
665
    def update(self, manual=False):
537
666
        """See `IDistroSeriesDifference`."""
538
667
        # Updating is expected to be a heavy operation (not called
539
668
        # during requests). We clear the cache beforehand - even though
543
672
        # update() (like the tests for this method do).
544
673
        clear_property_cache(self)
545
674
        self._updateType()
546
 
        updated = self._updateVersionsAndStatus()
 
675
        updated = self._updateVersionsAndStatus(manual=manual)
547
676
        if updated is True:
548
677
            self._setPackageDiffs()
549
678
        return updated
564
693
        if new_type != self.difference_type:
565
694
            self.difference_type = new_type
566
695
 
567
 
    def _updateVersionsAndStatus(self):
 
696
    def _updateVersionsAndStatus(self, manual):
568
697
        """Helper for the update() interface method.
569
698
 
570
699
        Check whether the status of this difference should be updated.
 
700
 
 
701
        :param manual: Boolean, True if this is a user-requested change.
 
702
            This overrides auto-blacklisting.
571
703
        """
 
704
        # XXX 2011-05-20 bigjools bug=785657
 
705
        # This method needs updating to use some sort of state
 
706
        # transition dictionary instead of this crazy mess of
 
707
        # conditions.
 
708
 
572
709
        updated = False
573
710
        new_source_version = new_parent_source_version = None
574
711
        if self.source_pub:
575
712
            new_source_version = self.source_pub.source_package_version
576
 
            if self.source_version != new_source_version:
 
713
            if self.source_version is None or apt_pkg.VersionCompare(
 
714
                    self.source_version, new_source_version) != 0:
577
715
                self.source_version = new_source_version
578
716
                updated = True
579
717
                # If the derived version has change and the previous version
584
722
        if self.parent_source_pub:
585
723
            new_parent_source_version = (
586
724
                self.parent_source_pub.source_package_version)
587
 
            if self.parent_source_version != new_parent_source_version:
 
725
            if self.parent_source_version is None or apt_pkg.VersionCompare(
 
726
                    self.parent_source_version,
 
727
                    new_parent_source_version) != 0:
588
728
                self.parent_source_version = new_parent_source_version
589
729
                updated = True
590
730
 
 
731
        if not self.source_pub or not self.parent_source_pub:
 
732
            # This is unlikely to happen in reality but return early so
 
733
            # that bad data cannot make us OOPS.
 
734
            return updated
 
735
 
591
736
        # If this difference was resolved but now the versions don't match
592
737
        # then we re-open the difference.
593
738
        if self.status == DistroSeriesDifferenceStatus.RESOLVED:
594
 
            if self.source_version != self.parent_source_version:
 
739
            if apt_pkg.VersionCompare(
 
740
                self.source_version, self.parent_source_version) < 0:
 
741
                # Higher parent version.
595
742
                updated = True
596
743
                self.status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
 
744
            elif (
 
745
                apt_pkg.VersionCompare(
 
746
                    self.source_version, self.parent_source_version) > 0
 
747
                and not manual):
 
748
                # The child was updated with a higher version so it's
 
749
                # auto-blacklisted.
 
750
                updated = True
 
751
                self.status = DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT
597
752
        # If this difference was needing attention, or the current version
598
753
        # was blacklisted and the versions now match we resolve it. Note:
599
754
        # we don't resolve it if this difference was blacklisted for all
601
756
        elif self.status in (
602
757
            DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
603
758
            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT):
604
 
            if self.source_version == self.parent_source_version:
 
759
            if apt_pkg.VersionCompare(
 
760
                    self.source_version, self.parent_source_version) == 0:
605
761
                updated = True
606
762
                self.status = DistroSeriesDifferenceStatus.RESOLVED
 
763
            elif (
 
764
                apt_pkg.VersionCompare(
 
765
                    self.source_version, self.parent_source_version) < 0
 
766
                and not manual):
 
767
                # If the derived version is lower than the parent's, we
 
768
                # ensure the diff status is blacklisted.
 
769
                self.status = DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT
607
770
 
608
771
        if self._updateBaseVersion():
609
772
            updated = True
681
844
    def unblacklist(self):
682
845
        """See `IDistroSeriesDifference`."""
683
846
        self.status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
684
 
        self.update()
 
847
        self.update(manual=True)
685
848
 
686
849
    def requestPackageDiffs(self, requestor):
687
850
        """See `IDistroSeriesDifference`."""