Merge ~cjwatson/launchpad:project-download-queries into launchpad:master
- Git
- lp:~cjwatson/launchpad
- project-download-queries
- Merge into master
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | 1f911649f0a427f3d80814e075128df40f7aa075 | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~cjwatson/launchpad:project-download-queries | ||||
Merge into: | launchpad:master | ||||
Diff against target: |
407 lines (+143/-108) 7 files modified
dev/null (+0/-93) lib/lp/registry/browser/product.py (+6/-5) lib/lp/registry/browser/tests/test_product.py (+87/-1) lib/lp/registry/browser/tests/test_views.py (+1/-2) lib/lp/registry/model/productrelease.py (+8/-3) lib/lp/services/librarian/interfaces/__init__.py (+4/-1) lib/lp/services/librarian/model.py (+37/-3) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+412706@code.launchpad.net |
Commit message
Preload more librarian references on Product:+download
Description of the change
The Product:+download page needs `ProductRelease
I took the opportunity to convert the corresponding tests away from doctests first.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py | |||
2 | index 942b4b9..3f58926 100644 | |||
3 | --- a/lib/lp/registry/browser/product.py | |||
4 | +++ b/lib/lp/registry/browser/product.py | |||
5 | @@ -208,6 +208,7 @@ from lp.services.fields import ( | |||
6 | 208 | PublicPersonChoice, | 208 | PublicPersonChoice, |
7 | 209 | URIField, | 209 | URIField, |
8 | 210 | ) | 210 | ) |
9 | 211 | from lp.services.librarian.interfaces import ILibraryFileAliasSet | ||
10 | 211 | from lp.services.propertycache import cachedproperty | 212 | from lp.services.propertycache import cachedproperty |
11 | 212 | from lp.services.webapp import ( | 213 | from lp.services.webapp import ( |
12 | 213 | ApplicationMenu, | 214 | ApplicationMenu, |
13 | @@ -848,8 +849,10 @@ class ReleaseWithFiles: | |||
14 | 848 | # returns all releases sorted properly. | 849 | # returns all releases sorted properly. |
15 | 849 | product = self.parent.parent | 850 | product = self.parent.parent |
16 | 850 | release_delegates = product.release_by_id.values() | 851 | release_delegates = product.release_by_id.values() |
19 | 851 | files = getUtility(IProductReleaseSet).getFilesForReleases( | 852 | files = list(getUtility(IProductReleaseSet).getFilesForReleases( |
20 | 852 | release_delegates) | 853 | release_delegates)) |
21 | 854 | getUtility(ILibraryFileAliasSet).preloadLastDownloaded( | ||
22 | 855 | {file.libraryfile for file in files}) | ||
23 | 853 | for release_delegate in release_delegates: | 856 | for release_delegate in release_delegates: |
24 | 854 | release_delegate._files = [] | 857 | release_delegate._files = [] |
25 | 855 | for file in files: | 858 | for file in files: |
26 | @@ -1271,8 +1274,6 @@ class ProductDownloadFilesView(LaunchpadView, | |||
27 | 1271 | ProductDownloadFileMixin): | 1274 | ProductDownloadFileMixin): |
28 | 1272 | """View class for the product's file downloads page.""" | 1275 | """View class for the product's file downloads page.""" |
29 | 1273 | 1276 | ||
30 | 1274 | batch_size = config.launchpad.download_batch_size | ||
31 | 1275 | |||
32 | 1276 | @property | 1277 | @property |
33 | 1277 | def page_title(self): | 1278 | def page_title(self): |
34 | 1278 | return "%s project files" % self.context.displayname | 1279 | return "%s project files" % self.context.displayname |
35 | @@ -1304,7 +1305,7 @@ class ProductDownloadFilesView(LaunchpadView, | |||
36 | 1304 | if pair not in series_and_releases: | 1305 | if pair not in series_and_releases: |
37 | 1305 | series_and_releases.append(pair) | 1306 | series_and_releases.append(pair) |
38 | 1306 | batch = BatchNavigator(series_and_releases, self.request, | 1307 | batch = BatchNavigator(series_and_releases, self.request, |
40 | 1307 | size=self.batch_size) | 1308 | size=config.launchpad.download_batch_size) |
41 | 1308 | batch.setHeadings("release", "releases") | 1309 | batch.setHeadings("release", "releases") |
42 | 1309 | return batch | 1310 | return batch |
43 | 1310 | 1311 | ||
44 | diff --git a/lib/lp/registry/browser/tests/product-files-views.txt b/lib/lp/registry/browser/tests/product-files-views.txt | |||
45 | 1311 | deleted file mode 100644 | 1312 | deleted file mode 100644 |
46 | index c999548..0000000 | |||
47 | --- a/lib/lp/registry/browser/tests/product-files-views.txt | |||
48 | +++ /dev/null | |||
49 | @@ -1,93 +0,0 @@ | |||
50 | 1 | Product Download Files Page | ||
51 | 2 | ============================= | ||
52 | 3 | |||
53 | 4 | Test for the product/+download page. | ||
54 | 5 | |||
55 | 6 | >>> product = factory.makeProduct(name='alfajore') | ||
56 | 7 | >>> productseries = factory.makeProductSeries( | ||
57 | 8 | ... product=product, name="sammy") | ||
58 | 9 | >>> milestone = factory.makeMilestone(productseries=productseries, | ||
59 | 10 | ... name="apple") | ||
60 | 11 | >>> release_file = factory.makeProductReleaseFile( | ||
61 | 12 | ... product=product, productseries=productseries, milestone=milestone) | ||
62 | 13 | >>> view = create_initialized_view(product, '+download') | ||
63 | 14 | |||
64 | 15 | >>> view.batch_size | ||
65 | 16 | 4 | ||
66 | 17 | |||
67 | 18 | >>> batch = view.series_and_releases_batch.currentBatch() | ||
68 | 19 | >>> print(len(list(batch))) | ||
69 | 20 | 1 | ||
70 | 21 | |||
71 | 22 | >>> def print_series_release(sr): | ||
72 | 23 | ... print("%s from the %s series" % (sr.release.name_with_codename, | ||
73 | 24 | ... sr.series.name)) | ||
74 | 25 | |||
75 | 26 | >>> for sr in batch: | ||
76 | 27 | ... print_series_release(sr) | ||
77 | 28 | apple from the sammy series | ||
78 | 29 | |||
79 | 30 | >>> product = factory.makeProduct(name='bombilla') | ||
80 | 31 | >>> for i in range(1,5): | ||
81 | 32 | ... productseries = factory.makeProductSeries( | ||
82 | 33 | ... product=product, name="s%d"%i) | ||
83 | 34 | ... for j in range(1,4): | ||
84 | 35 | ... milestone = factory.makeMilestone(productseries=productseries, | ||
85 | 36 | ... name="%d.%d"%(i,j)) | ||
86 | 37 | ... release_file = factory.makeProductReleaseFile( | ||
87 | 38 | ... product=product, productseries=productseries, | ||
88 | 39 | ... milestone=milestone) | ||
89 | 40 | >>> view = create_initialized_view(product, '+download') | ||
90 | 41 | >>> batch = view.series_and_releases_batch.currentBatch() | ||
91 | 42 | >>> print(len(batch)) | ||
92 | 43 | 4 | ||
93 | 44 | >>> for sr in batch: | ||
94 | 45 | ... print_series_release(sr) | ||
95 | 46 | 4.3 from the s4 series | ||
96 | 47 | 4.2 from the s4 series | ||
97 | 48 | 4.1 from the s4 series | ||
98 | 49 | 3.3 from the s3 series | ||
99 | 50 | |||
100 | 51 | >>> batch = batch.nextBatch() | ||
101 | 52 | >>> for sr in batch: | ||
102 | 53 | ... print_series_release(sr) | ||
103 | 54 | 3.2 from the s3 series | ||
104 | 55 | 3.1 from the s3 series | ||
105 | 56 | 2.3 from the s2 series | ||
106 | 57 | 2.2 from the s2 series | ||
107 | 58 | |||
108 | 59 | For an administrator of the project, at the bottom of each batched | ||
109 | 60 | page will be links to add new files for each series and release. | ||
110 | 61 | |||
111 | 62 | >>> from lp.testing.pages import ( | ||
112 | 63 | ... extract_text, find_tag_by_id) | ||
113 | 64 | >>> ignored = login_person(product.owner) | ||
114 | 65 | >>> view = create_initialized_view(product, '+download', | ||
115 | 66 | ... principal=product.owner) | ||
116 | 67 | >>> admin_links = find_tag_by_id(view.render(), 'admin-links') | ||
117 | 68 | >>> content = extract_text(admin_links) | ||
118 | 69 | >>> print(content) | ||
119 | 70 | Add download file to the s4 series for release: 4.3, 4.2, 4.1 | ||
120 | 71 | Add download file to the s3 series for release: 3.3, 3.2, 3.1 | ||
121 | 72 | Add download file to the s2 series for release: 2.3, 2.2, 2.1 | ||
122 | 73 | Add download file to the s1 series for release: 1.3, 1.2, 1.1 | ||
123 | 74 | |||
124 | 75 | |||
125 | 76 | Product index | ||
126 | 77 | ------------- | ||
127 | 78 | |||
128 | 79 | The product index view shows the latest release for the project. | ||
129 | 80 | |||
130 | 81 | >>> view = create_initialized_view(product, name='+index') | ||
131 | 82 | >>> print(view.latest_release_with_download_files.version) | ||
132 | 83 | 4.3 | ||
133 | 84 | |||
134 | 85 | Obsolete series are ignored. | ||
135 | 86 | |||
136 | 87 | >>> from lp.registry.interfaces.series import SeriesStatus | ||
137 | 88 | |||
138 | 89 | >>> obsolete_series = product.getSeries('s4') | ||
139 | 90 | >>> obsolete_series.status = SeriesStatus.OBSOLETE | ||
140 | 91 | >>> view = create_initialized_view(product, name='+index') | ||
141 | 92 | >>> print(view.latest_release_with_download_files.version) | ||
142 | 93 | 3.3 | ||
143 | diff --git a/lib/lp/registry/browser/tests/test_product.py b/lib/lp/registry/browser/tests/test_product.py | |||
144 | index 3e71422..1f571fe 100644 | |||
145 | --- a/lib/lp/registry/browser/tests/test_product.py | |||
146 | +++ b/lib/lp/registry/browser/tests/test_product.py | |||
147 | @@ -1,4 +1,4 @@ | |||
149 | 1 | # Copyright 2010-2020 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2021 Canonical Ltd. This software is licensed under the |
150 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
151 | 3 | 3 | ||
152 | 4 | """Tests for product views.""" | 4 | """Tests for product views.""" |
153 | @@ -6,6 +6,7 @@ | |||
154 | 6 | __all__ = ['make_product_form'] | 6 | __all__ = ['make_product_form'] |
155 | 7 | 7 | ||
156 | 8 | import re | 8 | import re |
157 | 9 | from textwrap import dedent | ||
158 | 9 | 10 | ||
159 | 10 | from lazr.restful.fields import Reference | 11 | from lazr.restful.fields import Reference |
160 | 11 | from lazr.restful.interfaces import ( | 12 | from lazr.restful.interfaces import ( |
161 | @@ -55,6 +56,7 @@ from lp.registry.interfaces.product import ( | |||
162 | 55 | License, | 56 | License, |
163 | 56 | ) | 57 | ) |
164 | 57 | from lp.registry.interfaces.productseries import IProductSeries | 58 | from lp.registry.interfaces.productseries import IProductSeries |
165 | 59 | from lp.registry.interfaces.series import SeriesStatus | ||
166 | 58 | from lp.registry.model.product import Product | 60 | from lp.registry.model.product import Product |
167 | 59 | from lp.services.config import config | 61 | from lp.services.config import config |
168 | 60 | from lp.services.database.interfaces import IStore | 62 | from lp.services.database.interfaces import IStore |
169 | @@ -69,6 +71,7 @@ from lp.testing import ( | |||
170 | 69 | login_celebrity, | 71 | login_celebrity, |
171 | 70 | login_person, | 72 | login_person, |
172 | 71 | person_logged_in, | 73 | person_logged_in, |
173 | 74 | record_two_runs, | ||
174 | 72 | StormStatementRecorder, | 75 | StormStatementRecorder, |
175 | 73 | TestCaseWithFactory, | 76 | TestCaseWithFactory, |
176 | 74 | ) | 77 | ) |
177 | @@ -835,6 +838,89 @@ class TestProductEditView(BrowserTestCase): | |||
178 | 835 | InformationType.PUBLIC, updated_product.information_type) | 838 | InformationType.PUBLIC, updated_product.information_type) |
179 | 836 | 839 | ||
180 | 837 | 840 | ||
181 | 841 | class TestProductDownloadFilesView(TestCaseWithFactory): | ||
182 | 842 | """Test `ProductDownloadFilesView`.""" | ||
183 | 843 | |||
184 | 844 | layer = LaunchpadFunctionalLayer | ||
185 | 845 | |||
186 | 846 | def makeProductAndReleases(self): | ||
187 | 847 | product = self.factory.makeProduct() | ||
188 | 848 | for i in range(1, 5): | ||
189 | 849 | productseries = self.factory.makeProductSeries( | ||
190 | 850 | product=product, name="s%d" % i) | ||
191 | 851 | for j in range(1, 4): | ||
192 | 852 | milestone = self.factory.makeMilestone( | ||
193 | 853 | productseries=productseries, name="%d.%d" % (i, j)) | ||
194 | 854 | self.factory.makeProductReleaseFile( | ||
195 | 855 | product=product, productseries=productseries, | ||
196 | 856 | milestone=milestone) | ||
197 | 857 | return product | ||
198 | 858 | |||
199 | 859 | def test_series_and_releases_batch(self): | ||
200 | 860 | self.pushConfig("launchpad", download_batch_size=4) | ||
201 | 861 | product = self.makeProductAndReleases() | ||
202 | 862 | view = create_initialized_view(product, "+download") | ||
203 | 863 | batch = view.series_and_releases_batch.currentBatch() | ||
204 | 864 | self.assertEqual( | ||
205 | 865 | [("4.3", "s4"), ("4.2", "s4"), ("4.1", "s4"), ("3.3", "s3")], | ||
206 | 866 | [(sr.release.name_with_codename, sr.series.name) for sr in batch]) | ||
207 | 867 | batch = batch.nextBatch() | ||
208 | 868 | self.assertEqual( | ||
209 | 869 | [("3.2", "s3"), ("3.1", "s3"), ("2.3", "s2"), ("2.2", "s2")], | ||
210 | 870 | [(sr.release.name_with_codename, sr.series.name) for sr in batch]) | ||
211 | 871 | |||
212 | 872 | def test_query_count(self): | ||
213 | 873 | self.pushConfig("launchpad", download_batch_size=20) | ||
214 | 874 | product = self.factory.makeProduct() | ||
215 | 875 | |||
216 | 876 | def create_series_and_releases(): | ||
217 | 877 | productseries = self.factory.makeProductSeries(product=product) | ||
218 | 878 | for _ in range(3): | ||
219 | 879 | self.factory.makeProductReleaseFile( | ||
220 | 880 | product=product, productseries=productseries) | ||
221 | 881 | |||
222 | 882 | def render_product(): | ||
223 | 883 | create_initialized_view(product, "+download").render() | ||
224 | 884 | |||
225 | 885 | recorder1, recorder2 = record_two_runs( | ||
226 | 886 | render_product, create_series_and_releases, 2) | ||
227 | 887 | self.assertThat(recorder2, HasQueryCount.byEquality(recorder1)) | ||
228 | 888 | |||
229 | 889 | def test_add_download_file_links(self): | ||
230 | 890 | # Project administrators have links at the bottom of each batched | ||
231 | 891 | # page to add new files for each series and release. | ||
232 | 892 | product = self.makeProductAndReleases() | ||
233 | 893 | login_person(product.owner) | ||
234 | 894 | view = create_initialized_view( | ||
235 | 895 | product, "+download", principal=product.owner) | ||
236 | 896 | admin_links = find_tag_by_id(view.render(), "admin-links") | ||
237 | 897 | expected_links = dedent(""" | ||
238 | 898 | Add download file to the s4 series for release: 4.3, 4.2, 4.1 | ||
239 | 899 | Add download file to the s3 series for release: 3.3, 3.2, 3.1 | ||
240 | 900 | Add download file to the s2 series for release: 2.3, 2.2, 2.1 | ||
241 | 901 | Add download file to the s1 series for release: 1.3, 1.2, 1.1 | ||
242 | 902 | """) | ||
243 | 903 | self.assertTextMatchesExpressionIgnoreWhitespace( | ||
244 | 904 | expected_links, extract_text(admin_links)) | ||
245 | 905 | |||
246 | 906 | def test_latest_release_on_index(self): | ||
247 | 907 | # The project index view shows the latest release for the project. | ||
248 | 908 | product = self.makeProductAndReleases() | ||
249 | 909 | view = create_initialized_view(product, "+index") | ||
250 | 910 | self.assertEqual( | ||
251 | 911 | "4.3", view.latest_release_with_download_files.version) | ||
252 | 912 | |||
253 | 913 | def test_latest_release_ignores_obsolete_series(self): | ||
254 | 914 | # The project index view ignores obsolete series for the purpose of | ||
255 | 915 | # showing the latest release. | ||
256 | 916 | product = self.makeProductAndReleases() | ||
257 | 917 | with person_logged_in(product.owner): | ||
258 | 918 | product.getSeries("s4").status = SeriesStatus.OBSOLETE | ||
259 | 919 | view = create_initialized_view(product, "+index") | ||
260 | 920 | self.assertEqual( | ||
261 | 921 | "3.3", view.latest_release_with_download_files.version) | ||
262 | 922 | |||
263 | 923 | |||
264 | 838 | class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory): | 924 | class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory): |
265 | 839 | """Tests the ProductSetReviewLicensesView.""" | 925 | """Tests the ProductSetReviewLicensesView.""" |
266 | 840 | 926 | ||
267 | diff --git a/lib/lp/registry/browser/tests/test_views.py b/lib/lp/registry/browser/tests/test_views.py | |||
268 | index 02ac47f..35254f4 100644 | |||
269 | --- a/lib/lp/registry/browser/tests/test_views.py | |||
270 | +++ b/lib/lp/registry/browser/tests/test_views.py | |||
271 | @@ -1,4 +1,4 @@ | |||
273 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
274 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
275 | 3 | 3 | ||
276 | 4 | """ | 4 | """ |
277 | @@ -34,7 +34,6 @@ special_test_layer = { | |||
278 | 34 | 'milestone-views.txt': LaunchpadFunctionalLayer, | 34 | 'milestone-views.txt': LaunchpadFunctionalLayer, |
279 | 35 | 'person-views.txt': LaunchpadFunctionalLayer, | 35 | 'person-views.txt': LaunchpadFunctionalLayer, |
280 | 36 | 'product-edit-people-view.txt': LaunchpadFunctionalLayer, | 36 | 'product-edit-people-view.txt': LaunchpadFunctionalLayer, |
281 | 37 | 'product-files-views.txt': LaunchpadFunctionalLayer, | ||
282 | 38 | 'product-views.txt': LaunchpadFunctionalLayer, | 37 | 'product-views.txt': LaunchpadFunctionalLayer, |
283 | 39 | 'productseries-views.txt': LaunchpadFunctionalLayer, | 38 | 'productseries-views.txt': LaunchpadFunctionalLayer, |
284 | 40 | 'projectgroup-views.txt': LaunchpadFunctionalLayer, | 39 | 'projectgroup-views.txt': LaunchpadFunctionalLayer, |
285 | diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py | |||
286 | index 3b88d33..00c7914 100644 | |||
287 | --- a/lib/lp/registry/model/productrelease.py | |||
288 | +++ b/lib/lp/registry/model/productrelease.py | |||
289 | @@ -1,4 +1,4 @@ | |||
291 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
292 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
293 | 3 | 3 | ||
294 | 4 | __all__ = [ | 4 | __all__ = [ |
295 | @@ -281,9 +281,14 @@ class ProductReleaseSet(object): | |||
296 | 281 | return EmptyResultSet() | 281 | return EmptyResultSet() |
297 | 282 | return ProductReleaseFile.select( | 282 | return ProductReleaseFile.select( |
298 | 283 | """ProductReleaseFile.productrelease IN %s""" % ( | 283 | """ProductReleaseFile.productrelease IN %s""" % ( |
300 | 284 | sqlvalues([release.id for release in releases])), | 284 | sqlvalues([release.id for release in releases])), |
301 | 285 | orderBy='-date_uploaded', | 285 | orderBy='-date_uploaded', |
303 | 286 | prejoins=['libraryfile', 'libraryfile.content', 'productrelease']) | 286 | prejoins=[ |
304 | 287 | 'libraryfile', | ||
305 | 288 | 'libraryfile.content', | ||
306 | 289 | 'productrelease', | ||
307 | 290 | 'signature', | ||
308 | 291 | ]) | ||
309 | 287 | 292 | ||
310 | 288 | 293 | ||
311 | 289 | def productrelease_to_milestone(productrelease): | 294 | def productrelease_to_milestone(productrelease): |
312 | diff --git a/lib/lp/services/librarian/interfaces/__init__.py b/lib/lp/services/librarian/interfaces/__init__.py | |||
313 | index 6b3646e..73b390b 100644 | |||
314 | --- a/lib/lp/services/librarian/interfaces/__init__.py | |||
315 | +++ b/lib/lp/services/librarian/interfaces/__init__.py | |||
316 | @@ -1,4 +1,4 @@ | |||
318 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
319 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
320 | 3 | 3 | ||
321 | 4 | """Librarian interfaces.""" | 4 | """Librarian interfaces.""" |
322 | @@ -175,6 +175,9 @@ class ILibraryFileAliasSet(Interface): | |||
323 | 175 | given sha256. | 175 | given sha256. |
324 | 176 | """ | 176 | """ |
325 | 177 | 177 | ||
326 | 178 | def preloadLastDownloaded(self, lfas): | ||
327 | 179 | """Preload last_downloaded for a collection of `LibraryFileAlias`es.""" | ||
328 | 180 | |||
329 | 178 | 181 | ||
330 | 179 | class ILibraryFileDownloadCount(Interface): | 182 | class ILibraryFileDownloadCount(Interface): |
331 | 180 | """Download count of a given file in a given day.""" | 183 | """Download count of a given file in a given day.""" |
332 | diff --git a/lib/lp/services/librarian/model.py b/lib/lp/services/librarian/model.py | |||
333 | index 3e730eb..2c513d3 100644 | |||
334 | --- a/lib/lp/services/librarian/model.py | |||
335 | +++ b/lib/lp/services/librarian/model.py | |||
336 | @@ -1,4 +1,4 @@ | |||
338 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
339 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
340 | 3 | 3 | ||
341 | 4 | __all__ = [ | 4 | __all__ = [ |
342 | @@ -40,7 +40,10 @@ from lp.services.database.constants import ( | |||
343 | 40 | UTC_NOW, | 40 | UTC_NOW, |
344 | 41 | ) | 41 | ) |
345 | 42 | from lp.services.database.datetimecol import UtcDateTimeCol | 42 | from lp.services.database.datetimecol import UtcDateTimeCol |
347 | 43 | from lp.services.database.interfaces import IMasterStore | 43 | from lp.services.database.interfaces import ( |
348 | 44 | IMasterStore, | ||
349 | 45 | IStore, | ||
350 | 46 | ) | ||
351 | 44 | from lp.services.database.sqlbase import ( | 47 | from lp.services.database.sqlbase import ( |
352 | 45 | session_store, | 48 | session_store, |
353 | 46 | SQLBase, | 49 | SQLBase, |
354 | @@ -66,6 +69,10 @@ from lp.services.librarian.interfaces.client import ( | |||
355 | 66 | IRestrictedLibrarianClient, | 69 | IRestrictedLibrarianClient, |
356 | 67 | LIBRARIAN_SERVER_DEFAULT_TIMEOUT, | 70 | LIBRARIAN_SERVER_DEFAULT_TIMEOUT, |
357 | 68 | ) | 71 | ) |
358 | 72 | from lp.services.propertycache import ( | ||
359 | 73 | cachedproperty, | ||
360 | 74 | get_property_cache, | ||
361 | 75 | ) | ||
362 | 69 | from lp.services.tokens import create_token | 76 | from lp.services.tokens import create_token |
363 | 70 | 77 | ||
364 | 71 | 78 | ||
365 | @@ -180,7 +187,7 @@ class LibraryFileAlias(SQLBase): | |||
366 | 180 | self._datafile.close() | 187 | self._datafile.close() |
367 | 181 | self._datafile = None | 188 | self._datafile = None |
368 | 182 | 189 | ||
370 | 183 | @property | 190 | @cachedproperty |
371 | 184 | def last_downloaded(self): | 191 | def last_downloaded(self): |
372 | 185 | """See `ILibraryFileAlias`.""" | 192 | """See `ILibraryFileAlias`.""" |
373 | 186 | store = Store.of(self) | 193 | store = Store.of(self) |
374 | @@ -271,6 +278,33 @@ class LibraryFileAliasSet(object): | |||
375 | 271 | AND LibraryFileContent.sha256 = '%s' | 278 | AND LibraryFileContent.sha256 = '%s' |
376 | 272 | """ % sha256, clauseTables=['LibraryFileContent']) | 279 | """ % sha256, clauseTables=['LibraryFileContent']) |
377 | 273 | 280 | ||
378 | 281 | def preloadLastDownloaded(self, lfas): | ||
379 | 282 | """See `ILibraryFileAliasSet`.""" | ||
380 | 283 | store = IStore(LibraryFileAlias) | ||
381 | 284 | results = store.find( | ||
382 | 285 | (LibraryFileDownloadCount.libraryfilealias_id, | ||
383 | 286 | LibraryFileDownloadCount.day), | ||
384 | 287 | LibraryFileDownloadCount.libraryfilealias_id.is_in( | ||
385 | 288 | sorted(lfa.id for lfa in lfas))) | ||
386 | 289 | results.order_by( | ||
387 | 290 | # libraryfilealias doesn't need to be descending for | ||
388 | 291 | # correctness, but this allows the index on | ||
389 | 292 | # LibraryFileDownloadCount (libraryfilealias, day, country) to | ||
390 | 293 | # satisfy this query efficiently. | ||
391 | 294 | Desc(LibraryFileDownloadCount.libraryfilealias_id), | ||
392 | 295 | Desc(LibraryFileDownloadCount.day)) | ||
393 | 296 | # Request the first row for each LFA, which corresponds to the most | ||
394 | 297 | # recent day due to the above ordering. | ||
395 | 298 | results.config( | ||
396 | 299 | distinct=(LibraryFileDownloadCount.libraryfilealias_id,)) | ||
397 | 300 | now = datetime.now(pytz.utc).date() | ||
398 | 301 | lfas_by_id = {lfa.id: lfa for lfa in lfas} | ||
399 | 302 | for lfa_id, day in results: | ||
400 | 303 | get_property_cache(lfas_by_id[lfa_id]).last_downloaded = now - day | ||
401 | 304 | del lfas_by_id[lfa_id] | ||
402 | 305 | for lfa in lfas_by_id.values(): | ||
403 | 306 | get_property_cache(lfa).last_downloaded = None | ||
404 | 307 | |||
405 | 274 | 308 | ||
406 | 275 | @implementer(ILibraryFileDownloadCount) | 309 | @implementer(ILibraryFileDownloadCount) |
407 | 276 | class LibraryFileDownloadCount(SQLBase): | 310 | class LibraryFileDownloadCount(SQLBase): |
LGTM