Merge lp:~salgado/offspring/private-projects into lp:offspring
- private-projects
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 90 |
Proposed branch: | lp:~salgado/offspring/private-projects |
Merge into: | lp:offspring |
Diff against target: |
719 lines (+490/-18) 11 files modified
Makefile (+4/-1) lib/offspring/web/queuemanager/models.py (+137/-14) lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql (+5/-0) lib/offspring/web/queuemanager/tests/__init__.py (+1/-0) lib/offspring/web/queuemanager/tests/factory.py (+23/-2) lib/offspring/web/queuemanager/tests/test_models.py (+306/-0) lib/offspring/web/queuemanager/views.py (+1/-0) lib/offspring/web/settings.py (+1/-0) lib/offspring/web/templates/queuemanager/projectgroup_details.html (+1/-1) migration/001-project-privacy.sql (+10/-0) requirements/requirements.web.txt (+1/-0) |
To merge this branch: | bzr merge lp:~salgado/offspring/private-projects |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kevin McDermott | Approve | ||
Offspring Committers | Pending | ||
Review via email:
|
This proposal supersedes a proposal from 2011-11-18.
Commit message
Description of the change
Add support for private projects on models of web.queuemanager
It adds a ._is_private attribute to Project, together with a few helper methods to decide who's able to see a given private project and to query only Projects a given user is allowed to see. This also works for classes that have a foreign key (direct or indirect) to Project, as we defer the checks to the class that provides the ._is_private attribute.
The default model manager (.objects) now returns only public objects, to reduce the risk of leaking private data. Subsequent branches will update all the callsites to use the privacy-aware API (.all_objects.
In order to try this you'll need to get http://
- 48. By Guilherme Salgado
-
merge trunk
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kevin McDermott (bigkevmcd) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
On Wed, 2011-11-30 at 14:57 +0000, Kevin McDermott wrote:
> Review: Approve
>
> #1
>
> +BUILDRESULTS_
>
> Is this the right path?
This is part of path-independence; I've already removed it from this
branch but it looks like you merged an old revision when you reviewed.
>
> #2
>
> Where is the migration used?
It's not; Offspring doesn't use south (Cody had concerns with it) so the
migration scripts have to be applied manually.
>
> #3
>
> + def test_get_
> No docstring.
Done
> #4
>
> +class ReleaseTests(
> + TestCase,
> ReleaseOrLexbui
> + model = Release
> +
> + def factoryMethod(self, project):
> + return factory.
> + build=factory.
> +
> +
> +class LexbuilderTests(
> + TestCase,
> ReleaseOrLexbui
> + model = Lexbuilder
> +
> + def factoryMethod(self, project):
> + return factory.
> + current_
> +
> +
> +class BuildResultTests(
> + TestCase,
> ReleaseOrLexbui
> + model = BuildResult
> + factoryMethod = factory.
> +
> +
> +class BuildRequestTests(
> + TestCase,
> ReleaseOrLexbui
> + model = BuildRequest
> + factoryMethod = factory.
>
> Couldn't this be done as a parent class, something like
> BasePrivateMode
I'm not sure I understand. What would we put in the parent class? Both
the model and the factory method are different on every class and they
already share a parent class which has the actual tests.
>
> #5
>
> +class ProjectTests(
>
> There are no docstrings explaining any of the tests...
I'm not convinced using docstrings in test methods is a good idea as
nobody is going to be using these tests methods (i.e. they're not an
API), but when I have a test which is not trivial I write some comments
around the interesting lines. I do that for a couple tests here, but I
don't think the others need any comments as they'd just be the test's
name with s/_/ /
> #6
>
> +from .test_models import *
>
> Any idea why we actually need this? (I know we do, but is there a way
> we can drop it?) My concern is that missing tests from there won't
> necessarily affect localised test runs, but could be missed from just
> running test-web.
Django requires it.
> Also...see PEP8
>
> - Relative imports for intra-package imports are highly
> discouraged.
> Always use the absolute package path for all imports.
> Even now that PEP 328 [7] is fully implemented in Python 2.5,
> its style of explicit relative imports is actively discouraged;
> absolute imports are more portable and usually more readable.
Ok, changed.
>
> #7
>
> === added directory 'lib/offspring/
> === added file
> 'lib/offspring/
>
> What are these used for? I...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kevin McDermott (bigkevmcd) wrote : | # |
> Where is the migration used?
>
> It's not; Offspring doesn't use south (Cody had concerns with it) so the
> migration scripts have to be applied manually.
Ok, I think we should definitely use South for migrations, it's in use successfully elsewhere in Canonical. Unless there are good reasons for not using it, then we can migrate that way over time.
At that point, we can fix this migration :-)
> >
> > Couldn't this be done as a parent class, something like
> > BasePrivateMode
>
> I'm not sure I understand. What would we put in the parent class? Both
> the model and the factory method are different on every class and they
> already share a parent class which has the actual tests.
After having read more on Nose's test finding (which I don't particularly like), it seems that this would be doomed to failure because it finds all TestCase subclasses, so it wouldn't work :-(
>
> I'm not convinced using docstrings in test methods is a good idea as
> nobody is going to be using these tests methods (i.e. they're not an
> API), but when I have a test which is not trivial I write some comments
> around the interesting lines. I do that for a couple tests here, but I
> don't think the others need any comments as they'd just be the test's
> name with s/_/ /
Instead of comments, these should be docstrings...
But, the problem with tests without docstrings is that they don't convey anything of what they're actually doing.
def test_myfunction
self.
Doesn't mean anything, but if the test passes, it's fine?
> Django requires it.
Why does Django require it? We removed it from Hexr recently and the tests still run :-)
> >
> > #7
> >
> > === added directory 'lib/offspring/
> > === added file
> > 'lib/offspring/
> >
> > What are these used for? Is there a speed benefit from using sqlite
> > in tests?
>
> Yes, it's incredibly faster to run using sqlite3, but there's also a
> test-web-postgres target to run the test-suite using postgres.
>
Sure, I see that, what a difference sqlite3 makes.
Ok, so is there some way we can deal with this in either South or in Django itself, so that it's not just in an SQL file?
This doesn't have to be fixed right away, but having bits of SQL lying around that may or may not be run isn't good...
> That's tricky because the Managers have to be declared on a models.Model
> instance and if I make MaybePrivateMixin a subclass of that, Project
> will end up inheriting from it twice: via MaybePrivateMixin and
> AccessGroupMixin, and that seems to case some weird issues. I'm sure I
> could create two separate mixins to workaround this but then I'm not
> sure it's worth it anymore.
Sure, I just hate to see duplication of code...maybe we can come up with something in a future refactoring...
> That was done in another branch, so I'd rather not do it here as it's
> not related to those changes I'm doing and would cause yet another
> conflict later on when I merge this back into the linaro branch.
Ok...seems reasonable...we do need to make sure we don't get caught by the Django/psyco...
- 49. By Guilherme Salgado
-
Lots of docstrings for tests
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kevin McDermott (bigkevmcd) wrote : | # |
Thanks for adding the docstrings...
pep8 throws a fit and lint chucks out at least one likely error :-)
But they're not in your branch, so let's not introduce any more noise :-)
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2011-09-21 22:44:51 +0000 |
3 | +++ Makefile 2011-12-01 15:15:28 +0000 |
4 | @@ -46,7 +46,10 @@ |
5 | ./.virtualenv/bin/nosetests offspring.master |
6 | |
7 | test-web: web install-test-runner |
8 | - ./bin/offspring-web test --settings=offspring.web.settings_test |
9 | + ./bin/offspring-web test queuemanager --settings=offspring.web.settings_test |
10 | + |
11 | +test-web-postgres: web install-test-runner |
12 | + ./bin/offspring-web test queuemanager --settings=offspring.web.settings_devel |
13 | |
14 | test: master slave web install-test-runner test-web |
15 | ./.virtualenv/bin/nosetests -e 'queuemanager.*' |
16 | |
17 | === modified file 'lib/offspring/web/queuemanager/models.py' |
18 | --- lib/offspring/web/queuemanager/models.py 2011-09-27 18:42:21 +0000 |
19 | +++ lib/offspring/web/queuemanager/models.py 2011-12-01 15:15:28 +0000 |
20 | @@ -13,12 +13,16 @@ |
21 | import math |
22 | |
23 | from django.conf import settings |
24 | -from django.contrib.auth.models import User |
25 | +from django.contrib.auth.models import AnonymousUser, User |
26 | from django.db import ( |
27 | connection, |
28 | models |
29 | ) |
30 | |
31 | +from django_group_access.models import ( |
32 | + AccessGroup, |
33 | + AccessGroupMixin, |
34 | + AccessManager) |
35 | from offspring.enums import ProjectBuildStates |
36 | |
37 | |
38 | @@ -73,9 +77,9 @@ |
39 | def get_absolute_url(self): |
40 | return "/project-groups/%s/" % self.name |
41 | |
42 | - @property |
43 | - def projects(self): |
44 | - return Project.objects.filter(project_group=self) |
45 | + def get_projects(self, user): |
46 | + return Project.all_objects.accessible_by_user(user).filter( |
47 | + project_group=self) |
48 | |
49 | @property |
50 | def builder(self): |
51 | @@ -83,11 +87,10 @@ |
52 | |
53 | @property |
54 | def is_active(self): |
55 | - return bool(Project.objects.filter(project_group=self).exclude(is_active=False).count() != 0) |
56 | - |
57 | - @property |
58 | - def needs_build(self): |
59 | - return BuildRequest.objects.filter(project__project_group=self).exists() |
60 | + # Can safely use .all_objects here because we're not returning any |
61 | + # objects so there's no risk of leaking private stuff. |
62 | + projects = Project.all_objects.filter(project_group=self) |
63 | + return projects.exclude(is_active=False).count() > 0 |
64 | |
65 | @property |
66 | def average_build_time(self): |
67 | @@ -111,7 +114,81 @@ |
68 | return "UNKNOWN" |
69 | |
70 | |
71 | -class Project(models.Model): |
72 | +class PublicOnlyObjectsManager(models.Manager): |
73 | + """A model manager which only returns public objects.""" |
74 | + |
75 | + def get_query_set(self): |
76 | + return super(PublicOnlyObjectsManager, self).get_query_set().filter( |
77 | + models.Q(**{self.model._get_privacy_attr(): False})) |
78 | + |
79 | + |
80 | +class AccessControlledObjectsManager(AccessManager): |
81 | + """A model manager which provides an extra API for querying objects a |
82 | + given user is allowed to see. |
83 | + |
84 | + We extend AccessManager because we want the access-control filters to be |
85 | + applied only for private objects, but also because we allow AnonymousUsers |
86 | + to be passed to accessible_by_user(). |
87 | + """ |
88 | + |
89 | + def _get_accessible_by_user_filter_rules(self, user): |
90 | + rules = super(AccessControlledObjectsManager, |
91 | + self)._get_accessible_by_user_filter_rules(user) |
92 | + return rules | models.Q(**{self.model._get_privacy_attr(): False}) |
93 | + |
94 | + def accessible_by_user(self, user): |
95 | + if isinstance(user, AnonymousUser): |
96 | + # If we're passed in an AnonymousUser we must not attempt to apply |
97 | + # the usual constraints because they rely on the given user |
98 | + # existing in the DB and that's not the case with AnonymousUsers. |
99 | + query = models.Q(**{self.model._get_privacy_attr(): False}) |
100 | + return super( |
101 | + AccessControlledObjectsManager, self).get_query_set().filter( |
102 | + query) |
103 | + else: |
104 | + return super( |
105 | + AccessControlledObjectsManager, self).accessible_by_user(user) |
106 | + |
107 | + |
108 | +class MaybePrivateMixin(object): |
109 | + """A model which may be private or linked to a private one. |
110 | + |
111 | + If the model itself is private, it's expected to have an '_is_private' |
112 | + boolean attribute. Otherwise it must have an 'access_relation' attribute |
113 | + pointing to the linked model which has the '_is_private' attribute. |
114 | + """ |
115 | + _privacy_attr_name = '_is_private' |
116 | + _is_visible_method_name = '_is_visible_to' |
117 | + |
118 | + @classmethod |
119 | + def _get_privacy_attr(self): |
120 | + privacy_attr = self._privacy_attr_name |
121 | + if getattr(self, 'access_relation', None) is not None: |
122 | + privacy_attr = self.access_relation + '__' + privacy_attr |
123 | + return privacy_attr |
124 | + |
125 | + def _get_access_controlled_object(self): |
126 | + if getattr(self, 'access_relation', None) is None: |
127 | + return self |
128 | + names = self.access_relation.split('__') |
129 | + obj = self |
130 | + while len(names): |
131 | + name = names.pop(0) |
132 | + obj = getattr(obj, name) |
133 | + return obj |
134 | + |
135 | + @property |
136 | + def is_private(self): |
137 | + obj = self._get_access_controlled_object() |
138 | + return getattr(obj, self._privacy_attr_name) |
139 | + |
140 | + def is_visible_to(self, user): |
141 | + is_visible_to = getattr(self._get_access_controlled_object(), |
142 | + self._is_visible_method_name) |
143 | + return is_visible_to(user) |
144 | + |
145 | + |
146 | +class Project(AccessGroupMixin, MaybePrivateMixin): |
147 | #XXX: This should probably be managed in the database. |
148 | STATUS_CHOICES = ( |
149 | (u'devel', u'Active Development'), |
150 | @@ -121,7 +198,15 @@ |
151 | (u'experimental', u'Experimental'), |
152 | (u'obsolete', u'Obsolete'), |
153 | ) |
154 | + all_objects = AccessControlledObjectsManager() |
155 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
156 | + # updating existing uses of Project.objects won't leak private projects. |
157 | + objects = PublicOnlyObjectsManager() |
158 | |
159 | + _is_private = models.BooleanField(default=False, db_column='is_private') |
160 | + # We allow projects without an owner for backwards compatibility but |
161 | + # there's a DB constraint to ensure private projects always have an owner. |
162 | + owner = models.ForeignKey(User, blank=True, null=True) |
163 | name = models.SlugField('codename', max_length=200, primary_key=True, unique=True) |
164 | title = models.CharField('project title', max_length=30) |
165 | project_group = models.ForeignKey(ProjectGroup, blank=True, null=True) |
166 | @@ -144,6 +229,16 @@ |
167 | def __unicode__(self): |
168 | return self.display_name() |
169 | |
170 | + def _is_visible_to(self, user): |
171 | + if not self._is_private or user == self.owner: |
172 | + return True |
173 | + if isinstance(user, AnonymousUser): |
174 | + # Private objects are never visible to anonymous users. |
175 | + return False |
176 | + access_groups = set(self.access_groups.all()) |
177 | + user_groups = AccessGroup.objects.filter(members=user) |
178 | + return len(access_groups.intersection(user_groups)) > 0 |
179 | + |
180 | def get_absolute_url(self): |
181 | return "/projects/%s/" % self.name |
182 | |
183 | @@ -191,7 +286,7 @@ |
184 | return u"" |
185 | |
186 | |
187 | -class Lexbuilder(models.Model): |
188 | +class Lexbuilder(models.Model, MaybePrivateMixin): |
189 | name = models.SlugField(max_length=200, unique=True) |
190 | uri = models.CharField(max_length=200) |
191 | current_state = models.CharField(max_length=200, default="UNKNOWN", editable=False) |
192 | @@ -204,6 +299,13 @@ |
193 | current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True) |
194 | notes = models.TextField('whiteboard', blank=True, null=True) |
195 | |
196 | + access_relation = 'current_job__project' |
197 | + all_objects = AccessControlledObjectsManager() |
198 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
199 | + # updating existing uses of Lexbuilder.objects won't leak anything related |
200 | + # to private projects. |
201 | + objects = PublicOnlyObjectsManager() |
202 | + |
203 | class Meta: |
204 | db_table = "lexbuilders" |
205 | |
206 | @@ -265,7 +367,7 @@ |
207 | return cursor.fetchone()[0] |
208 | |
209 | |
210 | -class BuildResult(models.Model): |
211 | +class BuildResult(models.Model, MaybePrivateMixin): |
212 | name = models.CharField('build name', max_length=200, blank=True, null=True, editable=False) |
213 | project = models.ForeignKey(Project, db_column="project_name", editable=False) |
214 | builder = models.ForeignKey(Lexbuilder, blank=True, null=True, editable=False) |
215 | @@ -278,6 +380,13 @@ |
216 | dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True) |
217 | notes = models.CharField(max_length=200, null=True, blank=True) |
218 | |
219 | + access_relation = 'project' |
220 | + all_objects = AccessControlledObjectsManager() |
221 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
222 | + # updating existing uses of BuildResult.objects won't leak anything |
223 | + # related to private projects. |
224 | + objects = PublicOnlyObjectsManager() |
225 | + |
226 | class Meta: |
227 | db_table = "buildresults" |
228 | get_latest_by = "started_at" |
229 | @@ -325,13 +434,20 @@ |
230 | recentBuildFailures = staticmethod(recentBuildFailures) |
231 | |
232 | |
233 | -class BuildRequest(models.Model): |
234 | +class BuildRequest(models.Model, MaybePrivateMixin): |
235 | project = models.ForeignKey(Project, db_column="project_name") |
236 | requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True) |
237 | created_at = models.DateTimeField('date created', auto_now_add=True) |
238 | score = models.IntegerField(default=10) |
239 | reason = models.TextField('request reason', blank=True, max_length=200) |
240 | |
241 | + access_relation = 'project' |
242 | + all_objects = AccessControlledObjectsManager() |
243 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
244 | + # updating existing uses of BuildRequest.objects won't leak anything |
245 | + # related to private projects. |
246 | + objects = PublicOnlyObjectsManager() |
247 | + |
248 | class Meta: |
249 | db_table = "buildrequests" |
250 | |
251 | @@ -450,7 +566,7 @@ |
252 | return "%s subscribed to %s" % (self.user, self.project) |
253 | |
254 | |
255 | -class Release(models.Model): |
256 | +class Release(models.Model, MaybePrivateMixin): |
257 | STATUS_PENDING, STATUS_PUBLISHED, STATUS_REMOVED, STATUS_MISSING, STATUS_ERROR = range(5) |
258 | STATUS_CHOICES = ( |
259 | (STATUS_PENDING, 'Pending Publication'), |
260 | @@ -468,6 +584,13 @@ |
261 | ("GM", "Gold Master"), |
262 | ) |
263 | |
264 | + access_relation = 'build__project' |
265 | + all_objects = AccessControlledObjectsManager() |
266 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
267 | + # updating existing uses of Release.objects won't leak anything related to |
268 | + # private projects. |
269 | + objects = PublicOnlyObjectsManager() |
270 | + |
271 | build = models.OneToOneField(BuildResult, primary_key=True, unique=True) |
272 | name = models.CharField('release name', max_length=200) |
273 | milestone = models.ForeignKey(LaunchpadProjectMilestone, null=True, blank=True) |
274 | |
275 | === added directory 'lib/offspring/web/queuemanager/sql' |
276 | === added file 'lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql' |
277 | --- lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 1970-01-01 00:00:00 +0000 |
278 | +++ lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 2011-12-01 15:15:28 +0000 |
279 | @@ -0,0 +1,5 @@ |
280 | +-- XXX: This doesn't apply when running tests because sqlite3 doesn't support |
281 | +-- addition of new constraints to existing tables. Need to decide whether |
282 | +-- it's preferred to just ignore this in our tests or use postgresql to run |
283 | +-- the tests as well. |
284 | +ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL); |
285 | |
286 | === modified file 'lib/offspring/web/queuemanager/tests/__init__.py' |
287 | --- lib/offspring/web/queuemanager/tests/__init__.py 2011-11-30 14:01:37 +0000 |
288 | +++ lib/offspring/web/queuemanager/tests/__init__.py 2011-12-01 15:15:28 +0000 |
289 | @@ -0,0 +1,1 @@ |
290 | +from offspring.web.queuemanager.tests.test_models import * |
291 | |
292 | === modified file 'lib/offspring/web/queuemanager/tests/factory.py' |
293 | --- lib/offspring/web/queuemanager/tests/factory.py 2011-11-30 14:01:37 +0000 |
294 | +++ lib/offspring/web/queuemanager/tests/factory.py 2011-12-01 15:15:28 +0000 |
295 | @@ -1,6 +1,7 @@ |
296 | from itertools import count |
297 | |
298 | from django.contrib.auth.models import User |
299 | +from django_group_access.models import AccessGroup |
300 | |
301 | from offspring.web.queuemanager.models import ( |
302 | BuildRequest, |
303 | @@ -41,7 +42,8 @@ |
304 | return User.objects.create_user( |
305 | userid, self.get_unique_email_address(), password) |
306 | |
307 | - def make_project(self, name=None, title=None): |
308 | + def make_project(self, name=None, title=None, is_private=False, |
309 | + project_group=None, owner=None, access_groups=[]): |
310 | """ |
311 | Create a Project with the given name and title. If any of those is not |
312 | given we use an arbitrary value. |
313 | @@ -50,7 +52,15 @@ |
314 | name = self.get_unique_string() |
315 | if title is None: |
316 | title = self.get_unique_string() |
317 | - return Project.objects.create(name=name, title=title) |
318 | + if owner is None: |
319 | + owner = self.make_user() |
320 | + project = Project.objects.create( |
321 | + name=name, title=title, _is_private=is_private, owner=owner, |
322 | + project_group=project_group) |
323 | + for group in access_groups: |
324 | + project.access_groups.add(group) |
325 | + project.save() |
326 | + return project |
327 | |
328 | def make_project_group(self, name=None, title=None): |
329 | """ |
330 | @@ -108,5 +118,16 @@ |
331 | project = self.make_project() |
332 | return BuildRequest.objects.create(project=project) |
333 | |
334 | + def make_access_group(self, users): |
335 | + """ |
336 | + Create an AccessGroup containing the given users. |
337 | + """ |
338 | + name = self.get_unique_string() |
339 | + group = AccessGroup.objects.create(name=name) |
340 | + for user in users: |
341 | + group.members.add(user) |
342 | + group.save() |
343 | + return group |
344 | + |
345 | |
346 | factory = ObjectFactory() |
347 | |
348 | === added file 'lib/offspring/web/queuemanager/tests/test_models.py' |
349 | --- lib/offspring/web/queuemanager/tests/test_models.py 1970-01-01 00:00:00 +0000 |
350 | +++ lib/offspring/web/queuemanager/tests/test_models.py 2011-12-01 15:15:28 +0000 |
351 | @@ -0,0 +1,306 @@ |
352 | +from operator import attrgetter |
353 | + |
354 | +from django.contrib.auth.models import AnonymousUser |
355 | +from django.test import TestCase |
356 | + |
357 | +from offspring.web.queuemanager.models import ( |
358 | + BuildRequest, |
359 | + BuildResult, |
360 | + Lexbuilder, |
361 | + Project, |
362 | + Release) |
363 | +from offspring.web.queuemanager.tests.factory import factory |
364 | + |
365 | + |
366 | +class ProjectTests(TestCase): |
367 | + |
368 | + def test_default_manager(self): |
369 | + """ |
370 | + Make sure .all_objects is the default manager. |
371 | + |
372 | + If that's not the case the admin UI will exclude all private objects |
373 | + and we would break model validation of unique fields if the user |
374 | + reuses a value from an object they're not allowed to see. |
375 | + """ |
376 | + self.assertEquals(Project.all_objects, Project._default_manager) |
377 | + |
378 | + def test_is_private_for_public_project(self): |
379 | + """ |
380 | + Check that Project.is_private returns False for public projects. |
381 | + """ |
382 | + project = factory.make_project(is_private=False) |
383 | + self.assertEqual(False, project.is_private) |
384 | + |
385 | + def test_is_private_for_private_project(self): |
386 | + """ |
387 | + Check that Project.is_private returns True for private projects. |
388 | + """ |
389 | + private_project = factory.make_project(is_private=True) |
390 | + self.assertEqual(True, private_project.is_private) |
391 | + |
392 | + def test_is_visible_to_anyone_if_public(self): |
393 | + """ |
394 | + Check that public projects are visible to anyone. |
395 | + """ |
396 | + project = factory.make_project(is_private=False) |
397 | + self.assertTrue(project.is_visible_to(AnonymousUser())) |
398 | + |
399 | + def test_is_not_visible_to_anyone_if_private(self): |
400 | + """ |
401 | + Check that private projects are not visible to anonymous users. |
402 | + """ |
403 | + project = factory.make_project(is_private=True) |
404 | + self.assertFalse(project.is_visible_to(AnonymousUser())) |
405 | + |
406 | + def test_is_visible_to_owner_if_private(self): |
407 | + """ |
408 | + Check that private projects are visible to their owners. |
409 | + """ |
410 | + project = factory.make_project(is_private=True) |
411 | + self.assertTrue(project.is_visible_to(project.owner)) |
412 | + |
413 | + def test_is_visible_to_member_of_access_group_if_private(self): |
414 | + """ |
415 | + Check that private projects are visible to members of their access |
416 | + groups. |
417 | + """ |
418 | + user = factory.make_user() |
419 | + group = factory.make_access_group([user]) |
420 | + project = factory.make_project(is_private=True, access_groups=[group]) |
421 | + self.assertTrue(project.is_visible_to(user)) |
422 | + |
423 | + def test_dot_objects_model_manager_only_returns_public_projects(self): |
424 | + """ |
425 | + Check that the .objects model manager return only public projects. |
426 | + """ |
427 | + project = factory.make_project(is_private=False) |
428 | + project2 = factory.make_project(is_private=True) |
429 | + self.assertEqual([project], list(Project.objects.all())) |
430 | + |
431 | + def test_all_objects_model_manager(self): |
432 | + """ |
433 | + Check that Project.all_objects includes private objects. |
434 | + |
435 | + Project.all_objects() is a separate model manager which returns |
436 | + private objects as well. Its .all() method will return public and |
437 | + private objects regardless of whether or not the current user has |
438 | + access to them. It is to be used with caution. |
439 | + """ |
440 | + project = factory.make_project(is_private=False) |
441 | + project2 = factory.make_project(is_private=True) |
442 | + self.assertEqual([project, project2], list(Project.all_objects.all())) |
443 | + |
444 | + def test_accessible_by_user_with_anonymous_user(self): |
445 | + """ |
446 | + Check that accessible_by_user() can be given an anonymous user as |
447 | + argument. |
448 | + """ |
449 | + project = factory.make_project(is_private=True) |
450 | + project2 = factory.make_project(is_private=False) |
451 | + self.assertEqual(True, project.is_private) |
452 | + self.assertEqual(False, project2.is_private) |
453 | + objects = Project.all_objects.accessible_by_user(AnonymousUser()) |
454 | + self.assertEqual([project2], list(objects)) |
455 | + |
456 | + def test_accessible_by_user_with_public_project(self): |
457 | + """ |
458 | + Check that accessible_by_user() returns public projects regardless of |
459 | + the given user. |
460 | + """ |
461 | + user = factory.make_user() |
462 | + project = factory.make_project(is_private=False) |
463 | + self.assertEqual(False, project.is_private) |
464 | + objects = Project.all_objects.accessible_by_user(user) |
465 | + self.assertEqual([project], list(objects)) |
466 | + |
467 | + def test_accessible_by_user_with_private_project(self): |
468 | + """ |
469 | + Check that accessible_by_user() returns only the private projects the |
470 | + given user is allowed to see. |
471 | + """ |
472 | + user = factory.make_user() |
473 | + group = factory.make_access_group([user]) |
474 | + project = factory.make_project(is_private=True, access_groups=[group]) |
475 | + # This second project is private but has no access groups set up, so |
476 | + # the user cannot see it. |
477 | + project2 = factory.make_project(is_private=True) |
478 | + self.assertEqual(True, project.is_private) |
479 | + self.assertEqual(True, project2.is_private) |
480 | + objects = Project.all_objects.accessible_by_user(user) |
481 | + self.assertEqual([project], list(objects)) |
482 | + |
483 | + |
484 | +class ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin(object): |
485 | + """A mixin with tests that work for the following models: |
486 | + |
487 | + Release |
488 | + Lexbuilder |
489 | + BuildResult |
490 | + BuildRequest |
491 | + |
492 | + You just need to mix this with TestCase in your subclass and define |
493 | + factoryMethod and model. |
494 | + """ |
495 | + model = None |
496 | + |
497 | + def factoryMethod(self, project): |
498 | + raise NotImplementedError() |
499 | + |
500 | + def test_default_manager(self): |
501 | + """ |
502 | + Make sure .all_objects is the default manager. |
503 | + |
504 | + If that's not the case the admin UI will exclude all private objects |
505 | + and we would break model validation of unique fields if the user |
506 | + reuses a value from an object they're not allowed to see. |
507 | + """ |
508 | + self.assertEquals(self.model.all_objects, self.model._default_manager) |
509 | + |
510 | + def test_is_private_for_public_object(self): |
511 | + """ |
512 | + Check that .is_private returns False for public objects. |
513 | + """ |
514 | + public_object = self.factoryMethod( |
515 | + factory.make_project(is_private=False)) |
516 | + self.assertFalse(public_object.is_private) |
517 | + |
518 | + def test_is_private_for_private_object(self): |
519 | + """ |
520 | + Check that .is_private returns True for private objects. |
521 | + """ |
522 | + private_object = self.factoryMethod( |
523 | + factory.make_project(is_private=True)) |
524 | + self.assertTrue(private_object.is_private) |
525 | + |
526 | + def test_is_visible_to_anyone_if_public(self): |
527 | + """ |
528 | + Check that public objects are visible to anyone. |
529 | + """ |
530 | + public_object = self.factoryMethod( |
531 | + factory.make_project(is_private=False)) |
532 | + self.assertTrue(public_object.is_visible_to(AnonymousUser())) |
533 | + |
534 | + def test_is_not_visible_to_anyone_if_private(self): |
535 | + """ |
536 | + Check that private objects are not visible to anonymous users. |
537 | + """ |
538 | + private_object = self.factoryMethod( |
539 | + factory.make_project(is_private=True)) |
540 | + self.assertFalse(private_object.is_visible_to(AnonymousUser())) |
541 | + |
542 | + def test_is_visible_to_owner_if_private(self): |
543 | + """ |
544 | + Check that private objects are visible to their project's owners. |
545 | + """ |
546 | + # The object we're dealing with inherits the access-control rules from |
547 | + # the project it's related to, so the owner that matters is the |
548 | + # project owner. |
549 | + owner = factory.make_user() |
550 | + private_object = self.factoryMethod( |
551 | + factory.make_project(is_private=True, owner=owner)) |
552 | + self.assertTrue(private_object.is_visible_to(owner)) |
553 | + |
554 | + def test_is_visible_to_member_of_access_group_if_private(self): |
555 | + """ |
556 | + Check that private objects are visible to members of their project's |
557 | + access groups. |
558 | + """ |
559 | + user = factory.make_user() |
560 | + group = factory.make_access_group([user]) |
561 | + private_object = self.factoryMethod( |
562 | + factory.make_project(is_private=True, access_groups=[group])) |
563 | + self.assertTrue(private_object.is_visible_to(user)) |
564 | + |
565 | + def test_dot_objects_model_manager_only_returns_public_objects(self): |
566 | + """ |
567 | + Check that the .objects model manager return only public objects. |
568 | + """ |
569 | + private_object = self.factoryMethod( |
570 | + factory.make_project(is_private=True)) |
571 | + public_object = self.factoryMethod( |
572 | + factory.make_project(is_private=False)) |
573 | + self.assertEqual([public_object], list(self.model.objects.all())) |
574 | + |
575 | + def test_all_objects_model_manager(self): |
576 | + """ |
577 | + Check that .all_objects includes private objects. |
578 | + |
579 | + self.model.all_objects() is a separate model manager which returns |
580 | + private objects as well. Its .all() method will return public and |
581 | + private objects regardless of whether or not the current user has |
582 | + access to them. It is to be used with caution. |
583 | + """ |
584 | + public_object = self.factoryMethod( |
585 | + project=factory.make_project(is_private=False)) |
586 | + private_object = self.factoryMethod( |
587 | + project=factory.make_project(is_private=True)) |
588 | + self.assertEqual([public_object, private_object], |
589 | + list(self.model.all_objects.all())) |
590 | + |
591 | + def test_accessible_by_user(self): |
592 | + """ |
593 | + Check that accessible_by_user() returns only the private objects the |
594 | + user is allowed to see. |
595 | + """ |
596 | + user = factory.make_user() |
597 | + group = factory.make_access_group([user]) |
598 | + visible_object = self.factoryMethod( |
599 | + factory.make_project(is_private=True, access_groups=[group])) |
600 | + # This object is linked to a private project which has no access |
601 | + # groups set up, so the user cannot see it. |
602 | + invisible_object = self.factoryMethod( |
603 | + project=factory.make_project(is_private=True)) |
604 | + self.assertEqual( |
605 | + [visible_object], |
606 | + list(self.model.all_objects.accessible_by_user(user))) |
607 | + |
608 | + |
609 | +class ReleaseTests( |
610 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
611 | + model = Release |
612 | + |
613 | + def factoryMethod(self, project): |
614 | + return factory.make_release( |
615 | + build=factory.make_build_result(project=project)) |
616 | + |
617 | + |
618 | +class LexbuilderTests( |
619 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
620 | + model = Lexbuilder |
621 | + |
622 | + def factoryMethod(self, project): |
623 | + return factory.make_lexbuilder( |
624 | + current_job=factory.make_build_result(project=project)) |
625 | + |
626 | + |
627 | +class BuildResultTests( |
628 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
629 | + model = BuildResult |
630 | + factoryMethod = factory.make_build_result |
631 | + |
632 | + |
633 | +class BuildRequestTests( |
634 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
635 | + model = BuildRequest |
636 | + factoryMethod = factory.make_build_request |
637 | + |
638 | + |
639 | +class ProjectGroupTests(TestCase): |
640 | + |
641 | + def test_get_projects_filters_private_objects(self): |
642 | + """ |
643 | + ProjectGroup.get_projects() will return only the private projects the |
644 | + given user is allowed to see. |
645 | + """ |
646 | + user = factory.make_user() |
647 | + group = factory.make_project_group() |
648 | + public_project = factory.make_project( |
649 | + is_private=False, project_group=group) |
650 | + private_project_visible_to_user = factory.make_project( |
651 | + is_private=True, project_group=group, owner=user) |
652 | + private_project = factory.make_project( |
653 | + is_private=True, project_group=group) |
654 | + self.assertEqual( |
655 | + sorted([public_project, private_project_visible_to_user], |
656 | + key=attrgetter('name')), |
657 | + sorted(group.get_projects(user), key=attrgetter('name'))) |
658 | |
659 | === modified file 'lib/offspring/web/queuemanager/views.py' |
660 | --- lib/offspring/web/queuemanager/views.py 2011-08-13 06:28:28 +0000 |
661 | +++ lib/offspring/web/queuemanager/views.py 2011-12-01 15:15:28 +0000 |
662 | @@ -241,6 +241,7 @@ |
663 | |
664 | pageData = { |
665 | 'projectGroup' : pg, |
666 | + 'projects': pg.get_projects(request.user), |
667 | 'pillar' : 'projects', |
668 | 'build_stats' : build_stats, |
669 | 'projectgroup_build_results' : projectgroup_build_results, |
670 | |
671 | === modified file 'lib/offspring/web/settings.py' |
672 | --- lib/offspring/web/settings.py 2011-08-11 00:16:09 +0000 |
673 | +++ lib/offspring/web/settings.py 2011-12-01 15:15:28 +0000 |
674 | @@ -96,6 +96,7 @@ |
675 | 'djcelery', |
676 | 'djkombu', |
677 | 'piston', |
678 | + 'django_group_access', |
679 | # Offspring Apps |
680 | 'offspring.web.queuemanager', |
681 | ) |
682 | |
683 | === modified file 'lib/offspring/web/templates/queuemanager/projectgroup_details.html' |
684 | --- lib/offspring/web/templates/queuemanager/projectgroup_details.html 2010-11-29 08:27:24 +0000 |
685 | +++ lib/offspring/web/templates/queuemanager/projectgroup_details.html 2011-12-01 15:15:28 +0000 |
686 | @@ -32,7 +32,7 @@ |
687 | <h3>Project</h3></td><td></td><td><h3>Last Build</h3></td><td width="30%"><h3>Date</h3></td><td width="15%"><h3>Result</h3> |
688 | </td> |
689 | </tr> |
690 | - {% for project in projectGroup.projects %} |
691 | + {% for project in projects %} |
692 | <tr> |
693 | <td> |
694 | {% if project.needs_build %}<img src="/assets/images/33.png" title="Pending Build">{% else %}{% ifequal project.latest_build.result "FAILED" %}<img src="/assets/images/34.png" title="Build Failure">{% else %} {% ifequal project.latest_build.result "COMPLETED" %} <img src="/assets/images/35.png" title="Build Successful"> {% else %} <img src="/assets/images/36.png" title="Not yet built">{% endifequal %}{% endifequal %}{% endif %}</td><td> <a href="{% url offspring.web.queuemanager.views.project_details project.name %}">{{ project.title }}</a> |
695 | |
696 | === added directory 'migration' |
697 | === added file 'migration/001-project-privacy.sql' |
698 | --- migration/001-project-privacy.sql 1970-01-01 00:00:00 +0000 |
699 | +++ migration/001-project-privacy.sql 2011-12-01 15:15:28 +0000 |
700 | @@ -0,0 +1,10 @@ |
701 | +ALTER TABLE projects ADD COLUMN "is_private" boolean NOT NULL default FALSE; |
702 | +ALTER TABLE projects ADD COLUMN "owner_id" integer REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED; |
703 | +ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL); |
704 | +CREATE TABLE "projects_access_groups" ( |
705 | + "id" serial NOT NULL PRIMARY KEY, |
706 | + "project_id" varchar(200) NOT NULL, |
707 | + "accessgroup_id" integer NOT NULL REFERENCES "django_group_access_accessgroup" ("id") DEFERRABLE INITIALLY DEFERRED, |
708 | + UNIQUE ("project_id", "accessgroup_id") |
709 | +); |
710 | +ALTER TABLE "projects_access_groups" ADD CONSTRAINT "project_id_refs_name_44bd562d" FOREIGN KEY ("project_id") REFERENCES "projects" ("name") DEFERRABLE INITIALLY DEFERRED; |
711 | |
712 | === modified file 'requirements/requirements.web.txt' |
713 | --- requirements/requirements.web.txt 2011-08-11 00:16:09 +0000 |
714 | +++ requirements/requirements.web.txt 2011-12-01 15:15:28 +0000 |
715 | @@ -11,3 +11,4 @@ |
716 | celery>=2.3.1 |
717 | django-celery>=2.3.0 |
718 | django-kombu>=0.9.4 |
719 | +django-group-access>=0.0.1 |
#1
+BUILDRESULTS_ DIRECTORY = '/srv/offspring .com/www/ builds/ '
Is this the right path?
#2
Where is the migration used?
#3
+ def test_get_ projects_ filters_ private_ objects( self):
No docstring.
#4
+class ReleaseTests( lderOrBuildResu ltOrBuildReques tTestsMixin) : make_release( make_build_ result( project= project) ) lderOrBuildResu ltOrBuildReques tTestsMixin) : make_lexbuilder ( job=factory. make_build_ result( project= project) ) lderOrBuildResu ltOrBuildReques tTestsMixin) : make_build_ result lderOrBuildResu ltOrBuildReques tTestsMixin) : make_build_ request
+ TestCase, ReleaseOrLexbui
+ model = Release
+
+ def factoryMethod(self, project):
+ return factory.
+ build=factory.
+
+
+class LexbuilderTests(
+ TestCase, ReleaseOrLexbui
+ model = Lexbuilder
+
+ def factoryMethod(self, project):
+ return factory.
+ current_
+
+
+class BuildResultTests(
+ TestCase, ReleaseOrLexbui
+ model = BuildResult
+ factoryMethod = factory.
+
+
+class BuildRequestTests(
+ TestCase, ReleaseOrLexbui
+ model = BuildRequest
+ factoryMethod = factory.
Couldn't this be done as a parent class, something like BasePrivateMode lTestCase or something?
#5
+class ProjectTests( TestCase) :
There are no docstrings explaining any of the tests...
#6
+from .test_models import *
Any idea why we actually need this? (I know we do, but is there a way we can drop it?) My concern is that missing tests from there won't necessarily affect localised test runs, but could be missed from just running test-web.
Also...see PEP8
- Relative imports for intra-package imports are highly discouraged.
Always use the absolute package path for all imports.
Even now that PEP 328 [7] is fully implemented in Python 2.5,
its style of explicit relative imports is actively discouraged;
absolute imports are more portable and usually more readable.
#7
=== added directory 'lib/offspring/ web/queuemanage r/sql' web/queuemanage r/sql/project. postgresql_ psycopg2. sql'
=== added file 'lib/offspring/
What are these used for? Is there a speed benefit from using sqlite in tests?
Personally, I'd prefer to run the tests against the database we use in production i.e. postgresql and not have SQL files in separate directories for this purpose...
#8
+class Release( models. Model, MaybePrivateMixin): models. Model, MaybePrivateMixin): models. Model, MaybePrivateMixin):
+class BuildResult(
+class Lexbuilder(
+ # Using PublicOnlyObjec tsManager here we ensure that any oversights when
+ # updating existing uses of Release.objects won't leak anything related to
+ # private projects.
How about defining a base class for these MaybePrivateMixin objects rather than redeclaring he same thing and comments each time, and just subclass for the various models that use it?
#9
+ hostname = os.popen( 'hostname' ).read( ).strip( ) set('DEFAULT' , 'hostname', hostname) set('DEFAULT' , 'username', os.environ[ 'USERNAME' ])
+ config.
+ config.
I reimplemented this in add-path- independence, but note that you can use os.uname to get the hostname...
#10
+base_dir: /srv
-development_pool: %(base_ dir)s/builds/
-production_pool: %...