Merge lp:~salgado/offspring/private-projects into lp:~linaro-automation/offspring/linaro

Proposed by Guilherme Salgado
Status: Superseded
Proposed branch: lp:~salgado/offspring/private-projects
Merge into: lp:~linaro-automation/offspring/linaro
Diff against target: 489 lines (+368/-4)
9 files modified
Makefile (+4/-1)
lib/offspring/web/queuemanager/models.py (+77/-3)
lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql (+5/-0)
lib/offspring/web/queuemanager/tests/__init__.py (+2/-0)
lib/offspring/web/queuemanager/tests/factory.py (+108/-0)
lib/offspring/web/queuemanager/tests/test_models.py (+167/-0)
lib/offspring/web/settings.py (+1/-0)
migration/001-project-privacy.sql (+3/-0)
requirements/requirements.web.txt (+1/-0)
To merge this branch: bzr merge lp:~salgado/offspring/private-projects
Reviewer Review Type Date Requested Status
Linaro Infrastructure Pending
Review via email: mp+76592@code.launchpad.net

Description of the change

This is a work in progress

To post a comment you must log in.
30. By Guilherme Salgado

Fix one final place which was using offspring-builder-script instead of offspring-builder-config

31. By Guilherme Salgado

merge all-python-config

32. By Guilherme Salgado

merge all-python-config

33. By Guilherme Salgado

Update one remaining place which was still running offspring-builder-config

34. By Guilherme Salgado

merge trunk

35. By Guilherme Salgado

Some minor tweaks

36. By Guilherme Salgado

revert a few unnecessary changes

37. By Guilherme Salgado

remove a .bzrignore change that's no longer needed

38. By Guilherme Salgado

Add an object factory to create anonymous django model instances in tests

39. By Guilherme Salgado

Remove makeAccessGroup() from ObjectFactory as we don't want to depend on django-group-access yet. Also add a makeProjectGroup() method to ObjectFactory

40. By Guilherme Salgado

Rename the object factory methods to follow PEP-8 naming standard rather than camelCase

41. By Guilherme Salgado

Add support for private projects on models of web.queuemanager

42. By Guilherme Salgado

merge object-factory branch

43. By Guilherme Salgado

Fix a typo on a factory method call

44. By Guilherme Salgado

Rename makeAccessGroup to make_access_group

45. By Guilherme Salgado

merge trunk

46. By Guilherme Salgado

Ammend the 001-project-privacy.sql migration script to create the projects_access_groups table

47. By Guilherme Salgado

Revert the path-independence changes

48. By Guilherme Salgado

merge trunk

49. By Guilherme Salgado

Lots of docstrings for tests

Unmerged revisions

49. By Guilherme Salgado

Lots of docstrings for tests

48. By Guilherme Salgado

merge trunk

47. By Guilherme Salgado

Revert the path-independence changes

46. By Guilherme Salgado

Ammend the 001-project-privacy.sql migration script to create the projects_access_groups table

45. By Guilherme Salgado

merge trunk

44. By Guilherme Salgado

Rename makeAccessGroup to make_access_group

43. By Guilherme Salgado

Fix a typo on a factory method call

42. By Guilherme Salgado

merge object-factory branch

41. By Guilherme Salgado

Add support for private projects on models of web.queuemanager

40. By Guilherme Salgado

Rename the object factory methods to follow PEP-8 naming standard rather than camelCase

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2011-09-21 23:22:28 +0000
3+++ Makefile 2011-09-22 20:33:19 +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-03-24 21:30:51 +0000
19+++ lib/offspring/web/queuemanager/models.py 2011-09-22 20:33:19 +0000
20@@ -12,14 +12,16 @@
21 )
22 import math
23
24-from django.conf import settings
25-from django.contrib.auth.models import User
26+from django.contrib.auth.models import AnonymousUser, User
27 from django.db import (
28 connection,
29 models
30 )
31
32 from offspring import config
33+from django_group_access.models import (
34+ AccessGroupMixin,
35+ AccessManager)
36 from offspring.enums import ProjectBuildStates
37
38
39@@ -130,7 +132,44 @@
40 return u""
41
42
43-class Project(models.Model):
44+class PrivacyAwareManagerMixin(object):
45+
46+ @property
47+ def _privacy_attr(self):
48+ privacy_attr = 'is_private'
49+ if getattr(self.model, 'access_relation', None) is not None:
50+ privacy_attr = self.model.access_relation + '__' + privacy_attr
51+ return privacy_attr
52+
53+
54+class PublicObjectsManager(models.Manager, PrivacyAwareManagerMixin):
55+
56+ def get_query_set(self):
57+ return super(PublicObjectsManager, self).get_query_set().filter(
58+ models.Q(**{self._privacy_attr: False}))
59+
60+
61+class PrivateObjectsManager(AccessManager, PrivacyAwareManagerMixin):
62+
63+ def _get_accessible_by_user_filter_rules(self, user):
64+ rules = super(
65+ PrivateObjectsManager, self)._get_accessible_by_user_filter_rules(
66+ user)
67+ return rules | models.Q(**{self._privacy_attr: False})
68+
69+ def accessible_by_user(self, user):
70+ if isinstance(user, AnonymousUser):
71+ # If we're passed in an AnonymousUser we must not attempt to apply
72+ # the usual constraints because they rely on the given user
73+ # existing in the DB and that's not the case with AnonymousUsers.
74+ return super(PrivateObjectsManager, self).get_query_set().filter(
75+ models.Q(**{self._privacy_attr: False}))
76+ else:
77+ return super(PrivateObjectsManager, self).accessible_by_user(
78+ user)
79+
80+
81+class Project(AccessGroupMixin):
82 #XXX: This should probably be managed in the database.
83 STATUS_CHOICES = (
84 (u'devel', u'Active Development'),
85@@ -140,7 +179,15 @@
86 (u'experimental', u'Experimental'),
87 (u'obsolete', u'Obsolete'),
88 )
89+ all_objects = PrivateObjectsManager()
90+ # Using PublicObjectsManager here we ensure that any oversights when
91+ # updating existing uses of Project.objects won't leak private projects.
92+ objects = PublicObjectsManager()
93
94+ is_private = models.BooleanField(default=False)
95+ # We allow projects without an owner for backwards compatibility but
96+ # there's a DB constraint to ensure private projects always have an owner.
97+ owner = models.ForeignKey(User, blank=True, null=True)
98 name = models.SlugField('codename', max_length=200, primary_key=True, unique=True)
99 title = models.CharField('project title', max_length=30)
100 project_group = models.ForeignKey(ProjectGroup, blank=True, null=True)
101@@ -229,6 +276,13 @@
102 current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True)
103 notes = models.TextField('whiteboard', blank=True, null=True)
104
105+ # Using PublicObjectsManager here we ensure that any oversights when
106+ # updating existing uses of Lexbuilder.objects won't leak anything related
107+ # to private projects.
108+ access_relation = 'current_job__project'
109+ objects = PublicObjectsManager()
110+ all_objects = PrivateObjectsManager()
111+
112 class Meta:
113 db_table = "lexbuilders"
114
115@@ -303,6 +357,13 @@
116 dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
117 notes = models.CharField(max_length=200, null=True, blank=True)
118
119+ # Using PublicObjectsManager here we ensure that any oversights when
120+ # updating existing uses of BuildResult.objects won't leak anything
121+ # related to private projects.
122+ access_relation = 'project'
123+ objects = PublicObjectsManager()
124+ all_objects = PrivateObjectsManager()
125+
126 class Meta:
127 db_table = "buildresults"
128 get_latest_by = "started_at"
129@@ -357,6 +418,13 @@
130 score = models.IntegerField(default=10)
131 reason = models.TextField('request reason', blank=True, max_length=200)
132
133+ # Using PublicObjectsManager here we ensure that any oversights when
134+ # updating existing uses of BuildRequest.objects won't leak anything
135+ # related to private projects.
136+ access_relation = 'project'
137+ objects = PublicObjectsManager()
138+ all_objects = PrivateObjectsManager()
139+
140 class Meta:
141 db_table = "buildrequests"
142
143@@ -492,6 +560,12 @@
144 ("RC", "Release Candidate"),
145 ("GM", "Gold Master"),
146 )
147+ # Using PublicObjectsManager here we ensure that any oversights when
148+ # updating existing uses of Release.objects won't leak anything related to
149+ # private projects.
150+ access_relation = 'build__project'
151+ objects = PublicObjectsManager()
152+ all_objects = PrivateObjectsManager()
153
154 build = models.OneToOneField(BuildResult, primary_key=True, unique=True)
155 name = models.CharField('release name', max_length=200)
156
157=== added directory 'lib/offspring/web/queuemanager/sql'
158=== added file 'lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql'
159--- lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 1970-01-01 00:00:00 +0000
160+++ lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 2011-09-22 20:33:19 +0000
161@@ -0,0 +1,5 @@
162+-- XXX: This doesn't apply when running tests because sqlite3 doesn't support
163+-- addition of new constraints to existing tables. Need to decide whether
164+-- it's preferred to just ignore this in our tests or use postgresql to run
165+-- the tests as well.
166+ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL);
167
168=== added directory 'lib/offspring/web/queuemanager/tests'
169=== added file 'lib/offspring/web/queuemanager/tests/__init__.py'
170--- lib/offspring/web/queuemanager/tests/__init__.py 1970-01-01 00:00:00 +0000
171+++ lib/offspring/web/queuemanager/tests/__init__.py 2011-09-22 20:33:19 +0000
172@@ -0,0 +1,2 @@
173+from .test_models import *
174+
175
176=== added file 'lib/offspring/web/queuemanager/tests/factory.py'
177--- lib/offspring/web/queuemanager/tests/factory.py 1970-01-01 00:00:00 +0000
178+++ lib/offspring/web/queuemanager/tests/factory.py 2011-09-22 20:33:19 +0000
179@@ -0,0 +1,108 @@
180+from itertools import count
181+
182+from django.contrib.auth.models import User
183+from django_group_access.models import AccessGroup
184+
185+from offspring.web.queuemanager.models import (
186+ BuildRequest,
187+ BuildResult,
188+ Lexbuilder,
189+ Project,
190+ Release)
191+
192+
193+class ObjectFactory(object):
194+
195+ def __init__(self):
196+ self.counter = count()
197+
198+ def getUniqueEmailAddress(self):
199+ return "%s@example.com" % self.getUniqueString('email')
200+
201+ def getUniqueString(self, prefix=None):
202+ """Return a string unique to this factory instance.
203+
204+ :param prefix: Used as a prefix for the unique string. If unspecified,
205+ defaults to 'generic-string'.
206+ """
207+ if prefix is None:
208+ prefix = "generic-string"
209+ string = "%s%s" % (prefix, self.getUniqueInteger())
210+ return string.lower()
211+
212+ def getUniqueInteger(self):
213+ """Return an integer unique to this factory instance."""
214+ return self.counter.next()
215+
216+ def makeAccessGroup(self, users):
217+ name = self.getUniqueString()
218+ group = AccessGroup(name=name)
219+ group.save()
220+ for user in users:
221+ group.members.add(user)
222+ group.save()
223+ return group
224+
225+ def makeUser(self):
226+ userid = password = self.getUniqueString()
227+ user = User.objects.create_user(
228+ userid, self.getUniqueEmailAddress(), password)
229+ user.save()
230+ return user
231+
232+ def makeProject(self, name=None, title=None, is_private=False,
233+ owner=None, access_groups=[]):
234+ if name is None:
235+ name = self.getUniqueString()
236+ if title is None:
237+ title = self.getUniqueString()
238+ if owner is None:
239+ owner = self.makeUser()
240+ project = Project(
241+ name=name, title=title, is_private=is_private, owner=owner)
242+ project.save()
243+ for group in access_groups:
244+ project.access_groups.add(group)
245+ project.save()
246+ return project
247+
248+ def makeBuildResult(self, project=None):
249+ if project is None:
250+ project = self.makeProject()
251+ project.save()
252+ result = BuildResult(project=project)
253+ result.save()
254+ return result
255+
256+ def makeRelease(self, build=None, name=None, creator=None):
257+ if build is None:
258+ build = self.makeBuildResult()
259+ if name is None:
260+ name = self.getUniqueString()
261+ if creator is None:
262+ creator = self.makeUser()
263+ release = Release(build=build, name=name, creator=creator)
264+ release.save()
265+ return release
266+
267+ def makeLexbuilder(self, name=None, uri=None, current_job=None):
268+ if name is None:
269+ name = self.getUniqueString()
270+ if uri is None:
271+ uri = 'htt://example.com/%s' % name
272+ if current_job is None:
273+ current_job = self.makeBuildResult()
274+ builder = Lexbuilder(name=name, uri=uri, current_job=current_job)
275+ builder.save()
276+ return builder
277+
278+ def makeBuildRequest(self, project=None):
279+ if project is None:
280+ project = self.makeProject()
281+ project.save()
282+ request = BuildRequest(project=project)
283+ request.save()
284+ return request
285+
286+
287+factory = ObjectFactory()
288
289=== added file 'lib/offspring/web/queuemanager/tests/test_models.py'
290--- lib/offspring/web/queuemanager/tests/test_models.py 1970-01-01 00:00:00 +0000
291+++ lib/offspring/web/queuemanager/tests/test_models.py 2011-09-22 20:33:19 +0000
292@@ -0,0 +1,167 @@
293+from django.contrib.auth.models import AnonymousUser
294+from django.test import TestCase
295+
296+from offspring.web.queuemanager.models import (
297+ BuildRequest,
298+ BuildResult,
299+ Lexbuilder,
300+ Project,
301+ Release)
302+from offspring.web.queuemanager.tests.factory import factory
303+
304+
305+class ProjectTests(TestCase):
306+
307+ def test_default_model_manager_only_returns_public_projects(self):
308+ project = factory.makeProject(is_private=False)
309+ project2 = factory.makeProject(is_private=True)
310+ self.assertEqual([project], list(Project.objects.all()))
311+
312+ def test_all_objects_model_manager(self):
313+ # Project.all_objects() is a separate model manager which returns
314+ # private objects as well. Its .all() method will return public and
315+ # private objects regardless of whether or not the current user has
316+ # access to them. It is to be used with caution.
317+ project = factory.makeProject(is_private=False)
318+ project2 = factory.makeProject(is_private=True)
319+ self.assertEqual([project, project2], list(Project.all_objects.all()))
320+
321+ def test_accessible_by_user_with_anonymous_user(self):
322+ project = factory.makeProject(is_private=True)
323+ project2 = factory.makeProject(is_private=False)
324+ self.assertEqual(True, project.is_private)
325+ self.assertEqual(False, project2.is_private)
326+ objects = Project.all_objects.accessible_by_user(AnonymousUser())
327+ self.assertEqual([project2], list(objects))
328+
329+ def test_accessible_by_user_with_public_project(self):
330+ user = factory.makeUser()
331+ project = factory.makeProject(is_private=False)
332+ self.assertEqual(False, project.is_private)
333+ objects = Project.all_objects.accessible_by_user(user)
334+ self.assertEqual([project], list(objects))
335+
336+ def test_accessible_by_user_with_private_project(self):
337+ user = factory.makeUser()
338+ group = factory.makeAccessGroup([user])
339+ project = factory.makeProject(is_private=True, access_groups=[group])
340+ # This second project is private but has no access groups set up, so
341+ # the user cannot see it.
342+ project2 = factory.makeProject(is_private=True)
343+ self.assertEqual(True, project.is_private)
344+ self.assertEqual(True, project2.is_private)
345+ objects = Project.all_objects.accessible_by_user(user)
346+ self.assertEqual([project], list(objects))
347+
348+
349+class ReleaseOrLexbuilderTestsMixin(object):
350+ """A mixin with tests that work for either Release or Lexbuilder.
351+
352+ You just need to mix this with TestCase in your subclass and define
353+ factoryMethod and model.
354+ """
355+ model = None
356+
357+ def factoryMethod(self, build):
358+ raise NotImplementedError()
359+
360+ def test_default_model_manager_only_returns_public_objects(self):
361+ private_project = factory.makeProject(is_private=True)
362+ private_object = self.factoryMethod(
363+ build=factory.makeBuildResult(project=private_project))
364+ public_project = factory.makeProject(is_private=False)
365+ public_object = self.factoryMethod(
366+ build=factory.makeBuildResult(project=public_project))
367+ self.assertEqual([public_object], list(self.model.objects.all()))
368+
369+ def test_all_objects_model_manager(self):
370+ # self.model.all_objects() is a separate model manager which returns
371+ # private objects as well. Its .all() method will return public and
372+ # private objects regardless of whether or not the current user has
373+ # access to them. It is to be used with caution.
374+ public_object = self.factoryMethod(build=factory.makeBuildResult(
375+ project=factory.makeProject(is_private=False)))
376+ private_object = self.factoryMethod(build=factory.makeBuildResult(
377+ project=factory.makeProject(is_private=True)))
378+ self.assertEqual([public_object, private_object],
379+ list(self.model.all_objects.all()))
380+
381+ def test_accessible_by_user(self):
382+ user = factory.makeUser()
383+ group = factory.makeAccessGroup([user])
384+ project = factory.makeProject(is_private=True, access_groups=[group])
385+ visible_object = self.factoryMethod(
386+ build=factory.makeBuildResult(project=project))
387+ # This release/builder is from a private project which has no access
388+ # groups set up, so the user cannot see it.
389+ invisible_object = self.factoryMethod(build=factory.makeBuildResult(
390+ project=factory.makeProject(is_private=True)))
391+ self.assertEqual(
392+ [visible_object],
393+ list(self.model.all_objects.accessible_by_user(user)))
394+
395+
396+class ReleaseTests(TestCase, ReleaseOrLexbuilderTestsMixin):
397+ model = Release
398+
399+ def factoryMethod(self, build):
400+ return factory.makeRelease(build=build)
401+
402+
403+class LexbuilderTests(TestCase, ReleaseOrLexbuilderTestsMixin):
404+ model = Lexbuilder
405+
406+ def factoryMethod(self, build):
407+ return factory.makeLexbuilder(current_job=build)
408+
409+
410+class BuildResultOrRequestsTestsMixin(object):
411+ """A mixin with tests that work for either BuildResult or BuildRequest.
412+
413+ You just need to mix this with TestCase in your subclass and define
414+ factoryMethod and model.
415+ """
416+ factoryMethod = None
417+ model = None
418+
419+ def test_default_model_manager_only_returns_public_builds(self):
420+ private_object = self.factoryMethod(
421+ project=factory.makeProject(is_private=True))
422+ public_object = self.factoryMethod(
423+ project=factory.makeProject(is_private=False))
424+ self.assertEqual([public_object], list(self.model.objects.all()))
425+
426+ def test_all_objects_model_manager(self):
427+ # self.model.all_objects() is a separate model manager which returns
428+ # private objects as well. Its .all() method will return public and
429+ # private objects regardless of whether or not the current user has
430+ # access to them. It is to be used with caution.
431+ public_object = self.factoryMethod(
432+ project=factory.makeProject(is_private=False))
433+ private_object = self.factoryMethod(
434+ project=factory.makeProject(is_private=True))
435+ self.assertEqual([public_object, private_object],
436+ list(self.model.all_objects.all()))
437+
438+ def test_accessible_by_user(self):
439+ user = factory.makeUser()
440+ group = factory.makeAccessGroup([user])
441+ project = factory.makeProject(is_private=True, access_groups=[group])
442+ visible_object = self.factoryMethod(project=project)
443+ # This release is from a private project which has no access groups
444+ # set up, so the user cannot see it.
445+ invisible_object = self.factoryMethod(
446+ project=factory.makeProject(is_private=True))
447+ self.assertEqual(
448+ [visible_object],
449+ list(self.model.all_objects.accessible_by_user(user)))
450+
451+
452+class BuildResultTests(TestCase, BuildResultOrRequestsTestsMixin):
453+ model = BuildResult
454+ factoryMethod = factory.makeBuildResult
455+
456+
457+class BuildRequestTests(TestCase, BuildResultOrRequestsTestsMixin):
458+ model = BuildRequest
459+ factoryMethod = factory.makeBuildRequest
460
461=== modified file 'lib/offspring/web/settings.py'
462--- lib/offspring/web/settings.py 2011-09-20 01:13:12 +0000
463+++ lib/offspring/web/settings.py 2011-09-22 20:33:19 +0000
464@@ -96,6 +96,7 @@
465 'djcelery',
466 'djkombu',
467 'piston',
468+ 'django_group_access',
469 # Offspring Apps
470 'offspring.web.queuemanager',
471 )
472
473=== added directory 'migration'
474=== added file 'migration/001-project-privacy.sql'
475--- migration/001-project-privacy.sql 1970-01-01 00:00:00 +0000
476+++ migration/001-project-privacy.sql 2011-09-22 20:33:19 +0000
477@@ -0,0 +1,3 @@
478+ALTER TABLE projects ADD COLUMN "is_private" boolean NOT NULL default FALSE;
479+ALTER TABLE projects ADD COLUMN "owner_id" integer REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED;
480+ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL);
481
482=== modified file 'requirements/requirements.web.txt'
483--- requirements/requirements.web.txt 2011-08-11 00:16:09 +0000
484+++ requirements/requirements.web.txt 2011-09-22 20:33:19 +0000
485@@ -11,3 +11,4 @@
486 celery>=2.3.1
487 django-celery>=2.3.0
488 django-kombu>=0.9.4
489+django-group-access>=0.0.1

Subscribers

People subscribed via source and target branches