Merge lp:~cjwatson/launchpad/git-collection into lp:launchpad
- git-collection
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | no longer in the source branch. |
Merged at revision: | 17365 |
Proposed branch: | lp:~cjwatson/launchpad/git-collection |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/git-lookup |
Diff against target: |
1309 lines (+1279/-0) 5 files modified
lib/lp/code/adapters/gitcollection.py (+62/-0) lib/lp/code/configure.zcml (+49/-0) lib/lp/code/interfaces/gitcollection.py (+125/-0) lib/lp/code/model/gitcollection.py (+327/-0) lib/lp/code/model/tests/test_gitcollection.py (+716/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-collection |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+250646@code.launchpad.net |
Commit message
Add support for collections of Git repositories.
Description of the change
Add support for collections of Git repositories.
This is mostly a much-reduced version of BranchLookup. It's also the last of the big core chunks of the Git repository model; after this it should be possible to work in more digestible pieces rather than thousand-line sections of entirely new code.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'lib/lp/code/adapters/gitcollection.py' | |||
2 | --- lib/lp/code/adapters/gitcollection.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ lib/lp/code/adapters/gitcollection.py 2015-02-26 17:21:27 +0000 | |||
4 | @@ -0,0 +1,62 @@ | |||
5 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
6 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
7 | 3 | |||
8 | 4 | """Adapters for different objects to Git repository collections.""" | ||
9 | 5 | |||
10 | 6 | __metaclass__ = type | ||
11 | 7 | __all__ = [ | ||
12 | 8 | 'git_collection_for_distribution', | ||
13 | 9 | 'git_collection_for_distro_source_package', | ||
14 | 10 | 'git_collection_for_person', | ||
15 | 11 | 'git_collection_for_person_distro_source_package', | ||
16 | 12 | 'git_collection_for_person_product', | ||
17 | 13 | 'git_collection_for_project', | ||
18 | 14 | 'git_collection_for_project_group', | ||
19 | 15 | ] | ||
20 | 16 | |||
21 | 17 | |||
22 | 18 | from zope.component import getUtility | ||
23 | 19 | |||
24 | 20 | from lp.code.interfaces.gitcollection import IAllGitRepositories | ||
25 | 21 | |||
26 | 22 | |||
27 | 23 | def git_collection_for_project(project): | ||
28 | 24 | """Adapt a product to a Git repository collection.""" | ||
29 | 25 | return getUtility(IAllGitRepositories).inProject(project) | ||
30 | 26 | |||
31 | 27 | |||
32 | 28 | def git_collection_for_project_group(project_group): | ||
33 | 29 | """Adapt a project group to a Git repository collection.""" | ||
34 | 30 | return getUtility(IAllGitRepositories).inProjectGroup(project_group) | ||
35 | 31 | |||
36 | 32 | |||
37 | 33 | def git_collection_for_distribution(distribution): | ||
38 | 34 | """Adapt a distribution to a Git repository collection.""" | ||
39 | 35 | return getUtility(IAllGitRepositories).inDistribution(distribution) | ||
40 | 36 | |||
41 | 37 | |||
42 | 38 | def git_collection_for_distro_source_package(distro_source_package): | ||
43 | 39 | """Adapt a distro_source_package to a Git repository collection.""" | ||
44 | 40 | return getUtility(IAllGitRepositories).inDistributionSourcePackage( | ||
45 | 41 | distro_source_package) | ||
46 | 42 | |||
47 | 43 | |||
48 | 44 | def git_collection_for_person(person): | ||
49 | 45 | """Adapt a person to a Git repository collection.""" | ||
50 | 46 | return getUtility(IAllGitRepositories).ownedBy(person) | ||
51 | 47 | |||
52 | 48 | |||
53 | 49 | def git_collection_for_person_product(person_product): | ||
54 | 50 | """Adapt a PersonProduct to a Git repository collection.""" | ||
55 | 51 | collection = getUtility(IAllGitRepositories).ownedBy(person_product.person) | ||
56 | 52 | collection = collection.inProject(person_product.product) | ||
57 | 53 | return collection | ||
58 | 54 | |||
59 | 55 | |||
60 | 56 | def git_collection_for_person_distro_source_package(person_dsp): | ||
61 | 57 | """Adapt a PersonDistributionSourcePackage to a Git repository | ||
62 | 58 | collection.""" | ||
63 | 59 | collection = getUtility(IAllGitRepositories).ownedBy(person_dsp.person) | ||
64 | 60 | collection = collection.inDistributionSourcePackage( | ||
65 | 61 | person_dsp.distro_source_package) | ||
66 | 62 | return collection | ||
67 | 0 | 63 | ||
68 | === modified file 'lib/lp/code/configure.zcml' | |||
69 | --- lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000 | |||
70 | +++ lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000 | |||
71 | @@ -858,6 +858,55 @@ | |||
72 | 858 | <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" /> | 858 | <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" /> |
73 | 859 | </securedutility> | 859 | </securedutility> |
74 | 860 | 860 | ||
75 | 861 | <!-- GitCollection --> | ||
76 | 862 | |||
77 | 863 | <class class="lp.code.model.gitcollection.GenericGitCollection"> | ||
78 | 864 | <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/> | ||
79 | 865 | </class> | ||
80 | 866 | <class class="lp.code.model.gitcollection.AnonymousGitCollection"> | ||
81 | 867 | <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/> | ||
82 | 868 | </class> | ||
83 | 869 | <class class="lp.code.model.gitcollection.VisibleGitCollection"> | ||
84 | 870 | <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/> | ||
85 | 871 | </class> | ||
86 | 872 | <adapter | ||
87 | 873 | for="storm.store.Store" | ||
88 | 874 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
89 | 875 | factory="lp.code.model.gitcollection.GenericGitCollection"/> | ||
90 | 876 | <adapter | ||
91 | 877 | for="lp.registry.interfaces.product.IProduct" | ||
92 | 878 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
93 | 879 | factory="lp.code.adapters.gitcollection.git_collection_for_project"/> | ||
94 | 880 | <adapter | ||
95 | 881 | for="lp.registry.interfaces.projectgroup.IProjectGroup" | ||
96 | 882 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
97 | 883 | factory="lp.code.adapters.gitcollection.git_collection_for_project_group"/> | ||
98 | 884 | <adapter | ||
99 | 885 | for="lp.registry.interfaces.distribution.IDistribution" | ||
100 | 886 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
101 | 887 | factory="lp.code.adapters.gitcollection.git_collection_for_distribution"/> | ||
102 | 888 | <adapter | ||
103 | 889 | for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage" | ||
104 | 890 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
105 | 891 | factory="lp.code.adapters.gitcollection.git_collection_for_distro_source_package"/> | ||
106 | 892 | <adapter | ||
107 | 893 | for="lp.registry.interfaces.person.IPerson" | ||
108 | 894 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
109 | 895 | factory="lp.code.adapters.gitcollection.git_collection_for_person"/> | ||
110 | 896 | <adapter | ||
111 | 897 | for="lp.registry.interfaces.personproduct.IPersonProduct" | ||
112 | 898 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
113 | 899 | factory="lp.code.adapters.gitcollection.git_collection_for_person_product"/> | ||
114 | 900 | <adapter | ||
115 | 901 | for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage" | ||
116 | 902 | provides="lp.code.interfaces.gitcollection.IGitCollection" | ||
117 | 903 | factory="lp.code.adapters.gitcollection.git_collection_for_person_distro_source_package"/> | ||
118 | 904 | <securedutility | ||
119 | 905 | class="lp.code.model.gitcollection.GenericGitCollection" | ||
120 | 906 | provides="lp.code.interfaces.gitcollection.IAllGitRepositories"> | ||
121 | 907 | <allow interface="lp.code.interfaces.gitcollection.IAllGitRepositories"/> | ||
122 | 908 | </securedutility> | ||
123 | 909 | |||
124 | 861 | <!-- Default Git repositories --> | 910 | <!-- Default Git repositories --> |
125 | 862 | 911 | ||
126 | 863 | <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" /> | 912 | <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" /> |
127 | 864 | 913 | ||
128 | === added file 'lib/lp/code/interfaces/gitcollection.py' | |||
129 | --- lib/lp/code/interfaces/gitcollection.py 1970-01-01 00:00:00 +0000 | |||
130 | +++ lib/lp/code/interfaces/gitcollection.py 2015-02-26 17:21:27 +0000 | |||
131 | @@ -0,0 +1,125 @@ | |||
132 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
133 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
134 | 3 | |||
135 | 4 | """A collection of Git repositories. | ||
136 | 5 | |||
137 | 6 | See `IGitCollection` for more details. | ||
138 | 7 | """ | ||
139 | 8 | |||
140 | 9 | __metaclass__ = type | ||
141 | 10 | __all__ = [ | ||
142 | 11 | 'IAllGitRepositories', | ||
143 | 12 | 'IGitCollection', | ||
144 | 13 | 'InvalidGitFilter', | ||
145 | 14 | ] | ||
146 | 15 | |||
147 | 16 | from zope.interface import Interface | ||
148 | 17 | |||
149 | 18 | |||
150 | 19 | class InvalidGitFilter(Exception): | ||
151 | 20 | """Raised when an `IGitCollection` cannot apply the given filter.""" | ||
152 | 21 | |||
153 | 22 | |||
154 | 23 | class IGitCollection(Interface): | ||
155 | 24 | """A collection of Git repositories. | ||
156 | 25 | |||
157 | 26 | An `IGitCollection` is an immutable collection of Git repositories. It | ||
158 | 27 | has two kinds of methods: filter methods and query methods. | ||
159 | 28 | |||
160 | 29 | Query methods get information about the contents of the collection. See | ||
161 | 30 | `IGitCollection.count` and `IGitCollection.getRepositories`. | ||
162 | 31 | |||
163 | 32 | Filter methods return new IGitCollection instances that have some sort | ||
164 | 33 | of restriction. Examples include `ownedBy`, `visibleByUser` and | ||
165 | 34 | `inProject`. | ||
166 | 35 | |||
167 | 36 | Implementations of this interface are not 'content classes'. That is, they | ||
168 | 37 | do not correspond to a particular row in the database. | ||
169 | 38 | |||
170 | 39 | This interface is intended for use within Launchpad, not to be exported as | ||
171 | 40 | a public API. | ||
172 | 41 | """ | ||
173 | 42 | |||
174 | 43 | def count(): | ||
175 | 44 | """The number of repositories in this collection.""" | ||
176 | 45 | |||
177 | 46 | def is_empty(): | ||
178 | 47 | """Is this collection empty?""" | ||
179 | 48 | |||
180 | 49 | def ownerCounts(): | ||
181 | 50 | """Return the number of different repository owners. | ||
182 | 51 | |||
183 | 52 | :return: a tuple (individual_count, team_count) containing the | ||
184 | 53 | number of individuals and teams that own repositories in this | ||
185 | 54 | collection. | ||
186 | 55 | """ | ||
187 | 56 | |||
188 | 57 | def getRepositories(eager_load=False): | ||
189 | 58 | """Return a result set of all repositories in this collection. | ||
190 | 59 | |||
191 | 60 | The returned result set will also join across the specified tables | ||
192 | 61 | as defined by the arguments to this function. These extra tables | ||
193 | 62 | are joined specifically to allow the caller to sort on values not in | ||
194 | 63 | the GitRepository table itself. | ||
195 | 64 | |||
196 | 65 | :param eager_load: If True trigger eager loading of all the related | ||
197 | 66 | objects in the collection. | ||
198 | 67 | """ | ||
199 | 68 | |||
200 | 69 | def getRepositoryIds(): | ||
201 | 70 | """Return a result set of all repository ids in this collection.""" | ||
202 | 71 | |||
203 | 72 | def getTeamsWithRepositories(person): | ||
204 | 73 | """Return the teams that person is a member of that have | ||
205 | 74 | repositories.""" | ||
206 | 75 | |||
207 | 76 | def inProject(project): | ||
208 | 77 | """Restrict the collection to repositories in 'project'.""" | ||
209 | 78 | |||
210 | 79 | def inProjectGroup(projectgroup): | ||
211 | 80 | """Restrict the collection to repositories in 'projectgroup'.""" | ||
212 | 81 | |||
213 | 82 | def inDistribution(distribution): | ||
214 | 83 | """Restrict the collection to repositories in 'distribution'.""" | ||
215 | 84 | |||
216 | 85 | def inDistributionSourcePackage(distro_source_package): | ||
217 | 86 | """Restrict to repositories in a package for a distribution.""" | ||
218 | 87 | |||
219 | 88 | def isPersonal(): | ||
220 | 89 | """Restrict the collection to personal repositories.""" | ||
221 | 90 | |||
222 | 91 | def isPrivate(): | ||
223 | 92 | """Restrict the collection to private repositories.""" | ||
224 | 93 | |||
225 | 94 | def isExclusive(): | ||
226 | 95 | """Restrict the collection to repositories owned by exclusive | ||
227 | 96 | people.""" | ||
228 | 97 | |||
229 | 98 | def ownedBy(person): | ||
230 | 99 | """Restrict the collection to repositories owned by 'person'.""" | ||
231 | 100 | |||
232 | 101 | def ownedByTeamMember(person): | ||
233 | 102 | """Restrict the collection to repositories owned by 'person' or a | ||
234 | 103 | team of which person is a member. | ||
235 | 104 | """ | ||
236 | 105 | |||
237 | 106 | def registeredBy(person): | ||
238 | 107 | """Restrict the collection to repositories registered by 'person'.""" | ||
239 | 108 | |||
240 | 109 | def search(term): | ||
241 | 110 | """Search the collection for repositories matching 'term'. | ||
242 | 111 | |||
243 | 112 | :param term: A string. | ||
244 | 113 | :return: A `ResultSet` of repositories that matched. | ||
245 | 114 | """ | ||
246 | 115 | |||
247 | 116 | def visibleByUser(person): | ||
248 | 117 | """Restrict the collection to repositories that person is allowed to | ||
249 | 118 | see.""" | ||
250 | 119 | |||
251 | 120 | def withIds(*repository_ids): | ||
252 | 121 | """Restrict the collection to repositories with the specified ids.""" | ||
253 | 122 | |||
254 | 123 | |||
255 | 124 | class IAllGitRepositories(IGitCollection): | ||
256 | 125 | """A `IGitCollection` representing all Git repositories in Launchpad.""" | ||
257 | 0 | 126 | ||
258 | === added file 'lib/lp/code/model/gitcollection.py' | |||
259 | --- lib/lp/code/model/gitcollection.py 1970-01-01 00:00:00 +0000 | |||
260 | +++ lib/lp/code/model/gitcollection.py 2015-02-26 17:21:27 +0000 | |||
261 | @@ -0,0 +1,327 @@ | |||
262 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
263 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
264 | 3 | |||
265 | 4 | """Implementations of `IGitCollection`.""" | ||
266 | 5 | |||
267 | 6 | __metaclass__ = type | ||
268 | 7 | __all__ = [ | ||
269 | 8 | 'GenericGitCollection', | ||
270 | 9 | ] | ||
271 | 10 | |||
272 | 11 | from lazr.uri import ( | ||
273 | 12 | InvalidURIError, | ||
274 | 13 | URI, | ||
275 | 14 | ) | ||
276 | 15 | from storm.expr import ( | ||
277 | 16 | Count, | ||
278 | 17 | In, | ||
279 | 18 | Join, | ||
280 | 19 | Select, | ||
281 | 20 | ) | ||
282 | 21 | from zope.component import getUtility | ||
283 | 22 | from zope.interface import implements | ||
284 | 23 | |||
285 | 24 | from lp.app.enums import PRIVATE_INFORMATION_TYPES | ||
286 | 25 | from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES | ||
287 | 26 | from lp.code.interfaces.gitcollection import ( | ||
288 | 27 | IGitCollection, | ||
289 | 28 | InvalidGitFilter, | ||
290 | 29 | ) | ||
291 | 30 | from lp.code.interfaces.gitlookup import IGitLookup | ||
292 | 31 | from lp.code.interfaces.gitrepository import ( | ||
293 | 32 | user_has_special_git_repository_access, | ||
294 | 33 | ) | ||
295 | 34 | from lp.code.model.gitrepository import ( | ||
296 | 35 | GitRepository, | ||
297 | 36 | get_git_repository_privacy_filter, | ||
298 | 37 | ) | ||
299 | 38 | from lp.registry.enums import EXCLUSIVE_TEAM_POLICY | ||
300 | 39 | from lp.registry.model.person import Person | ||
301 | 40 | from lp.registry.model.product import Product | ||
302 | 41 | from lp.registry.model.teammembership import TeamParticipation | ||
303 | 42 | from lp.services.database.bulk import load_related | ||
304 | 43 | from lp.services.database.decoratedresultset import DecoratedResultSet | ||
305 | 44 | from lp.services.database.interfaces import IStore | ||
306 | 45 | from lp.services.propertycache import get_property_cache | ||
307 | 46 | |||
308 | 47 | |||
309 | 48 | class GenericGitCollection: | ||
310 | 49 | """See `IGitCollection`.""" | ||
311 | 50 | |||
312 | 51 | implements(IGitCollection) | ||
313 | 52 | |||
314 | 53 | def __init__(self, store=None, filter_expressions=None, tables=None): | ||
315 | 54 | """Construct a `GenericGitCollection`. | ||
316 | 55 | |||
317 | 56 | :param store: The store to look in for repositories. If not | ||
318 | 57 | specified, use the default store. | ||
319 | 58 | :param filter_expressions: A list of Storm expressions to | ||
320 | 59 | restrict the repositories in the collection. If unspecified, | ||
321 | 60 | then there will be no restrictions on the result set. That is, | ||
322 | 61 | all repositories in the store will be in the collection. | ||
323 | 62 | :param tables: A dict of Storm tables to the Join expression. If an | ||
324 | 63 | expression in filter_expressions refers to a table, then that | ||
325 | 64 | table *must* be in this list. | ||
326 | 65 | """ | ||
327 | 66 | self._store = store | ||
328 | 67 | if filter_expressions is None: | ||
329 | 68 | filter_expressions = [] | ||
330 | 69 | self._filter_expressions = list(filter_expressions) | ||
331 | 70 | if tables is None: | ||
332 | 71 | tables = {} | ||
333 | 72 | self._tables = tables | ||
334 | 73 | self._user = None | ||
335 | 74 | |||
336 | 75 | def count(self): | ||
337 | 76 | """See `IGitCollection`.""" | ||
338 | 77 | return self.getRepositories(eager_load=False).count() | ||
339 | 78 | |||
340 | 79 | def is_empty(self): | ||
341 | 80 | """See `IGitCollection`.""" | ||
342 | 81 | return self.getRepositories(eager_load=False).is_empty() | ||
343 | 82 | |||
344 | 83 | def ownerCounts(self): | ||
345 | 84 | """See `IGitCollection`.""" | ||
346 | 85 | is_team = Person.teamowner != None | ||
347 | 86 | owners = self._getRepositorySelect((GitRepository.owner_id,)) | ||
348 | 87 | counts = dict(self.store.find( | ||
349 | 88 | (is_team, Count(Person.id)), | ||
350 | 89 | Person.id.is_in(owners)).group_by(is_team)) | ||
351 | 90 | return (counts.get(False, 0), counts.get(True, 0)) | ||
352 | 91 | |||
353 | 92 | @property | ||
354 | 93 | def store(self): | ||
355 | 94 | # Although you might think we could set the default value for store | ||
356 | 95 | # in the constructor, we can't. The IStore utility is not available | ||
357 | 96 | # at the time that the ZCML is parsed, which means we get an error | ||
358 | 97 | # if this code is in the constructor. | ||
359 | 98 | # -- JonathanLange 2009-02-17. | ||
360 | 99 | if self._store is None: | ||
361 | 100 | return IStore(GitRepository) | ||
362 | 101 | else: | ||
363 | 102 | return self._store | ||
364 | 103 | |||
365 | 104 | def _filterBy(self, expressions, table=None, join=None): | ||
366 | 105 | """Return a subset of this collection, filtered by 'expressions'.""" | ||
367 | 106 | # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need | ||
368 | 107 | # for explicit 'tables' by harnessing Storm's table inference system. | ||
369 | 108 | # See http://paste.ubuntu.com/118711/ for one way to do that. | ||
370 | 109 | if table is not None and join is None: | ||
371 | 110 | raise InvalidGitFilter("Cannot specify a table without a join.") | ||
372 | 111 | if expressions is None: | ||
373 | 112 | expressions = [] | ||
374 | 113 | tables = self._tables.copy() | ||
375 | 114 | if table is not None: | ||
376 | 115 | tables[table] = join | ||
377 | 116 | return self.__class__( | ||
378 | 117 | self.store, self._filter_expressions + expressions, tables) | ||
379 | 118 | |||
380 | 119 | def _getRepositorySelect(self, columns=(GitRepository.id,)): | ||
381 | 120 | """Return a Storm 'Select' for columns in this collection.""" | ||
382 | 121 | repositories = self.getRepositories( | ||
383 | 122 | eager_load=False, find_expr=columns) | ||
384 | 123 | return repositories.get_plain_result_set()._get_select() | ||
385 | 124 | |||
386 | 125 | def _getRepositoryExpressions(self): | ||
387 | 126 | """Return the where expressions for this collection.""" | ||
388 | 127 | return (self._filter_expressions + | ||
389 | 128 | self._getRepositoryVisibilityExpression()) | ||
390 | 129 | |||
391 | 130 | def _getRepositoryVisibilityExpression(self): | ||
392 | 131 | """Return the where clauses for visibility.""" | ||
393 | 132 | return [] | ||
394 | 133 | |||
395 | 134 | def getRepositories(self, find_expr=GitRepository, eager_load=False): | ||
396 | 135 | """See `IGitCollection`.""" | ||
397 | 136 | tables = [GitRepository] + list(set(self._tables.values())) | ||
398 | 137 | expressions = self._getRepositoryExpressions() | ||
399 | 138 | resultset = self.store.using(*tables).find(find_expr, *expressions) | ||
400 | 139 | |||
401 | 140 | def do_eager_load(rows): | ||
402 | 141 | repository_ids = set(repository.id for repository in rows) | ||
403 | 142 | if not repository_ids: | ||
404 | 143 | return | ||
405 | 144 | load_related(Product, rows, ['project_id']) | ||
406 | 145 | # So far have only needed the persons for their canonical_url - no | ||
407 | 146 | # need for validity etc in the API call. | ||
408 | 147 | load_related(Person, rows, ['owner_id', 'registrant_id']) | ||
409 | 148 | |||
410 | 149 | def cache_permission(repository): | ||
411 | 150 | if self._user: | ||
412 | 151 | get_property_cache(repository)._known_viewers = set( | ||
413 | 152 | [self._user.id]) | ||
414 | 153 | return repository | ||
415 | 154 | |||
416 | 155 | eager_load_hook = ( | ||
417 | 156 | do_eager_load if eager_load and find_expr == GitRepository | ||
418 | 157 | else None) | ||
419 | 158 | return DecoratedResultSet( | ||
420 | 159 | resultset, pre_iter_hook=eager_load_hook, | ||
421 | 160 | result_decorator=cache_permission) | ||
422 | 161 | |||
423 | 162 | def getRepositoryIds(self): | ||
424 | 163 | """See `IGitCollection`.""" | ||
425 | 164 | return self.getRepositories( | ||
426 | 165 | find_expr=GitRepository.id).get_plain_result_set() | ||
427 | 166 | |||
428 | 167 | def getTeamsWithRepositories(self, person): | ||
429 | 168 | """See `IGitCollection`.""" | ||
430 | 169 | # This method doesn't entirely fit with the intent of the | ||
431 | 170 | # GitCollection conceptual model, but we're not quite sure how to | ||
432 | 171 | # fix it just yet. | ||
433 | 172 | repository_query = self._getRepositorySelect((GitRepository.owner_id,)) | ||
434 | 173 | return self.store.find( | ||
435 | 174 | Person, | ||
436 | 175 | Person.id == TeamParticipation.teamID, | ||
437 | 176 | TeamParticipation.person == person, | ||
438 | 177 | TeamParticipation.team != person, | ||
439 | 178 | Person.id.is_in(repository_query)) | ||
440 | 179 | |||
441 | 180 | def inProject(self, project): | ||
442 | 181 | """See `IGitCollection`.""" | ||
443 | 182 | return self._filterBy([GitRepository.project == project]) | ||
444 | 183 | |||
445 | 184 | def inProjectGroup(self, projectgroup): | ||
446 | 185 | """See `IGitCollection`.""" | ||
447 | 186 | return self._filterBy( | ||
448 | 187 | [Product.projectgroup == projectgroup.id], | ||
449 | 188 | table=Product, | ||
450 | 189 | join=Join(Product, GitRepository.project == Product.id)) | ||
451 | 190 | |||
452 | 191 | def inDistribution(self, distribution): | ||
453 | 192 | """See `IGitCollection`.""" | ||
454 | 193 | return self._filterBy([GitRepository.distribution == distribution]) | ||
455 | 194 | |||
456 | 195 | def inDistributionSourcePackage(self, distro_source_package): | ||
457 | 196 | """See `IGitCollection`.""" | ||
458 | 197 | distribution = distro_source_package.distribution | ||
459 | 198 | sourcepackagename = distro_source_package.sourcepackagename | ||
460 | 199 | return self._filterBy( | ||
461 | 200 | [GitRepository.distribution == distribution, | ||
462 | 201 | GitRepository.sourcepackagename == sourcepackagename]) | ||
463 | 202 | |||
464 | 203 | def isPersonal(self): | ||
465 | 204 | """See `IGitCollection`.""" | ||
466 | 205 | return self._filterBy( | ||
467 | 206 | [GitRepository.project == None, | ||
468 | 207 | GitRepository.distribution == None]) | ||
469 | 208 | |||
470 | 209 | def isPrivate(self): | ||
471 | 210 | """See `IGitCollection`.""" | ||
472 | 211 | return self._filterBy( | ||
473 | 212 | [GitRepository.information_type.is_in(PRIVATE_INFORMATION_TYPES)]) | ||
474 | 213 | |||
475 | 214 | def isExclusive(self): | ||
476 | 215 | """See `IGitCollection`.""" | ||
477 | 216 | return self._filterBy( | ||
478 | 217 | [Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY)], | ||
479 | 218 | table=Person, | ||
480 | 219 | join=Join(Person, GitRepository.owner_id == Person.id)) | ||
481 | 220 | |||
482 | 221 | def ownedBy(self, person): | ||
483 | 222 | """See `IGitCollection`.""" | ||
484 | 223 | return self._filterBy([GitRepository.owner == person]) | ||
485 | 224 | |||
486 | 225 | def ownedByTeamMember(self, person): | ||
487 | 226 | """See `IGitCollection`.""" | ||
488 | 227 | subquery = Select( | ||
489 | 228 | TeamParticipation.teamID, | ||
490 | 229 | where=TeamParticipation.personID == person.id) | ||
491 | 230 | return self._filterBy([In(GitRepository.owner_id, subquery)]) | ||
492 | 231 | |||
493 | 232 | def registeredBy(self, person): | ||
494 | 233 | """See `IGitCollection`.""" | ||
495 | 234 | return self._filterBy([GitRepository.registrant == person]) | ||
496 | 235 | |||
497 | 236 | def _getExactMatch(self, term): | ||
498 | 237 | # Look up the repository by its URL, which handles both shortcuts | ||
499 | 238 | # and unique names. | ||
500 | 239 | repository = getUtility(IGitLookup).getByUrl(term) | ||
501 | 240 | if repository is not None: | ||
502 | 241 | return repository | ||
503 | 242 | # Fall back to searching by unique_name, stripping out the path if | ||
504 | 243 | # it's a URI. | ||
505 | 244 | try: | ||
506 | 245 | path = URI(term).path.strip("/") | ||
507 | 246 | except InvalidURIError: | ||
508 | 247 | path = term | ||
509 | 248 | return getUtility(IGitLookup).getByUniqueName(path) | ||
510 | 249 | |||
511 | 250 | def search(self, term): | ||
512 | 251 | """See `IGitCollection`.""" | ||
513 | 252 | repository = self._getExactMatch(term) | ||
514 | 253 | if repository: | ||
515 | 254 | collection = self._filterBy([GitRepository.id == repository.id]) | ||
516 | 255 | else: | ||
517 | 256 | term = unicode(term) | ||
518 | 257 | # Filter by name. | ||
519 | 258 | field = GitRepository.name | ||
520 | 259 | # Except if the term contains /, when we use unique_name. | ||
521 | 260 | # XXX cjwatson 2015-02-06: Disabled until the URL format settles | ||
522 | 261 | # down, at which point we can make GitRepository.unique_name a | ||
523 | 262 | # trigger-maintained column rather than a property. | ||
524 | 263 | #if '/' in term: | ||
525 | 264 | # field = GitRepository.unique_name | ||
526 | 265 | collection = self._filterBy( | ||
527 | 266 | [field.lower().contains_string(term.lower())]) | ||
528 | 267 | return collection.getRepositories(eager_load=False).order_by( | ||
529 | 268 | GitRepository.name, GitRepository.id) | ||
530 | 269 | |||
531 | 270 | def visibleByUser(self, person): | ||
532 | 271 | """See `IGitCollection`.""" | ||
533 | 272 | if (person == LAUNCHPAD_SERVICES or | ||
534 | 273 | user_has_special_git_repository_access(person)): | ||
535 | 274 | return self | ||
536 | 275 | if person is None: | ||
537 | 276 | return AnonymousGitCollection( | ||
538 | 277 | self._store, self._filter_expressions, self._tables) | ||
539 | 278 | return VisibleGitCollection( | ||
540 | 279 | person, self._store, self._filter_expressions, self._tables) | ||
541 | 280 | |||
542 | 281 | def withIds(self, *repository_ids): | ||
543 | 282 | """See `IGitCollection`.""" | ||
544 | 283 | return self._filterBy([GitRepository.id.is_in(repository_ids)]) | ||
545 | 284 | |||
546 | 285 | |||
547 | 286 | class AnonymousGitCollection(GenericGitCollection): | ||
548 | 287 | """Repository collection that only shows public repositories.""" | ||
549 | 288 | |||
550 | 289 | def _getRepositoryVisibilityExpression(self): | ||
551 | 290 | """Return the where clauses for visibility.""" | ||
552 | 291 | return get_git_repository_privacy_filter(None) | ||
553 | 292 | |||
554 | 293 | |||
555 | 294 | class VisibleGitCollection(GenericGitCollection): | ||
556 | 295 | """A repository collection that has special logic for visibility.""" | ||
557 | 296 | |||
558 | 297 | def __init__(self, user, store=None, filter_expressions=None, tables=None): | ||
559 | 298 | super(VisibleGitCollection, self).__init__( | ||
560 | 299 | store=store, filter_expressions=filter_expressions, tables=tables) | ||
561 | 300 | self._user = user | ||
562 | 301 | |||
563 | 302 | def _filterBy(self, expressions, table=None, join=None): | ||
564 | 303 | """Return a subset of this collection, filtered by 'expressions'.""" | ||
565 | 304 | # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need | ||
566 | 305 | # for explicit 'tables' by harnessing Storm's table inference system. | ||
567 | 306 | # See http://paste.ubuntu.com/118711/ for one way to do that. | ||
568 | 307 | if table is not None and join is None: | ||
569 | 308 | raise InvalidGitFilter("Cannot specify a table without a join.") | ||
570 | 309 | if expressions is None: | ||
571 | 310 | expressions = [] | ||
572 | 311 | tables = self._tables.copy() | ||
573 | 312 | if table is not None: | ||
574 | 313 | tables[table] = join | ||
575 | 314 | return self.__class__( | ||
576 | 315 | self._user, self.store, self._filter_expressions + expressions) | ||
577 | 316 | |||
578 | 317 | def _getRepositoryVisibilityExpression(self): | ||
579 | 318 | """Return the where clauses for visibility.""" | ||
580 | 319 | return get_git_repository_privacy_filter(self._user) | ||
581 | 320 | |||
582 | 321 | def visibleByUser(self, person): | ||
583 | 322 | """See `IGitCollection`.""" | ||
584 | 323 | if person == self._user: | ||
585 | 324 | return self | ||
586 | 325 | raise InvalidGitFilter( | ||
587 | 326 | "Cannot filter for Git repositories visible by user %r, already " | ||
588 | 327 | "filtering for %r" % (person, self._user)) | ||
589 | 0 | 328 | ||
590 | === added file 'lib/lp/code/model/tests/test_gitcollection.py' | |||
591 | --- lib/lp/code/model/tests/test_gitcollection.py 1970-01-01 00:00:00 +0000 | |||
592 | +++ lib/lp/code/model/tests/test_gitcollection.py 2015-02-26 17:21:27 +0000 | |||
593 | @@ -0,0 +1,716 @@ | |||
594 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
595 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
596 | 3 | |||
597 | 4 | """Tests for Git repository collections.""" | ||
598 | 5 | |||
599 | 6 | __metaclass__ = type | ||
600 | 7 | |||
601 | 8 | from testtools.matchers import Equals | ||
602 | 9 | from zope.component import getUtility | ||
603 | 10 | from zope.security.proxy import removeSecurityProxy | ||
604 | 11 | |||
605 | 12 | from lp.app.enums import InformationType | ||
606 | 13 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | ||
607 | 14 | from lp.app.interfaces.services import IService | ||
608 | 15 | from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES | ||
609 | 16 | from lp.code.interfaces.gitcollection import ( | ||
610 | 17 | IAllGitRepositories, | ||
611 | 18 | IGitCollection, | ||
612 | 19 | ) | ||
613 | 20 | from lp.code.interfaces.gitrepository import IGitRepositorySet | ||
614 | 21 | from lp.code.model.gitcollection import GenericGitCollection | ||
615 | 22 | from lp.code.model.gitrepository import GitRepository | ||
616 | 23 | from lp.registry.enums import PersonVisibility | ||
617 | 24 | from lp.registry.interfaces.person import TeamMembershipPolicy | ||
618 | 25 | from lp.registry.model.persondistributionsourcepackage import ( | ||
619 | 26 | PersonDistributionSourcePackage, | ||
620 | 27 | ) | ||
621 | 28 | from lp.registry.model.personproduct import PersonProduct | ||
622 | 29 | from lp.services.database.interfaces import IStore | ||
623 | 30 | from lp.testing import ( | ||
624 | 31 | person_logged_in, | ||
625 | 32 | StormStatementRecorder, | ||
626 | 33 | TestCaseWithFactory, | ||
627 | 34 | ) | ||
628 | 35 | from lp.testing.layers import DatabaseFunctionalLayer | ||
629 | 36 | from lp.testing.matchers import HasQueryCount | ||
630 | 37 | |||
631 | 38 | |||
632 | 39 | class TestGitCollectionAdaptation(TestCaseWithFactory): | ||
633 | 40 | """Check that certain objects can be adapted to a Git repository | ||
634 | 41 | collection.""" | ||
635 | 42 | |||
636 | 43 | layer = DatabaseFunctionalLayer | ||
637 | 44 | |||
638 | 45 | def assertCollection(self, target): | ||
639 | 46 | self.assertIsNotNone(IGitCollection(target, None)) | ||
640 | 47 | |||
641 | 48 | def test_project(self): | ||
642 | 49 | # A project can be adapted to a Git repository collection. | ||
643 | 50 | self.assertCollection(self.factory.makeProduct()) | ||
644 | 51 | |||
645 | 52 | def test_project_group(self): | ||
646 | 53 | # A project group can be adapted to a Git repository collection. | ||
647 | 54 | self.assertCollection(self.factory.makeProject()) | ||
648 | 55 | |||
649 | 56 | def test_distribution(self): | ||
650 | 57 | # A distribution can be adapted to a Git repository collection. | ||
651 | 58 | self.assertCollection(self.factory.makeDistribution()) | ||
652 | 59 | |||
653 | 60 | def test_distribution_source_package(self): | ||
654 | 61 | # A distribution source package can be adapted to a Git repository | ||
655 | 62 | # collection. | ||
656 | 63 | self.assertCollection(self.factory.makeDistributionSourcePackage()) | ||
657 | 64 | |||
658 | 65 | def test_person(self): | ||
659 | 66 | # A person can be adapted to a Git repository collection. | ||
660 | 67 | self.assertCollection(self.factory.makePerson()) | ||
661 | 68 | |||
662 | 69 | def test_person_product(self): | ||
663 | 70 | # A PersonProduct can be adapted to a Git repository collection. | ||
664 | 71 | project = self.factory.makeProduct() | ||
665 | 72 | self.assertCollection(PersonProduct(project.owner, project)) | ||
666 | 73 | |||
667 | 74 | def test_person_distribution_source_package(self): | ||
668 | 75 | # A PersonDistributionSourcePackage can be adapted to a Git | ||
669 | 76 | # repository collection. | ||
670 | 77 | dsp = self.factory.makeDistributionSourcePackage() | ||
671 | 78 | self.assertCollection( | ||
672 | 79 | PersonDistributionSourcePackage(dsp.distribution.owner, dsp)) | ||
673 | 80 | |||
674 | 81 | |||
675 | 82 | class TestGenericGitCollection(TestCaseWithFactory): | ||
676 | 83 | |||
677 | 84 | layer = DatabaseFunctionalLayer | ||
678 | 85 | |||
679 | 86 | def setUp(self): | ||
680 | 87 | super(TestGenericGitCollection, self).setUp() | ||
681 | 88 | self.store = IStore(GitRepository) | ||
682 | 89 | |||
683 | 90 | def test_provides_gitcollection(self): | ||
684 | 91 | # `GenericGitCollection` provides the `IGitCollection` | ||
685 | 92 | # interface. | ||
686 | 93 | self.assertProvides(GenericGitCollection(self.store), IGitCollection) | ||
687 | 94 | |||
688 | 95 | def test_getRepositories_no_filter_no_repositories(self): | ||
689 | 96 | # If no filter is specified, then the collection is of all | ||
690 | 97 | # repositories in Launchpad. By default, there are no repositories. | ||
691 | 98 | collection = GenericGitCollection(self.store) | ||
692 | 99 | self.assertEqual([], list(collection.getRepositories())) | ||
693 | 100 | |||
694 | 101 | def test_getRepositories_no_filter(self): | ||
695 | 102 | # If no filter is specified, then the collection is of all | ||
696 | 103 | # repositories in Launchpad. | ||
697 | 104 | collection = GenericGitCollection(self.store) | ||
698 | 105 | repository = self.factory.makeGitRepository() | ||
699 | 106 | self.assertEqual([repository], list(collection.getRepositories())) | ||
700 | 107 | |||
701 | 108 | def test_getRepositories_project_filter(self): | ||
702 | 109 | # If the specified filter is for the repositories of a particular | ||
703 | 110 | # project, then the collection contains only repositories of that | ||
704 | 111 | # project. | ||
705 | 112 | repository = self.factory.makeGitRepository() | ||
706 | 113 | self.factory.makeGitRepository() | ||
707 | 114 | collection = GenericGitCollection( | ||
708 | 115 | self.store, [GitRepository.project == repository.target]) | ||
709 | 116 | self.assertEqual([repository], list(collection.getRepositories())) | ||
710 | 117 | |||
711 | 118 | def test_getRepositories_caches_viewers(self): | ||
712 | 119 | # getRepositories() caches the user as a known viewer so that | ||
713 | 120 | # repository.visibleByUser() does not have to hit the database. | ||
714 | 121 | collection = GenericGitCollection(self.store) | ||
715 | 122 | owner = self.factory.makePerson() | ||
716 | 123 | project = self.factory.makeProduct() | ||
717 | 124 | repository = self.factory.makeGitRepository( | ||
718 | 125 | owner=owner, target=project, | ||
719 | 126 | information_type=InformationType.USERDATA) | ||
720 | 127 | someone = self.factory.makePerson() | ||
721 | 128 | with person_logged_in(owner): | ||
722 | 129 | getUtility(IService, 'sharing').ensureAccessGrants( | ||
723 | 130 | [someone], owner, gitrepositories=[repository], | ||
724 | 131 | ignore_permissions=True) | ||
725 | 132 | [repository] = list(collection.visibleByUser( | ||
726 | 133 | someone).getRepositories()) | ||
727 | 134 | with StormStatementRecorder() as recorder: | ||
728 | 135 | self.assertTrue(repository.visibleByUser(someone)) | ||
729 | 136 | self.assertThat(recorder, HasQueryCount(Equals(0))) | ||
730 | 137 | |||
731 | 138 | def test_getRepositoryIds(self): | ||
732 | 139 | repository = self.factory.makeGitRepository() | ||
733 | 140 | self.factory.makeGitRepository() | ||
734 | 141 | collection = GenericGitCollection( | ||
735 | 142 | self.store, [GitRepository.project == repository.target]) | ||
736 | 143 | self.assertEqual([repository.id], list(collection.getRepositoryIds())) | ||
737 | 144 | |||
738 | 145 | def test_count(self): | ||
739 | 146 | # The 'count' property of a collection is the number of elements in | ||
740 | 147 | # the collection. | ||
741 | 148 | collection = GenericGitCollection(self.store) | ||
742 | 149 | self.assertEqual(0, collection.count()) | ||
743 | 150 | for i in range(3): | ||
744 | 151 | self.factory.makeGitRepository() | ||
745 | 152 | self.assertEqual(3, collection.count()) | ||
746 | 153 | |||
747 | 154 | def test_count_respects_filter(self): | ||
748 | 155 | # If a collection is a subset of all possible repositories, then the | ||
749 | 156 | # count will be the size of that subset. That is, 'count' respects | ||
750 | 157 | # any filters that are applied. | ||
751 | 158 | repository = self.factory.makeGitRepository() | ||
752 | 159 | self.factory.makeGitRepository() | ||
753 | 160 | collection = GenericGitCollection( | ||
754 | 161 | self.store, [GitRepository.project == repository.target]) | ||
755 | 162 | self.assertEqual(1, collection.count()) | ||
756 | 163 | |||
757 | 164 | |||
758 | 165 | class TestGitCollectionFilters(TestCaseWithFactory): | ||
759 | 166 | |||
760 | 167 | layer = DatabaseFunctionalLayer | ||
761 | 168 | |||
762 | 169 | def setUp(self): | ||
763 | 170 | TestCaseWithFactory.setUp(self) | ||
764 | 171 | self.all_repositories = getUtility(IAllGitRepositories) | ||
765 | 172 | |||
766 | 173 | def test_order_by_repository_name(self): | ||
767 | 174 | # The result of getRepositories() can be ordered by | ||
768 | 175 | # `GitRepository.name`, no matter what filters are applied. | ||
769 | 176 | aardvark = self.factory.makeProduct(name='aardvark') | ||
770 | 177 | badger = self.factory.makeProduct(name='badger') | ||
771 | 178 | repository_a = self.factory.makeGitRepository(target=aardvark) | ||
772 | 179 | repository_b = self.factory.makeGitRepository(target=badger) | ||
773 | 180 | person = self.factory.makePerson() | ||
774 | 181 | repository_c = self.factory.makeGitRepository( | ||
775 | 182 | owner=person, target=person) | ||
776 | 183 | self.assertEqual( | ||
777 | 184 | sorted([repository_a, repository_b, repository_c]), | ||
778 | 185 | sorted(self.all_repositories.getRepositories() | ||
779 | 186 | .order_by(GitRepository.name))) | ||
780 | 187 | |||
781 | 188 | def test_count_respects_visibleByUser_filter(self): | ||
782 | 189 | # IGitCollection.count() returns the number of repositories that | ||
783 | 190 | # getRepositories() yields, even when the visibleByUser filter is | ||
784 | 191 | # applied. | ||
785 | 192 | repository = self.factory.makeGitRepository() | ||
786 | 193 | self.factory.makeGitRepository( | ||
787 | 194 | information_type=InformationType.USERDATA) | ||
788 | 195 | collection = self.all_repositories.visibleByUser(repository.owner) | ||
789 | 196 | self.assertEqual(1, collection.getRepositories().count()) | ||
790 | 197 | self.assertEqual(1, len(list(collection.getRepositories()))) | ||
791 | 198 | self.assertEqual(1, collection.count()) | ||
792 | 199 | |||
793 | 200 | def test_ownedBy(self): | ||
794 | 201 | # 'ownedBy' returns a new collection restricted to repositories | ||
795 | 202 | # owned by the given person. | ||
796 | 203 | repository = self.factory.makeGitRepository() | ||
797 | 204 | self.factory.makeGitRepository() | ||
798 | 205 | collection = self.all_repositories.ownedBy(repository.owner) | ||
799 | 206 | self.assertEqual([repository], list(collection.getRepositories())) | ||
800 | 207 | |||
801 | 208 | def test_ownedByTeamMember(self): | ||
802 | 209 | # 'ownedBy' returns a new collection restricted to repositories | ||
803 | 210 | # owned by any team of which the given person is a member. | ||
804 | 211 | person = self.factory.makePerson() | ||
805 | 212 | team = self.factory.makeTeam(members=[person]) | ||
806 | 213 | repository = self.factory.makeGitRepository(owner=team) | ||
807 | 214 | self.factory.makeGitRepository() | ||
808 | 215 | collection = self.all_repositories.ownedByTeamMember(person) | ||
809 | 216 | self.assertEqual([repository], list(collection.getRepositories())) | ||
810 | 217 | |||
811 | 218 | def test_in_project(self): | ||
812 | 219 | # 'inProject' returns a new collection restricted to repositories in | ||
813 | 220 | # the given project. | ||
814 | 221 | # | ||
815 | 222 | # NOTE: JonathanLange 2009-02-11: Maybe this should be a more | ||
816 | 223 | # generic method called 'onTarget' that takes a person, package or | ||
817 | 224 | # project. | ||
818 | 225 | repository = self.factory.makeGitRepository() | ||
819 | 226 | self.factory.makeGitRepository() | ||
820 | 227 | collection = self.all_repositories.inProject(repository.target) | ||
821 | 228 | self.assertEqual([repository], list(collection.getRepositories())) | ||
822 | 229 | |||
823 | 230 | def test_inProjectGroup(self): | ||
824 | 231 | # 'inProjectGroup' returns a new collection restricted to | ||
825 | 232 | # repositories in the given project group. | ||
826 | 233 | repository = self.factory.makeGitRepository() | ||
827 | 234 | self.factory.makeGitRepository() | ||
828 | 235 | projectgroup = self.factory.makeProject() | ||
829 | 236 | removeSecurityProxy(repository.target).projectgroup = projectgroup | ||
830 | 237 | collection = self.all_repositories.inProjectGroup(projectgroup) | ||
831 | 238 | self.assertEqual([repository], list(collection.getRepositories())) | ||
832 | 239 | |||
833 | 240 | def test_isExclusive(self): | ||
834 | 241 | # 'isExclusive' is restricted to repositories owned by exclusive | ||
835 | 242 | # teams and users. | ||
836 | 243 | user = self.factory.makePerson() | ||
837 | 244 | team = self.factory.makeTeam( | ||
838 | 245 | membership_policy=TeamMembershipPolicy.RESTRICTED) | ||
839 | 246 | other_team = self.factory.makeTeam( | ||
840 | 247 | membership_policy=TeamMembershipPolicy.OPEN) | ||
841 | 248 | team_repository = self.factory.makeGitRepository(owner=team) | ||
842 | 249 | user_repository = self.factory.makeGitRepository(owner=user) | ||
843 | 250 | self.factory.makeGitRepository(owner=other_team) | ||
844 | 251 | collection = self.all_repositories.isExclusive() | ||
845 | 252 | self.assertContentEqual( | ||
846 | 253 | [team_repository, user_repository], | ||
847 | 254 | list(collection.getRepositories())) | ||
848 | 255 | |||
849 | 256 | def test_inProject_and_isExclusive(self): | ||
850 | 257 | # 'inProject' and 'isExclusive' can combine to form a collection | ||
851 | 258 | # that is restricted to repositories of a particular project owned | ||
852 | 259 | # by exclusive teams and users. | ||
853 | 260 | team = self.factory.makeTeam( | ||
854 | 261 | membership_policy=TeamMembershipPolicy.RESTRICTED) | ||
855 | 262 | other_team = self.factory.makeTeam( | ||
856 | 263 | membership_policy=TeamMembershipPolicy.OPEN) | ||
857 | 264 | project = self.factory.makeProduct() | ||
858 | 265 | repository = self.factory.makeGitRepository(target=project, owner=team) | ||
859 | 266 | self.factory.makeGitRepository(owner=team) | ||
860 | 267 | self.factory.makeGitRepository(target=project, owner=other_team) | ||
861 | 268 | collection = self.all_repositories.inProject(project).isExclusive() | ||
862 | 269 | self.assertEqual([repository], list(collection.getRepositories())) | ||
863 | 270 | collection = self.all_repositories.isExclusive().inProject(project) | ||
864 | 271 | self.assertEqual([repository], list(collection.getRepositories())) | ||
865 | 272 | |||
866 | 273 | def test_ownedBy_and_inProject(self): | ||
867 | 274 | # 'ownedBy' and 'inProject' can combine to form a collection that is | ||
868 | 275 | # restricted to repositories of a particular project owned by a | ||
869 | 276 | # particular person. | ||
870 | 277 | person = self.factory.makePerson() | ||
871 | 278 | project = self.factory.makeProduct() | ||
872 | 279 | repository = self.factory.makeGitRepository( | ||
873 | 280 | target=project, owner=person) | ||
874 | 281 | self.factory.makeGitRepository(owner=person) | ||
875 | 282 | self.factory.makeGitRepository(target=project) | ||
876 | 283 | collection = self.all_repositories.inProject(project).ownedBy(person) | ||
877 | 284 | self.assertEqual([repository], list(collection.getRepositories())) | ||
878 | 285 | collection = self.all_repositories.ownedBy(person).inProject(project) | ||
879 | 286 | self.assertEqual([repository], list(collection.getRepositories())) | ||
880 | 287 | |||
881 | 288 | def test_ownedBy_and_isPrivate(self): | ||
882 | 289 | # 'ownedBy' and 'isPrivate' can combine to form a collection that is | ||
883 | 290 | # restricted to private repositories owned by a particular person. | ||
884 | 291 | person = self.factory.makePerson() | ||
885 | 292 | project = self.factory.makeProduct() | ||
886 | 293 | repository = self.factory.makeGitRepository( | ||
887 | 294 | target=project, owner=person, | ||
888 | 295 | information_type=InformationType.USERDATA) | ||
889 | 296 | self.factory.makeGitRepository(owner=person) | ||
890 | 297 | self.factory.makeGitRepository(target=project) | ||
891 | 298 | collection = self.all_repositories.isPrivate().ownedBy(person) | ||
892 | 299 | self.assertEqual([repository], list(collection.getRepositories())) | ||
893 | 300 | collection = self.all_repositories.ownedBy(person).isPrivate() | ||
894 | 301 | self.assertEqual([repository], list(collection.getRepositories())) | ||
895 | 302 | |||
896 | 303 | def test_ownedByTeamMember_and_inProject(self): | ||
897 | 304 | # 'ownedBy' and 'inProject' can combine to form a collection that is | ||
898 | 305 | # restricted to repositories of a particular project owned by a | ||
899 | 306 | # particular person or team of which the person is a member. | ||
900 | 307 | person = self.factory.makePerson() | ||
901 | 308 | team = self.factory.makeTeam(members=[person]) | ||
902 | 309 | project = self.factory.makeProduct() | ||
903 | 310 | repository = self.factory.makeGitRepository( | ||
904 | 311 | target=project, owner=person) | ||
905 | 312 | repository2 = self.factory.makeGitRepository( | ||
906 | 313 | target=project, owner=team) | ||
907 | 314 | self.factory.makeGitRepository(owner=person) | ||
908 | 315 | self.factory.makeGitRepository(target=project) | ||
909 | 316 | project_repositories = self.all_repositories.inProject(project) | ||
910 | 317 | collection = project_repositories.ownedByTeamMember(person) | ||
911 | 318 | self.assertContentEqual( | ||
912 | 319 | [repository, repository2], collection.getRepositories()) | ||
913 | 320 | person_repositories = self.all_repositories.ownedByTeamMember(person) | ||
914 | 321 | collection = person_repositories.inProject(project) | ||
915 | 322 | self.assertContentEqual( | ||
916 | 323 | [repository, repository2], collection.getRepositories()) | ||
917 | 324 | |||
918 | 325 | def test_in_distribution(self): | ||
919 | 326 | # 'inDistribution' returns a new collection that only has | ||
920 | 327 | # repositories that are package repositories associated with the | ||
921 | 328 | # distribution specified. | ||
922 | 329 | distro = self.factory.makeDistribution() | ||
923 | 330 | # Make two repositories in the same distribution, but different | ||
924 | 331 | # source packages. | ||
925 | 332 | dsp = self.factory.makeDistributionSourcePackage(distribution=distro) | ||
926 | 333 | repository = self.factory.makeGitRepository(target=dsp) | ||
927 | 334 | dsp2 = self.factory.makeDistributionSourcePackage(distribution=distro) | ||
928 | 335 | repository2 = self.factory.makeGitRepository(target=dsp2) | ||
929 | 336 | # Another repository in a different distribution. | ||
930 | 337 | self.factory.makeGitRepository( | ||
931 | 338 | target=self.factory.makeDistributionSourcePackage()) | ||
932 | 339 | # And a project repository. | ||
933 | 340 | self.factory.makeGitRepository() | ||
934 | 341 | collection = self.all_repositories.inDistribution(distro) | ||
935 | 342 | self.assertEqual( | ||
936 | 343 | sorted([repository, repository2]), | ||
937 | 344 | sorted(collection.getRepositories())) | ||
938 | 345 | |||
939 | 346 | def test_in_distribution_source_package(self): | ||
940 | 347 | # 'inDistributionSourcePackage' returns a new collection that only | ||
941 | 348 | # has repositories for the source package in the distribution. | ||
942 | 349 | distro = self.factory.makeDistribution() | ||
943 | 350 | dsp = self.factory.makeDistributionSourcePackage(distribution=distro) | ||
944 | 351 | dsp_other_distro = self.factory.makeDistributionSourcePackage() | ||
945 | 352 | repository = self.factory.makeGitRepository(target=dsp) | ||
946 | 353 | repository2 = self.factory.makeGitRepository(target=dsp) | ||
947 | 354 | self.factory.makeGitRepository(target=dsp_other_distro) | ||
948 | 355 | self.factory.makeGitRepository() | ||
949 | 356 | collection = self.all_repositories.inDistributionSourcePackage(dsp) | ||
950 | 357 | self.assertEqual( | ||
951 | 358 | sorted([repository, repository2]), | ||
952 | 359 | sorted(collection.getRepositories())) | ||
953 | 360 | |||
954 | 361 | def test_withIds(self): | ||
955 | 362 | # 'withIds' returns a new collection that only has repositories with | ||
956 | 363 | # the given ids. | ||
957 | 364 | repository1 = self.factory.makeGitRepository() | ||
958 | 365 | repository2 = self.factory.makeGitRepository() | ||
959 | 366 | self.factory.makeGitRepository() | ||
960 | 367 | ids = [repository1.id, repository2.id] | ||
961 | 368 | collection = self.all_repositories.withIds(*ids) | ||
962 | 369 | self.assertEqual( | ||
963 | 370 | sorted([repository1, repository2]), | ||
964 | 371 | sorted(collection.getRepositories())) | ||
965 | 372 | |||
966 | 373 | def test_registeredBy(self): | ||
967 | 374 | # 'registeredBy' returns a new collection that only has repositories | ||
968 | 375 | # that were registered by the given user. | ||
969 | 376 | registrant = self.factory.makePerson() | ||
970 | 377 | repository = self.factory.makeGitRepository( | ||
971 | 378 | owner=registrant, registrant=registrant) | ||
972 | 379 | removeSecurityProxy(repository).owner = self.factory.makePerson() | ||
973 | 380 | self.factory.makeGitRepository() | ||
974 | 381 | collection = self.all_repositories.registeredBy(registrant) | ||
975 | 382 | self.assertEqual([repository], list(collection.getRepositories())) | ||
976 | 383 | |||
977 | 384 | |||
978 | 385 | class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory): | ||
979 | 386 | |||
980 | 387 | layer = DatabaseFunctionalLayer | ||
981 | 388 | |||
982 | 389 | def setUp(self): | ||
983 | 390 | TestCaseWithFactory.setUp(self) | ||
984 | 391 | self.public_repository = self.factory.makeGitRepository(name=u'public') | ||
985 | 392 | self.private_repository = self.factory.makeGitRepository( | ||
986 | 393 | name=u'private', information_type=InformationType.USERDATA) | ||
987 | 394 | self.all_repositories = getUtility(IAllGitRepositories) | ||
988 | 395 | |||
989 | 396 | def test_all_repositories(self): | ||
990 | 397 | # Without the visibleByUser filter, all repositories are in the | ||
991 | 398 | # collection. | ||
992 | 399 | self.assertEqual( | ||
993 | 400 | sorted([self.public_repository, self.private_repository]), | ||
994 | 401 | sorted(self.all_repositories.getRepositories())) | ||
995 | 402 | |||
996 | 403 | def test_anonymous_sees_only_public(self): | ||
997 | 404 | # Anonymous users can see only public repositories. | ||
998 | 405 | repositories = self.all_repositories.visibleByUser(None) | ||
999 | 406 | self.assertEqual( | ||
1000 | 407 | [self.public_repository], list(repositories.getRepositories())) | ||
1001 | 408 | |||
1002 | 409 | def test_visibility_then_project(self): | ||
1003 | 410 | # We can apply other filters after applying the visibleByUser filter. | ||
1004 | 411 | # Create another public repository. | ||
1005 | 412 | self.factory.makeGitRepository() | ||
1006 | 413 | repositories = self.all_repositories.visibleByUser(None).inProject( | ||
1007 | 414 | self.public_repository.target).getRepositories() | ||
1008 | 415 | self.assertEqual([self.public_repository], list(repositories)) | ||
1009 | 416 | |||
1010 | 417 | def test_random_person_sees_only_public(self): | ||
1011 | 418 | # Logged in users with no special permissions can see only public | ||
1012 | 419 | # repositories. | ||
1013 | 420 | person = self.factory.makePerson() | ||
1014 | 421 | repositories = self.all_repositories.visibleByUser(person) | ||
1015 | 422 | self.assertEqual( | ||
1016 | 423 | [self.public_repository], list(repositories.getRepositories())) | ||
1017 | 424 | |||
1018 | 425 | def test_owner_sees_own_repositories(self): | ||
1019 | 426 | # Users can always see the repositories that they own, as well as public | ||
1020 | 427 | # repositories. | ||
1021 | 428 | owner = removeSecurityProxy(self.private_repository).owner | ||
1022 | 429 | repositories = self.all_repositories.visibleByUser(owner) | ||
1023 | 430 | self.assertEqual( | ||
1024 | 431 | sorted([self.public_repository, self.private_repository]), | ||
1025 | 432 | sorted(repositories.getRepositories())) | ||
1026 | 433 | |||
1027 | 434 | def test_launchpad_services_sees_all(self): | ||
1028 | 435 | # The LAUNCHPAD_SERVICES special user sees *everything*. | ||
1029 | 436 | repositories = self.all_repositories.visibleByUser(LAUNCHPAD_SERVICES) | ||
1030 | 437 | self.assertEqual( | ||
1031 | 438 | sorted(self.all_repositories.getRepositories()), | ||
1032 | 439 | sorted(repositories.getRepositories())) | ||
1033 | 440 | |||
1034 | 441 | def test_admins_see_all(self): | ||
1035 | 442 | # Launchpad administrators see *everything*. | ||
1036 | 443 | admin = self.factory.makePerson() | ||
1037 | 444 | admin_team = removeSecurityProxy( | ||
1038 | 445 | getUtility(ILaunchpadCelebrities).admin) | ||
1039 | 446 | admin_team.addMember(admin, admin_team.teamowner) | ||
1040 | 447 | repositories = self.all_repositories.visibleByUser(admin) | ||
1041 | 448 | self.assertEqual( | ||
1042 | 449 | sorted(self.all_repositories.getRepositories()), | ||
1043 | 450 | sorted(repositories.getRepositories())) | ||
1044 | 451 | |||
1045 | 452 | def test_private_teams_see_own_private_personal_repositories(self): | ||
1046 | 453 | # Private teams are given an access grant to see their private | ||
1047 | 454 | # personal repositories. | ||
1048 | 455 | team_owner = self.factory.makePerson() | ||
1049 | 456 | team = self.factory.makeTeam( | ||
1050 | 457 | visibility=PersonVisibility.PRIVATE, | ||
1051 | 458 | membership_policy=TeamMembershipPolicy.MODERATED, | ||
1052 | 459 | owner=team_owner) | ||
1053 | 460 | with person_logged_in(team_owner): | ||
1054 | 461 | personal_repository = self.factory.makeGitRepository( | ||
1055 | 462 | owner=team, target=team, | ||
1056 | 463 | information_type=InformationType.USERDATA) | ||
1057 | 464 | # The team is automatically subscribed to the repository since | ||
1058 | 465 | # they are the owner. We want to unsubscribe them so that they | ||
1059 | 466 | # lose access conferred via subscription and rely instead on the | ||
1060 | 467 | # APG. | ||
1061 | 468 | # XXX cjwatson 2015-02-05: Uncomment this once | ||
1062 | 469 | # GitRepositorySubscriptions exist. | ||
1063 | 470 | #personal_repository.unsubscribe(team, team_owner, True) | ||
1064 | 471 | # Make another personal repository the team can't see. | ||
1065 | 472 | other_person = self.factory.makePerson() | ||
1066 | 473 | self.factory.makeGitRepository( | ||
1067 | 474 | owner=other_person, target=other_person, | ||
1068 | 475 | information_type=InformationType.USERDATA) | ||
1069 | 476 | repositories = self.all_repositories.visibleByUser(team) | ||
1070 | 477 | self.assertEqual( | ||
1071 | 478 | sorted([self.public_repository, personal_repository]), | ||
1072 | 479 | sorted(repositories.getRepositories())) | ||
1073 | 480 | |||
1074 | 481 | |||
1075 | 482 | class TestSearch(TestCaseWithFactory): | ||
1076 | 483 | """Tests for IGitCollection.search().""" | ||
1077 | 484 | |||
1078 | 485 | layer = DatabaseFunctionalLayer | ||
1079 | 486 | |||
1080 | 487 | def setUp(self): | ||
1081 | 488 | TestCaseWithFactory.setUp(self) | ||
1082 | 489 | self.collection = getUtility(IAllGitRepositories) | ||
1083 | 490 | |||
1084 | 491 | def test_exact_match_unique_name(self): | ||
1085 | 492 | # If you search for a unique name of a repository that exists, | ||
1086 | 493 | # you'll get a single result with a repository with that repository | ||
1087 | 494 | # name. | ||
1088 | 495 | repository = self.factory.makeGitRepository() | ||
1089 | 496 | self.factory.makeGitRepository() | ||
1090 | 497 | search_results = self.collection.search(repository.unique_name) | ||
1091 | 498 | self.assertEqual([repository], list(search_results)) | ||
1092 | 499 | |||
1093 | 500 | def test_unique_name_match_not_in_collection(self): | ||
1094 | 501 | # If you search for a unique name of a repository that does not | ||
1095 | 502 | # exist, you'll get an empty result set. | ||
1096 | 503 | repository = self.factory.makeGitRepository() | ||
1097 | 504 | collection = self.collection.inProject(self.factory.makeProduct()) | ||
1098 | 505 | search_results = collection.search(repository.unique_name) | ||
1099 | 506 | self.assertEqual([], list(search_results)) | ||
1100 | 507 | |||
1101 | 508 | def test_exact_match_launchpad_url(self): | ||
1102 | 509 | # If you search for the Launchpad URL of a repository, and there is | ||
1103 | 510 | # a repository with that URL, then you get a single result with that | ||
1104 | 511 | # repository. | ||
1105 | 512 | repository = self.factory.makeGitRepository() | ||
1106 | 513 | self.factory.makeGitRepository() | ||
1107 | 514 | search_results = self.collection.search(repository.getCodebrowseUrl()) | ||
1108 | 515 | self.assertEqual([repository], list(search_results)) | ||
1109 | 516 | |||
1110 | 517 | def test_exact_match_with_lp_colon_url(self): | ||
1111 | 518 | repository = self.factory.makeGitRepository() | ||
1112 | 519 | lp_name = 'lp:' + repository.unique_name | ||
1113 | 520 | search_results = self.collection.search(lp_name) | ||
1114 | 521 | self.assertEqual([repository], list(search_results)) | ||
1115 | 522 | |||
1116 | 523 | def test_exact_match_bad_url(self): | ||
1117 | 524 | search_results = self.collection.search('http:hahafail') | ||
1118 | 525 | self.assertEqual([], list(search_results)) | ||
1119 | 526 | |||
1120 | 527 | def test_exact_match_git_identity(self): | ||
1121 | 528 | # If you search for the Git identity of a repository, then you get a | ||
1122 | 529 | # single result with that repository. | ||
1123 | 530 | repository = self.factory.makeGitRepository() | ||
1124 | 531 | self.factory.makeGitRepository() | ||
1125 | 532 | search_results = self.collection.search(repository.git_identity) | ||
1126 | 533 | self.assertEqual([repository], list(search_results)) | ||
1127 | 534 | |||
1128 | 535 | def test_exact_match_git_identity_development_focus(self): | ||
1129 | 536 | # If you search for the development focus and it is set, you get a | ||
1130 | 537 | # single result with the development focus repository. | ||
1131 | 538 | fooix = self.factory.makeProduct(name='fooix') | ||
1132 | 539 | repository = self.factory.makeGitRepository( | ||
1133 | 540 | owner=fooix.owner, target=fooix) | ||
1134 | 541 | with person_logged_in(fooix.owner): | ||
1135 | 542 | getUtility(IGitRepositorySet).setDefaultRepository( | ||
1136 | 543 | fooix, repository) | ||
1137 | 544 | self.factory.makeGitRepository() | ||
1138 | 545 | search_results = self.collection.search('lp:fooix') | ||
1139 | 546 | self.assertEqual([repository], list(search_results)) | ||
1140 | 547 | |||
1141 | 548 | def test_bad_match_git_identity_development_focus(self): | ||
1142 | 549 | # If you search for the development focus for a project where one | ||
1143 | 550 | # isn't set, you get an empty search result. | ||
1144 | 551 | fooix = self.factory.makeProduct(name='fooix') | ||
1145 | 552 | self.factory.makeGitRepository(target=fooix) | ||
1146 | 553 | self.factory.makeGitRepository() | ||
1147 | 554 | search_results = self.collection.search('lp:fooix') | ||
1148 | 555 | self.assertEqual([], list(search_results)) | ||
1149 | 556 | |||
1150 | 557 | def test_bad_match_git_identity_no_project(self): | ||
1151 | 558 | # If you search for the development focus for a project where one | ||
1152 | 559 | # isn't set, you get an empty search result. | ||
1153 | 560 | self.factory.makeGitRepository() | ||
1154 | 561 | search_results = self.collection.search('lp:fooix') | ||
1155 | 562 | self.assertEqual([], list(search_results)) | ||
1156 | 563 | |||
1157 | 564 | def test_exact_match_url_trailing_slash(self): | ||
1158 | 565 | # Sometimes, users are inconsiderately unaware of our arbitrary | ||
1159 | 566 | # database restrictions and will put trailing slashes on their | ||
1160 | 567 | # search queries. Rather bravely, we refuse to explode in this | ||
1161 | 568 | # case. | ||
1162 | 569 | repository = self.factory.makeGitRepository() | ||
1163 | 570 | self.factory.makeGitRepository() | ||
1164 | 571 | search_results = self.collection.search( | ||
1165 | 572 | repository.getCodebrowseUrl() + '/') | ||
1166 | 573 | self.assertEqual([repository], list(search_results)) | ||
1167 | 574 | |||
1168 | 575 | def test_match_exact_repository_name(self): | ||
1169 | 576 | # search returns all repositories with the same name as the search | ||
1170 | 577 | # term. | ||
1171 | 578 | repository1 = self.factory.makeGitRepository(name=u'foo') | ||
1172 | 579 | repository2 = self.factory.makeGitRepository(name=u'foo') | ||
1173 | 580 | self.factory.makeGitRepository() | ||
1174 | 581 | search_results = self.collection.search('foo') | ||
1175 | 582 | self.assertEqual( | ||
1176 | 583 | sorted([repository1, repository2]), sorted(search_results)) | ||
1177 | 584 | |||
1178 | 585 | def disabled_test_match_against_unique_name(self): | ||
1179 | 586 | # XXX cjwatson 2015-02-06: Disabled until the URL format settles | ||
1180 | 587 | # down. | ||
1181 | 588 | repository = self.factory.makeGitRepository(name=u'fooa') | ||
1182 | 589 | search_term = repository.target.name + '/foo' | ||
1183 | 590 | search_results = self.collection.search(search_term) | ||
1184 | 591 | self.assertEqual([repository], list(search_results)) | ||
1185 | 592 | |||
1186 | 593 | def test_match_sub_repository_name(self): | ||
1187 | 594 | # search returns all repositories which have a name of which the | ||
1188 | 595 | # search term is a substring. | ||
1189 | 596 | repository1 = self.factory.makeGitRepository(name=u'afoo') | ||
1190 | 597 | repository2 = self.factory.makeGitRepository(name=u'foob') | ||
1191 | 598 | self.factory.makeGitRepository() | ||
1192 | 599 | search_results = self.collection.search('foo') | ||
1193 | 600 | self.assertEqual( | ||
1194 | 601 | sorted([repository1, repository2]), sorted(search_results)) | ||
1195 | 602 | |||
1196 | 603 | def test_match_ignores_case(self): | ||
1197 | 604 | repository = self.factory.makeGitRepository(name=u'foobar') | ||
1198 | 605 | search_results = self.collection.search('FOOBAR') | ||
1199 | 606 | self.assertEqual([repository], list(search_results)) | ||
1200 | 607 | |||
1201 | 608 | def test_dont_match_project_if_in_project(self): | ||
1202 | 609 | # If the container is restricted to the project, then we don't match | ||
1203 | 610 | # the project name. | ||
1204 | 611 | project = self.factory.makeProduct('foo') | ||
1205 | 612 | repository1 = self.factory.makeGitRepository( | ||
1206 | 613 | target=project, name=u'foo') | ||
1207 | 614 | self.factory.makeGitRepository(target=project, name=u'bar') | ||
1208 | 615 | search_results = self.collection.inProject(project).search('foo') | ||
1209 | 616 | self.assertEqual([repository1], list(search_results)) | ||
1210 | 617 | |||
1211 | 618 | |||
1212 | 619 | class TestGetTeamsWithRepositories(TestCaseWithFactory): | ||
1213 | 620 | """Test the IGitCollection.getTeamsWithRepositories method.""" | ||
1214 | 621 | |||
1215 | 622 | layer = DatabaseFunctionalLayer | ||
1216 | 623 | |||
1217 | 624 | def setUp(self): | ||
1218 | 625 | TestCaseWithFactory.setUp(self) | ||
1219 | 626 | self.all_repositories = getUtility(IAllGitRepositories) | ||
1220 | 627 | |||
1221 | 628 | def test_no_teams(self): | ||
1222 | 629 | # If the user is not a member of any teams, there are no results, | ||
1223 | 630 | # even if the person owns a repository themselves. | ||
1224 | 631 | person = self.factory.makePerson() | ||
1225 | 632 | self.factory.makeGitRepository(owner=person) | ||
1226 | 633 | teams = list(self.all_repositories.getTeamsWithRepositories(person)) | ||
1227 | 634 | self.assertEqual([], teams) | ||
1228 | 635 | |||
1229 | 636 | def test_team_repositories(self): | ||
1230 | 637 | # Return the teams that the user is in and that have repositories. | ||
1231 | 638 | person = self.factory.makePerson() | ||
1232 | 639 | team = self.factory.makeTeam(owner=person) | ||
1233 | 640 | self.factory.makeGitRepository(owner=team) | ||
1234 | 641 | # Make another team that person is in that has no repositories. | ||
1235 | 642 | self.factory.makeTeam(owner=person) | ||
1236 | 643 | teams = list(self.all_repositories.getTeamsWithRepositories(person)) | ||
1237 | 644 | self.assertEqual([team], teams) | ||
1238 | 645 | |||
1239 | 646 | def test_respects_restrictions(self): | ||
1240 | 647 | # Create a team with repositories on a project, and another | ||
1241 | 648 | # repository in a different namespace owned by a different team that | ||
1242 | 649 | # the person is a member of. Restricting the collection will return | ||
1243 | 650 | # just the teams that have repositories in that restricted | ||
1244 | 651 | # collection. | ||
1245 | 652 | person = self.factory.makePerson() | ||
1246 | 653 | team1 = self.factory.makeTeam(owner=person) | ||
1247 | 654 | repository = self.factory.makeGitRepository(owner=team1) | ||
1248 | 655 | # Make another team that person is in that owns a repository in a | ||
1249 | 656 | # different namespace to the namespace of the repository owned by team1. | ||
1250 | 657 | team2 = self.factory.makeTeam(owner=person) | ||
1251 | 658 | self.factory.makeGitRepository(owner=team2) | ||
1252 | 659 | collection = self.all_repositories.inProject(repository.target) | ||
1253 | 660 | teams = list(collection.getTeamsWithRepositories(person)) | ||
1254 | 661 | self.assertEqual([team1], teams) | ||
1255 | 662 | |||
1256 | 663 | |||
1257 | 664 | class TestGitCollectionOwnerCounts(TestCaseWithFactory): | ||
1258 | 665 | """Test IGitCollection.ownerCounts.""" | ||
1259 | 666 | |||
1260 | 667 | layer = DatabaseFunctionalLayer | ||
1261 | 668 | |||
1262 | 669 | def setUp(self): | ||
1263 | 670 | TestCaseWithFactory.setUp(self) | ||
1264 | 671 | self.all_repositories = getUtility(IAllGitRepositories) | ||
1265 | 672 | |||
1266 | 673 | def test_no_repositories(self): | ||
1267 | 674 | # If there are no repositories, we should get zero counts for both. | ||
1268 | 675 | person_count, team_count = self.all_repositories.ownerCounts() | ||
1269 | 676 | self.assertEqual(0, person_count) | ||
1270 | 677 | self.assertEqual(0, team_count) | ||
1271 | 678 | |||
1272 | 679 | def test_individual_repository_owners(self): | ||
1273 | 680 | # Repositories owned by an individual are returned as the first part | ||
1274 | 681 | # of the tuple. | ||
1275 | 682 | self.factory.makeGitRepository() | ||
1276 | 683 | self.factory.makeGitRepository() | ||
1277 | 684 | person_count, team_count = self.all_repositories.ownerCounts() | ||
1278 | 685 | self.assertEqual(2, person_count) | ||
1279 | 686 | self.assertEqual(0, team_count) | ||
1280 | 687 | |||
1281 | 688 | def test_team_repository_owners(self): | ||
1282 | 689 | # Repositories owned by teams are returned as the second part of the | ||
1283 | 690 | # tuple. | ||
1284 | 691 | self.factory.makeGitRepository(owner=self.factory.makeTeam()) | ||
1285 | 692 | self.factory.makeGitRepository(owner=self.factory.makeTeam()) | ||
1286 | 693 | person_count, team_count = self.all_repositories.ownerCounts() | ||
1287 | 694 | self.assertEqual(0, person_count) | ||
1288 | 695 | self.assertEqual(2, team_count) | ||
1289 | 696 | |||
1290 | 697 | def test_multiple_repositories_owned_counted_once(self): | ||
1291 | 698 | # Confirming that a person that owns multiple repositories only gets | ||
1292 | 699 | # counted once. | ||
1293 | 700 | individual = self.factory.makePerson() | ||
1294 | 701 | team = self.factory.makeTeam() | ||
1295 | 702 | for owner in [individual, individual, team, team]: | ||
1296 | 703 | self.factory.makeGitRepository(owner=owner) | ||
1297 | 704 | person_count, team_count = self.all_repositories.ownerCounts() | ||
1298 | 705 | self.assertEqual(1, person_count) | ||
1299 | 706 | self.assertEqual(1, team_count) | ||
1300 | 707 | |||
1301 | 708 | def test_counts_limited_by_collection(self): | ||
1302 | 709 | # For collections that are constrained in some way, we only get | ||
1303 | 710 | # counts for the constrained collection. | ||
1304 | 711 | r1 = self.factory.makeGitRepository() | ||
1305 | 712 | project = r1.target | ||
1306 | 713 | self.factory.makeGitRepository() | ||
1307 | 714 | collection = self.all_repositories.inProject(project) | ||
1308 | 715 | person_count, team_count = collection.ownerCounts() | ||
1309 | 716 | self.assertEqual(1, person_count) |