Merge ~ilasc/launchpad:add-link-to-status-report-log into launchpad:master

Proposed by Ioana Lasc
Status: Merged
Approved by: Ioana Lasc
Approved revision: fbc03d6e33a5373713e73f79a884b27dbb187ee8
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ilasc/launchpad:add-link-to-status-report-log
Merge into: launchpad:master
Diff against target: 103 lines (+33/-1)
4 files modified
lib/lp/code/interfaces/revisionstatus.py (+7/-0)
lib/lp/code/model/revisionstatus.py (+15/-0)
lib/lp/code/model/tests/test_revisionstatus.py (+8/-0)
lib/lp/code/templates/git-macros.pt (+3/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+417156@code.launchpad.net

Commit message

Add link to latest internal log of status reports

Description of the change

This adds the link to the latest internally stored log on status reports.

We use the URL to the log as a fallback if report.url isn't set instead of defining a new column in the table and we only link to the internally-stored log when we don't have an external URL.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote (last edit ):

This work needs a DB change: adding date_created to RevisionStatusArtifact and after that lands, the query here in `latestLog` will need to change. DB patch for the change: https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/417161

It also requires unit tests after that.

Revision history for this message
Colin Watson (cjwatson) wrote :

It seems to me that it's confusing to have a method called "latestLog" that either returns a URL that doesn't come from a log artifact at all or the download URL of a log artifact, and in no case actually returns a log artifact.

How about instead writing the link tag in the template as follows:

    <a tal:attributes="href report/url|report/latest_log/download_url|nothing" tal:content="report/title" />

Then the report can have a `latest_log` property instead of a `latestLog` method (which was anomalously named - method names normally contain a verb), and can just return the artifact rather than picking out its download URL in Python. I think that's somewhat neater, and it feels better to me to put this sort of display logic in the template.

review: Needs Fixing
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin, seems this "href report/url | report/latest_log/download_url | nothing" doesn't have quite the expected behavior - it evaluates to no link when 'report/url' is null and 'download_url' is set.. will try more things around it tomorrow, couldn't spot the problem today.

Revision history for this message
Colin Watson (cjwatson) wrote :

Sorry, it looks like I misled you slightly here. Looking at the TALES documentation (https://pagetemplates.readthedocs.io/en/latest/tales.html), it says:

    If a traversal step fails, and no alternate expression has been specified, an error results. Otherwise, the alternate expression is evaluated.

What it means by "fails" here is roughly "raises an exception" - just returning None doesn't count as failing. `report/latest_log/download_url` will indeed raise an exception if `report.latest_log` is None, but `report/url` will just return None if the report doesn't have a URL.

I think the simplest fix, though it's a bit ugly, would be to change the template to this:

              <td tal:define="url python: report.url if report.url is not None else path('report/latest_log/download_url | nothing')">
                <a tal:attributes="href url" tal:content="report/title" />
              </td>

(Alternatively, you could add another property to `RevisionStatusReport` to encapsulate this. I was going to suggest that as an alternative but then realized I couldn't think of a suitable name for the property.)

Also, you'll need to rebase/merge this onto current master, as I touched some nearby template code recently.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin, indeed that did the trick, updated and tested, it needs an approval vote before it can land.

Revision history for this message
Colin Watson (cjwatson) wrote :

Just a couple of minor tweaks to the test, but after that you can go ahead and land this. Thanks!

review: Approve
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin! Tweaks added and landing this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/code/interfaces/revisionstatus.py b/lib/lp/code/interfaces/revisionstatus.py
index b36c832..708cc40 100644
--- a/lib/lp/code/interfaces/revisionstatus.py
+++ b/lib/lp/code/interfaces/revisionstatus.py
@@ -79,6 +79,8 @@ class IRevisionStatusReportView(Interface):
79 date_finished = exported(Datetime(79 date_finished = exported(Datetime(
80 title=_("When the report has finished.")), readonly=False)80 title=_("When the report has finished.")), readonly=False)
8181
82 latest_log = Attribute("The most recent log for this report.")
83
82 @operation_parameters(84 @operation_parameters(
83 artifact_type=Choice(vocabulary=RevisionStatusArtifactType,85 artifact_type=Choice(vocabulary=RevisionStatusArtifactType,
84 required=False))86 required=False))
@@ -278,5 +280,10 @@ class IRevisionStatusArtifact(Interface):
278280
279 repository = Attribute("The repository for this artifact.")281 repository = Attribute("The repository for this artifact.")
280282
283 download_url = Attribute("The download url for this artifact.")
284
285 date_created = Datetime(
286 title=_("When the artifact was created."), readonly=True)
287
281 def getFileByName(filename):288 def getFileByName(filename):
282 """Returns an artifact by name."""289 """Returns an artifact by name."""
diff --git a/lib/lp/code/model/revisionstatus.py b/lib/lp/code/model/revisionstatus.py
index 1f74af3..78fc85e 100644
--- a/lib/lp/code/model/revisionstatus.py
+++ b/lib/lp/code/model/revisionstatus.py
@@ -11,6 +11,7 @@ import io
11import os11import os
1212
13import pytz13import pytz
14from storm.expr import Desc
14from storm.locals import (15from storm.locals import (
15 And,16 And,
16 DateTime,17 DateTime,
@@ -157,6 +158,16 @@ class RevisionStatusReport(StormBase):
157 artifacts = IStore(RevisionStatusArtifact).find(*clauses)158 artifacts = IStore(RevisionStatusArtifact).find(*clauses)
158 return [artifact.download_url for artifact in artifacts]159 return [artifact.download_url for artifact in artifacts]
159160
161 @property
162 def latest_log(self):
163 log = IStore(RevisionStatusArtifact).find(
164 RevisionStatusArtifact,
165 RevisionStatusArtifact.report == self,
166 RevisionStatusArtifact.artifact_type ==
167 RevisionStatusArtifactType.LOG).order_by(
168 Desc(RevisionStatusArtifact.date_created)).first()
169 return log
170
160171
161@implementer(IRevisionStatusReportSet)172@implementer(IRevisionStatusReportSet)
162class RevisionStatusReportSet:173class RevisionStatusReportSet:
@@ -223,11 +234,15 @@ class RevisionStatusArtifact(StormBase):
223 artifact_type = DBEnum(name='type', allow_none=False,234 artifact_type = DBEnum(name='type', allow_none=False,
224 enum=RevisionStatusArtifactType)235 enum=RevisionStatusArtifactType)
225236
237 date_created = DateTime(
238 name='date_created', tzinfo=pytz.UTC, allow_none=True)
239
226 def __init__(self, library_file, report, artifact_type):240 def __init__(self, library_file, report, artifact_type):
227 super().__init__()241 super().__init__()
228 self.library_file = library_file242 self.library_file = library_file
229 self.report = report243 self.report = report
230 self.artifact_type = artifact_type244 self.artifact_type = artifact_type
245 self.date_created = UTC_NOW
231246
232 @property247 @property
233 def download_url(self):248 def download_url(self):
diff --git a/lib/lp/code/model/tests/test_revisionstatus.py b/lib/lp/code/model/tests/test_revisionstatus.py
index 33f9c3c..deb9f6d 100644
--- a/lib/lp/code/model/tests/test_revisionstatus.py
+++ b/lib/lp/code/model/tests/test_revisionstatus.py
@@ -150,6 +150,14 @@ class TestRevisionStatusReport(TestCaseWithFactory):
150 IRevisionStatusReportSet).getByCIBuildAndTitle(build, "test")150 IRevisionStatusReportSet).getByCIBuildAndTitle(build, "test")
151 self.assertEqual("test", report.title)151 self.assertEqual("test", report.title)
152152
153 def test_latest_log(self):
154 report = self.factory.makeRevisionStatusReport()
155 self.makeRevisionStatusArtifact(report=report)
156 self.makeRevisionStatusArtifact(report=report)
157 artifact3 = self.makeRevisionStatusArtifact(report=report)
158 with person_logged_in(report.git_repository.owner):
159 self.assertEqual(artifact3, report.latest_log)
160
153161
154class TestRevisionStatusReportWebservice(TestCaseWithFactory):162class TestRevisionStatusReportWebservice(TestCaseWithFactory):
155 layer = LaunchpadFunctionalLayer163 layer = LaunchpadFunctionalLayer
diff --git a/lib/lp/code/templates/git-macros.pt b/lib/lp/code/templates/git-macros.pt
index ea6dd1d..262586b 100644
--- a/lib/lp/code/templates/git-macros.pt
+++ b/lib/lp/code/templates/git-macros.pt
@@ -235,7 +235,9 @@
235 <tbody>235 <tbody>
236 <tr tal:repeat="report batch">236 <tr tal:repeat="report batch">
237 <td class="icon" tal:content="structure report/image:icon" />237 <td class="icon" tal:content="structure report/image:icon" />
238 <td><a tal:attributes="href report/url|nothing" tal:content="report/title" /></td>238 <td tal:define="url python: report.url if report.url is not None else path('report/latest_log/download_url | nothing')">
239 <a tal:attributes="href url" tal:content="report/title" />
240 </td>
239 <td tal:content="report/result_summary" />241 <td tal:content="report/result_summary" />
240 </tr>242 </tr>
241 </tbody>243 </tbody>

Subscribers

People subscribed via source and target branches

to status/vote changes: