Skip to content

Commit

Permalink
Merge pull request readthedocs#1735 from rtfd/build-filtering
Browse files Browse the repository at this point in the history
Add a BuildManager & proper UI around filtered out builds
  • Loading branch information
ericholscher committed Oct 7, 2015
2 parents 3d70712 + dd7db9e commit 6f9f454
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 116 deletions.
2 changes: 1 addition & 1 deletion readthedocs/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class Meta(object):
# return bundle

def get_object_list(self, request):
self._meta.queryset = Version.objects.api(user=request.user, only_active=False)
self._meta.queryset = Version.objects.api(user=request.user)
return super(VersionResource, self).get_object_list(request)

def version_compare(self, request, project_slug, base=None, **kwargs):
Expand Down
10 changes: 5 additions & 5 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
from guardian.shortcuts import assign
from taggit.managers import TaggableManager

from readthedocs.privacy.loader import (VersionManager, RelatedProjectManager,
RelatedBuildManager)
from readthedocs.privacy.loader import (VersionManager, RelatedBuildManager,
BuildManager)
from readthedocs.projects.models import Project
from readthedocs.projects.constants import (PRIVACY_CHOICES, REPO_TYPE_GIT,
REPO_TYPE_HG, GITHUB_URL,
from readthedocs.projects.constants import (PRIVACY_CHOICES, GITHUB_URL,
GITHUB_REGEXS, BITBUCKET_URL,
BITBUCKET_REGEXS, PRIVATE)
from readthedocs.core.resolver import resolve
Expand Down Expand Up @@ -323,7 +322,7 @@ class Build(models.Model):

# Manager

objects = RelatedProjectManager()
objects = BuildManager()

class Meta:
ordering = ['-date']
Expand Down Expand Up @@ -351,6 +350,7 @@ def finished(self):


class BuildCommandResultMixin(object):

'''Mixin for common command result methods/properties
Shared methods between the database model :py:cls:`BuildCommandResult` and
Expand Down
24 changes: 8 additions & 16 deletions readthedocs/builds/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@
log = logging.getLogger(__name__)


class BuildList(ListView):
class BuildBase(object):
model = Build

def get_queryset(self):
self.project_slug = self.kwargs.get('project_slug', None)

self.project = get_object_or_404(
Project.objects.protected(self.request.user),
slug=self.project_slug
)
queryset = Build.objects.filter(project=self.project)
queryset = Build.objects.public(user=self.request.user, project=self.project)

return queryset


class BuildList(BuildBase, ListView):

def get_context_data(self, **kwargs):
context = super(BuildList, self).get_context_data(**kwargs)

Expand All @@ -50,27 +52,17 @@ def get_context_data(self, **kwargs):
return context


class BuildDetail(DetailView):
model = Build
class BuildDetail(BuildBase, DetailView):
pk_url_kwarg = 'build_pk'

def get_queryset(self):
self.project_slug = self.kwargs.get('project_slug', None)

self.project = get_object_or_404(
Project.objects.protected(self.request.user),
slug=self.project_slug
)
queryset = Build.objects.filter(project=self.project)

return queryset

def get_context_data(self, **kwargs):
context = super(BuildDetail, self).get_context_data(**kwargs)
context['project'] = self.project
return context


# Old build view redirects

def builds_redirect_list(request, project_slug):
return HttpResponsePermanentRedirect(reverse('builds_project_list', args=[project_slug]))

Expand Down
3 changes: 2 additions & 1 deletion readthedocs/core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def readthedocs_processor(request):
'PRODUCTION_DOMAIN': getattr(settings, 'PRODUCTION_DOMAIN', None),
'USE_SUBDOMAINS': getattr(settings, 'USE_SUBDOMAINS', None),
'GLOBAL_ANALYTICS_CODE': getattr(settings, 'GLOBAL_ANALYTICS_CODE', 'UA-17997319-1'),
'TEMPLATE_ROOT': getattr(settings, 'TEMPLATE_ROOT', None) + '/',
'SITE_ROOT': getattr(settings, 'SITE_ROOT', '') + '/',
'TEMPLATE_ROOT': getattr(settings, 'TEMPLATE_ROOT', '') + '/',
}
return exports
198 changes: 113 additions & 85 deletions readthedocs/privacy/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@

class ProjectManager(models.Manager):

"""
Projects take into account their own privacy_level setting.
"""

def _add_user_repos(self, queryset, user):
# Avoid circular import
from readthedocs.projects.models import Project
# Show all projects to super user
if user.has_perm('projects.view_project'):
return Project.objects.all().distinct()
# Show user projects to user
return self.get_queryset().all().distinct()
if user.is_authenticated():
# Add in possible user-specific views
user_queryset = get_objects_for_user(user, 'projects.view_project')
return user_queryset | queryset
# User has no special privs
queryset = user_queryset | queryset
return queryset.distinct()

def for_user_and_viewer(self, user, viewer, *args, **kwargs):
def for_user_and_viewer(self, user, viewer):
"""
Show projects that a user owns, that another user can see.
"""
Expand All @@ -37,20 +35,20 @@ def for_user_and_viewer(self, user, viewer, *args, **kwargs):
queryset = queryset.filter(users__in=[user])
return queryset

def for_admin_user(self, user=None, *args, **kwargs):
def for_admin_user(self, user=None):
if user.is_authenticated():
return self.filter(users__in=[user])
else:
return self.none()

def public(self, user=None, *args, **kwargs):
def public(self, user=None):
queryset = self.filter(privacy_level=constants.PUBLIC)
if user:
return self._add_user_repos(queryset, user)
else:
return queryset

def protected(self, user=None, *args, **kwargs):
def protected(self, user=None):
queryset = self.filter(privacy_level__in=[constants.PUBLIC, constants.PROTECTED])
if user:
return self._add_user_repos(queryset, user)
Expand All @@ -59,83 +57,29 @@ def protected(self, user=None, *args, **kwargs):

# Aliases

def dashboard(self, user=None, *args, **kwargs):
def dashboard(self, user=None):
return self.for_admin_user(user)

def api(self, user=None, *args, **kwargs):
def api(self, user=None):
return self.public(user)


class RelatedProjectManager(models.Manager):
class VersionManager(models.Manager):

def _add_user_repos(self, queryset, user=None, *args, **kwargs):
# Hack around get_objects_for_user not supporting global perms
if user.has_perm('projects.view_project'):
return self.get_queryset().all().distinct()
if user.is_authenticated():
# Add in possible user-specific views
project_qs = get_objects_for_user(user, 'projects.view_project')
pks = [p.pk for p in project_qs]
queryset = self.get_queryset().filter(project__pk__in=pks) | queryset
return queryset.distinct()
"""
Versions take into account their own privacy_level setting.
"""

def public(self, user=None, project=None, *args, **kwargs):
queryset = self.filter(project__privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(project=project)
return queryset

def api(self, user=None, *args, **kwargs):
return self.public(user)


class RelatedBuildManager(models.Manager):
'''For models with association to a project through :py:cls:`Build`'''

def _add_user_repos(self, queryset, user=None, *args, **kwargs):
# Hack around get_objects_for_user not supporting global perms
if user.has_perm('projects.view_project'):
def _add_user_repos(self, queryset, user):
if user.has_perm('builds.view_version'):
return self.get_queryset().all().distinct()
if user.is_authenticated():
# Add in possible user-specific views
project_qs = get_objects_for_user(user, 'projects.view_project')
pks = [p.pk for p in project_qs]
queryset = (self.get_queryset()
.filter(build__project__pk__in=pks) | queryset)
return queryset.distinct()

def public(self, user=None, project=None, *args, **kwargs):
queryset = self.filter(build__project__privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(build__project=project)
return queryset

def api(self, user=None, *args, **kwargs):
return self.public(user)


class VersionManager(RelatedProjectManager):

def _add_user_repos(self, queryset, user=None, *args, **kwargs):
queryset = super(VersionManager, self)._add_user_repos(queryset, user)
if user and user.is_authenticated():
# Add in possible user-specific views
user_queryset = get_objects_for_user(user, 'builds.view_version')
queryset = user_queryset.distinct() | queryset
elif user:
# Hack around get_objects_for_user not supporting global perms
global_access = user.has_perm('builds.view_version')
if global_access:
queryset = self.get_queryset().all().distinct()
queryset = user_queryset | queryset
return queryset.distinct()

def public(self, user=None, project=None, only_active=True, *args, **kwargs):
queryset = self.filter(project__privacy_level=constants.PUBLIC,
privacy_level=constants.PUBLIC)
def public(self, user=None, project=None, only_active=True):
queryset = self.filter(privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
Expand All @@ -144,7 +88,7 @@ def public(self, user=None, project=None, only_active=True, *args, **kwargs):
queryset = queryset.filter(active=True)
return queryset

def api(self, user=None, *args, **kwargs):
def api(self, user=None):
return self.public(user, only_active=False)

def create_stable(self, **kwargs):
Expand Down Expand Up @@ -172,23 +116,107 @@ def create_latest(self, **kwargs):
return self.create(**defaults)


class AdminPermission(object):
class BuildManager(models.Manager):

@classmethod
def is_admin(cls, user, project):
return user in project.users.all()
"""
Build objects take into account the privacy of the Version they relate to.
"""

def _add_user_repos(self, queryset, user=None):
if user.has_perm('builds.view_version'):
return self.get_queryset().all().distinct()
if user.is_authenticated():
user_queryset = get_objects_for_user(user, 'builds.view_version')
pks = [p.pk for p in user_queryset]
queryset = self.get_queryset().filter(version__pk__in=pks) | queryset
return queryset.distinct()

class AdminNotAuthorized(ValueError):
pass
def public(self, user=None, project=None):
queryset = self.filter(version__privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(project=project)
return queryset

def api(self, user=None):
return self.public(user)


class RelatedProjectManager(models.Manager):

"""
A manager for things that relate to Project and need to get their perms from the project.
This shouldn't be used as a subclass.
"""

def _add_user_repos(self, queryset, user=None):
# Hack around get_objects_for_user not supporting global perms
if user.has_perm('projects.view_project'):
return self.get_queryset().all().distinct()
if user.is_authenticated():
# Add in possible user-specific views
project_qs = get_objects_for_user(user, 'projects.view_project')
pks = [p.pk for p in project_qs]
queryset = self.get_queryset().filter(project__pk__in=pks) | queryset
return queryset.distinct()

def public(self, user=None, project=None):
queryset = self.filter(project__privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(project=project)
return queryset

def api(self, user=None):
return self.public(user)


class RelatedBuildManager(models.Manager):

'''For models with association to a project through :py:cls:`Build`'''

def _add_user_repos(self, queryset, user=None):
if user.has_perm('builds.view_version'):
return self.get_queryset().all().distinct()
if user.is_authenticated():
user_queryset = get_objects_for_user(user, 'builds.view_version')
pks = [p.pk for p in user_queryset]
queryset = self.get_queryset().filter(
build__version__pk__in=pks) | queryset
return queryset.distinct()

def public(self, user=None, project=None):
queryset = self.filter(build__version__privacy_level=constants.PUBLIC)
if user:
queryset = self._add_user_repos(queryset, user)
if project:
queryset = queryset.filter(build__project=project)
return queryset

def api(self, user=None):
return self.public(user)


class RelatedUserManager(models.Manager):

"""For models with relations through :py:cls:`User`"""

def api(self, user=None, *args, **kwargs):
def api(self, user=None):
"""Return objects for user"""
if not user.is_authenticated():
return self.none()
return self.filter(users=user)


class AdminPermission(object):

@classmethod
def is_admin(cls, user, project):
return user in project.users.all()


class AdminNotAuthorized(ValueError):
pass
4 changes: 4 additions & 0 deletions readthedocs/privacy/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
VersionManager = import_by_path(
getattr(settings, 'VERSION_MANAGER',
'readthedocs.privacy.backend.VersionManager'))
BuildManager = import_by_path(
getattr(settings, 'BUILD_MANAGER',
'readthedocs.privacy.backend.BuildManager'))

RelatedProjectManager = import_by_path(
getattr(settings, 'RELATED_PROJECT_MANAGER',
'readthedocs.privacy.backend.RelatedProjectManager'))
Expand Down
Loading

0 comments on commit 6f9f454

Please sign in to comment.