From cc34c19542bb129cc9c124af1fefe003b47b118c Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 11:39:17 -0700 Subject: [PATCH 01/12] Add proper filtering of build objects in manager. --- readthedocs/builds/models.py | 10 +++++----- readthedocs/builds/views.py | 25 +++++++++---------------- readthedocs/privacy/backend.py | 31 +++++++++++++++++++++++++++++++ readthedocs/privacy/loader.py | 4 ++++ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 57eccf1b7ca..ccc6079ffb9 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -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) from readthedocs.core.resolver import resolve @@ -321,7 +320,7 @@ class Build(models.Model): # Manager - objects = RelatedProjectManager() + objects = BuildManager() class Meta: ordering = ['-date'] @@ -349,6 +348,7 @@ def finished(self): class BuildCommandResultMixin(object): + '''Mixin for common command result methods/properties Shared methods between the database model :py:cls:`BuildCommandResult` and diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 485ce6bb4f1..30d522c075a 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -16,20 +16,22 @@ log = logging.getLogger(__name__) -class BuildList(ListView): - model = Build +class BaseBuild(object): 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(BaseBuild, ListView): + model = Build + def get_context_data(self, **kwargs): context = super(BuildList, self).get_context_data(**kwargs) @@ -50,27 +52,18 @@ def get_context_data(self, **kwargs): return context -class BuildDetail(DetailView): +class BuildDetail(BaseBuild, DetailView): model = Build 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])) diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py index b2685ae5c2d..100e49100e6 100644 --- a/readthedocs/privacy/backend.py +++ b/readthedocs/privacy/backend.py @@ -92,6 +92,7 @@ def api(self, user=None, *args, **kwargs): 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): @@ -118,6 +119,36 @@ def api(self, user=None, *args, **kwargs): return self.public(user) +class BuildManager(RelatedProjectManager): + """ + Build objects mostly related to Versions, + but should also take into account the privacy of the Project as well. + """ + + def _add_user_repos(self, queryset, user=None): + queryset = super(BuildManager, 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') + pks = [p.pk for p in user_queryset] + queryset = self.get_queryset().filter(version__pk__in=pks).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() + return queryset.distinct() + + def public(self, user=None, project=None): + queryset = self.filter(project__privacy_level=constants.PUBLIC, + version__privacy_level=constants.PUBLIC) + if user: + queryset = self._add_user_repos(queryset, user) + if project: + queryset = queryset.filter(project=project) + return queryset + + class VersionManager(RelatedProjectManager): def _add_user_repos(self, queryset, user=None, *args, **kwargs): diff --git a/readthedocs/privacy/loader.py b/readthedocs/privacy/loader.py index 5076fc2ce78..c81a663a88b 100644 --- a/readthedocs/privacy/loader.py +++ b/readthedocs/privacy/loader.py @@ -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')) From ceea8712b93cafa816fa89309a7d34b5cc557778 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 11:39:38 -0700 Subject: [PATCH 02/12] Clean up a few bits of UX around build filtering --- readthedocs/templates/builds/build_list.html | 3 +++ readthedocs/templates/core/project_details.html | 7 ++++++- readthedocs/templates/core/project_downloads.html | 4 ++-- readthedocs/templates/projects/project_version_list.html | 5 +++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/readthedocs/templates/builds/build_list.html b/readthedocs/templates/builds/build_list.html index b4cc93a13d9..5ac801b3bc0 100644 --- a/readthedocs/templates/builds/build_list.html +++ b/readthedocs/templates/builds/build_list.html @@ -50,6 +50,7 @@

+ {% if versions %}
@@ -67,6 +68,8 @@

+ {% endif %} +

{% trans "Recent Builds" %}

diff --git a/readthedocs/templates/core/project_details.html b/readthedocs/templates/core/project_details.html index d836790e7e3..93370bdb0ff 100644 --- a/readthedocs/templates/core/project_details.html +++ b/readthedocs/templates/core/project_details.html @@ -24,7 +24,6 @@ {% endif %} -{% if versions %}

{% trans "Versions" %}

@@ -64,11 +63,17 @@

{% trans "Versions" %}

{% endif %} + + {% empty %} +

+ No active versions. +

{% endfor %}
+{% if versions %}

{% trans "Build a version" %}

diff --git a/readthedocs/templates/core/project_downloads.html b/readthedocs/templates/core/project_downloads.html index cd9bdf1eaf6..7c850e05a28 100644 --- a/readthedocs/templates/core/project_downloads.html +++ b/readthedocs/templates/core/project_downloads.html @@ -26,7 +26,7 @@ {% endif %} {% empty %} -
  • +

    {% trans "No downloads for this project." %} -

  • +

    {% endfor %} diff --git a/readthedocs/templates/projects/project_version_list.html b/readthedocs/templates/projects/project_version_list.html index 4d7e8b304b0..d8475f2afc7 100644 --- a/readthedocs/templates/projects/project_version_list.html +++ b/readthedocs/templates/projects/project_version_list.html @@ -64,6 +64,11 @@

    {% trans "Active Versions" %}

    {% endblock active-versions %} + + {% empty %} +

    + No active versions. +

    {% endfor %}
    From 5241b5d52ced10ecc6840cce72c45c3393df025e Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 13:26:49 -0700 Subject: [PATCH 03/12] =?UTF-8?q?Kill=20args=20and=20kwargs=20that=20weren?= =?UTF-8?q?=E2=80=99t=20needed=20&=20misleading.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readthedocs/privacy/backend.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py index 100e49100e6..129fd10d173 100644 --- a/readthedocs/privacy/backend.py +++ b/readthedocs/privacy/backend.py @@ -28,7 +28,7 @@ def _add_user_repos(self, queryset, user): # User has no special privs 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. """ @@ -37,20 +37,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) @@ -59,16 +59,16 @@ 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): - def _add_user_repos(self, queryset, user=None, *args, **kwargs): + 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() @@ -79,7 +79,7 @@ def _add_user_repos(self, queryset, user=None, *args, **kwargs): queryset = self.get_queryset().filter(project__pk__in=pks) | queryset return queryset.distinct() - def public(self, user=None, project=None, *args, **kwargs): + def public(self, user=None, project=None): queryset = self.filter(project__privacy_level=constants.PUBLIC) if user: queryset = self._add_user_repos(queryset, user) @@ -87,7 +87,7 @@ def public(self, user=None, project=None, *args, **kwargs): queryset = queryset.filter(project=project) return queryset - def api(self, user=None, *args, **kwargs): + def api(self, user=None): return self.public(user) @@ -95,7 +95,7 @@ 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): + 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() @@ -107,7 +107,7 @@ def _add_user_repos(self, queryset, user=None, *args, **kwargs): .filter(build__project__pk__in=pks) | queryset) return queryset.distinct() - def public(self, user=None, project=None, *args, **kwargs): + def public(self, user=None, project=None): queryset = self.filter(build__project__privacy_level=constants.PUBLIC) if user: queryset = self._add_user_repos(queryset, user) @@ -115,7 +115,7 @@ def public(self, user=None, project=None, *args, **kwargs): queryset = queryset.filter(build__project=project) return queryset - def api(self, user=None, *args, **kwargs): + def api(self, user=None): return self.public(user) @@ -151,7 +151,7 @@ def public(self, user=None, project=None): class VersionManager(RelatedProjectManager): - def _add_user_repos(self, queryset, user=None, *args, **kwargs): + def _add_user_repos(self, queryset, user=None): queryset = super(VersionManager, self)._add_user_repos(queryset, user) if user and user.is_authenticated(): # Add in possible user-specific views @@ -164,7 +164,7 @@ def _add_user_repos(self, queryset, user=None, *args, **kwargs): queryset = self.get_queryset().all().distinct() return queryset.distinct() - def public(self, user=None, project=None, only_active=True, *args, **kwargs): + def public(self, user=None, project=None, only_active=True): queryset = self.filter(project__privacy_level=constants.PUBLIC, privacy_level=constants.PUBLIC) if user: @@ -175,7 +175,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): @@ -218,7 +218,7 @@ 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() From 63b9493ebda36853e360643069630531f3c469af Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 13:27:54 -0700 Subject: [PATCH 04/12] Add basic blocks to footer --- readthedocs/restapi/templates/restapi/footer.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/readthedocs/restapi/templates/restapi/footer.html b/readthedocs/restapi/templates/restapi/footer.html index dd4425a3311..a620af05cea 100644 --- a/readthedocs/restapi/templates/restapi/footer.html +++ b/readthedocs/restapi/templates/restapi/footer.html @@ -11,6 +11,7 @@
    {% endif %} + {% block versions %} {% if translations %}
    Languages
    @@ -44,7 +45,9 @@ {% endfor %}
    {% endif %} + {% endblock %} + {% block downloads %} {% if downloads %}
    Downloads
    @@ -53,7 +56,9 @@ {% endfor %}
    {% endif %} + {% endblock %} + {% block readthedocs %}
    On Read the Docs
    @@ -67,6 +72,9 @@ Downloads
    + {% endblock %} + + {% block vcs %} {% if github_edit_url %}
    @@ -86,7 +94,9 @@
    {% endif %} + {% endblock %} + {% block search %}
    Search
    @@ -97,6 +107,7 @@
    + {% endblock %} From cf0f12610e13cd07c024eec76e9f8a7acad0e3e1 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 14:47:00 -0700 Subject: [PATCH 05/12] Add privacy filtering properly --- readthedocs/privacy/backend.py | 185 ++++++++++---------- readthedocs/rtd_tests/tests/test_privacy.py | 4 + 2 files changed, 95 insertions(+), 94 deletions(-) diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py index 129fd10d173..a1c6df7cb89 100644 --- a/readthedocs/privacy/backend.py +++ b/readthedocs/privacy/backend.py @@ -14,18 +14,16 @@ 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): @@ -66,141 +64,151 @@ 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): - # Hack around get_objects_for_user not supporting global perms + """ + Versions take into account their own privacy_level setting. + """ + + def _add_user_repos(self, queryset, user): 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 + user_queryset = get_objects_for_user(user, 'builds.view_version') + queryset = user_queryset | queryset return queryset.distinct() - def public(self, user=None, project=None): - queryset = self.filter(project__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: queryset = queryset.filter(project=project) + if only_active: + queryset = queryset.filter(active=True) return queryset def api(self, user=None): - return self.public(user) + return self.public(user, only_active=False) + + def create_stable(self, **kwargs): + defaults = { + 'slug': STABLE, + 'verbose_name': STABLE_VERBOSE_NAME, + 'machine': True, + 'active': True, + 'identifier': STABLE, + 'type': TAG, + } + defaults.update(kwargs) + return self.create(**defaults) + def create_latest(self, **kwargs): + defaults = { + 'slug': LATEST, + 'verbose_name': LATEST_VERBOSE_NAME, + 'machine': True, + 'active': True, + 'identifier': LATEST, + 'type': BRANCH, + } + defaults.update(kwargs) + return self.create(**defaults) -class RelatedBuildManager(models.Manager): - '''For models with association to a project through :py:cls:`Build`''' +class BuildManager(models.Manager): + + """ + Build objects take into account the privacy of the Version they relate to. + """ 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'): + 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) + 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() def public(self, user=None, project=None): - queryset = self.filter(build__project__privacy_level=constants.PUBLIC) + queryset = self.filter(version__privacy_level=constants.PUBLIC) if user: queryset = self._add_user_repos(queryset, user) if project: - queryset = queryset.filter(build__project=project) + queryset = queryset.filter(project=project) return queryset def api(self, user=None): return self.public(user) -class BuildManager(RelatedProjectManager): +class RelatedProjectManager(models.Manager): + """ - Build objects mostly related to Versions, - but should also take into account the privacy of the Project as well. + 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): - queryset = super(BuildManager, self)._add_user_repos(queryset, user) - if user and user.is_authenticated(): + # 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 - 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).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() + 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, - version__privacy_level=constants.PUBLIC) + 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 VersionManager(RelatedProjectManager): + +class RelatedBuildManager(models.Manager): + + '''For models with association to a project through :py:cls:`Build`''' def _add_user_repos(self, queryset, user=None): - queryset = super(VersionManager, self)._add_user_repos(queryset, user) - if user and user.is_authenticated(): - # Add in possible user-specific views + 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') - 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() + 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, only_active=True): - queryset = self.filter(project__privacy_level=constants.PUBLIC, - privacy_level=constants.PUBLIC) + 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(project=project) - if only_active: - queryset = queryset.filter(active=True) + queryset = queryset.filter(build__project=project) return queryset def api(self, user=None): - return self.public(user, only_active=False) + return self.public(user) - def create_stable(self, **kwargs): - defaults = { - 'slug': STABLE, - 'verbose_name': STABLE_VERBOSE_NAME, - 'machine': True, - 'active': True, - 'identifier': STABLE, - 'type': TAG, - } - defaults.update(kwargs) - return self.create(**defaults) - def create_latest(self, **kwargs): - defaults = { - 'slug': LATEST, - 'verbose_name': LATEST_VERBOSE_NAME, - 'machine': True, - 'active': True, - 'identifier': LATEST, - 'type': BRANCH, - } - defaults.update(kwargs) - return self.create(**defaults) +class RelatedUserManager(models.Manager): + + """For models with relations through :py:cls:`User`""" + + 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): @@ -212,14 +220,3 @@ def is_admin(cls, user, project): class AdminNotAuthorized(ValueError): pass - - -class RelatedUserManager(models.Manager): - - """For models with relations through :py:cls:`User`""" - - def api(self, user=None): - """Return objects for user""" - if not user.is_authenticated(): - return self.none() - return self.filter(users=user) diff --git a/readthedocs/rtd_tests/tests/test_privacy.py b/readthedocs/rtd_tests/tests/test_privacy.py index c0371561183..729425423ed 100644 --- a/readthedocs/rtd_tests/tests/test_privacy.py +++ b/readthedocs/rtd_tests/tests/test_privacy.py @@ -55,6 +55,10 @@ def _create_kong(self, privacy_level='private', proj.num_point = 2 proj.save() + latest = proj.versions.get(slug='latest') + latest.privacy_level = version_privacy_level + latest.save() + self.assertAlmostEqual(Project.objects.count(), 1) r = self.client.get('/projects/django-kong/') self.assertEqual(r.status_code, 200) From 61264099c2e7ea1f4565ea16e4eb9f5753359004 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 16:22:58 -0700 Subject: [PATCH 06/12] Clean up permissions bits --- readthedocs/api/base.py | 2 +- readthedocs/privacy/backend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/api/base.py b/readthedocs/api/base.py index e5e71d5b768..eeb8b5ed487 100644 --- a/readthedocs/api/base.py +++ b/readthedocs/api/base.py @@ -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): diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py index a1c6df7cb89..60a25e5f2ae 100644 --- a/readthedocs/privacy/backend.py +++ b/readthedocs/privacy/backend.py @@ -123,7 +123,7 @@ class BuildManager(models.Manager): """ def _add_user_repos(self, queryset, user=None): - if user.has_perm('builds.view_version'): + if user.has_perm('projects.view_project'): return self.get_queryset().all().distinct() if user.is_authenticated(): user_queryset = get_objects_for_user(user, 'builds.view_version') From befd236e86912d3b35e07ecc90a76d9080d81a9e Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Oct 2015 16:37:35 -0700 Subject: [PATCH 07/12] Correct global perm --- readthedocs/privacy/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/privacy/backend.py b/readthedocs/privacy/backend.py index 60a25e5f2ae..035756b4975 100644 --- a/readthedocs/privacy/backend.py +++ b/readthedocs/privacy/backend.py @@ -71,7 +71,7 @@ class VersionManager(models.Manager): """ def _add_user_repos(self, queryset, user): - if user.has_perm('projects.view_project'): + 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') @@ -123,7 +123,7 @@ class BuildManager(models.Manager): """ def _add_user_repos(self, queryset, user=None): - if user.has_perm('projects.view_project'): + 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') From 10989739abe3ced4d3542965ac22fe99701fea62 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 7 Oct 2015 10:57:29 -0700 Subject: [PATCH 08/12] Mark strings as translatable --- readthedocs/templates/core/project_details.html | 2 +- readthedocs/templates/projects/project_version_list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/templates/core/project_details.html b/readthedocs/templates/core/project_details.html index 93370bdb0ff..9513b0b8e55 100644 --- a/readthedocs/templates/core/project_details.html +++ b/readthedocs/templates/core/project_details.html @@ -66,7 +66,7 @@

    {% trans "Versions" %}

    {% empty %}

    - No active versions. + {% trans "No active versions." %}

    {% endfor %} diff --git a/readthedocs/templates/projects/project_version_list.html b/readthedocs/templates/projects/project_version_list.html index d8475f2afc7..80a937beaf0 100644 --- a/readthedocs/templates/projects/project_version_list.html +++ b/readthedocs/templates/projects/project_version_list.html @@ -67,7 +67,7 @@

    {% trans "Active Versions" %}

    {% empty %}

    - No active versions. + {% trans "No active versions." %}

    {% endfor %} From d07780cd7d385eb790c1bac18dd395ca602bdbca Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 7 Oct 2015 11:11:44 -0700 Subject: [PATCH 09/12] Add test for build content --- readthedocs/rtd_tests/tests/test_privacy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readthedocs/rtd_tests/tests/test_privacy.py b/readthedocs/rtd_tests/tests/test_privacy.py index 729425423ed..c8f919febe0 100644 --- a/readthedocs/rtd_tests/tests/test_privacy.py +++ b/readthedocs/rtd_tests/tests/test_privacy.py @@ -122,11 +122,15 @@ def test_private_branch(self): self.assertEqual(Version.objects.get(slug='test-slug').privacy_level, 'private') r = self.client.get('/projects/django-kong/') self.assertTrue('test-slug' in r.content) + r = self.client.get('/projects/django-kong/builds/') + self.assertTrue('test-slug' in r.content) # Make sure it doesn't show up as tester self.client.login(username='tester', password='test') r = self.client.get('/projects/django-kong/') self.assertTrue('test-slug' not in r.content) + r = self.client.get('/projects/django-kong/builds/') + self.assertTrue('test-slug' not in r.content) def test_public_branch(self): kong = self._create_kong('public', 'public') From f56adf6f10b862732a0689ccadce74c197fe3497 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 7 Oct 2015 11:15:32 -0700 Subject: [PATCH 10/12] Add another dedicated test --- readthedocs/rtd_tests/tests/test_privacy.py | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/readthedocs/rtd_tests/tests/test_privacy.py b/readthedocs/rtd_tests/tests/test_privacy.py index c8f919febe0..a13472c4306 100644 --- a/readthedocs/rtd_tests/tests/test_privacy.py +++ b/readthedocs/rtd_tests/tests/test_privacy.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from readthedocs.builds.constants import LATEST -from readthedocs.builds.models import Version +from readthedocs.builds.models import Version, Build from readthedocs.projects.models import Project from readthedocs.projects.forms import UpdateProjectForm from readthedocs.projects import tasks @@ -327,3 +327,23 @@ def test_public_download_filename(self): r = self.client.get('/projects/django-kong/downloads/htmlzip/latest/') self.assertEqual(r.status_code, 302) self.assertEqual(r._headers['location'][1], 'http://testserver/media/htmlzip/django-kong/latest/django-kong.zip') + +# Build Filtering + + def test_build_filtering(self): + kong = self._create_kong('public', 'private') + + self.client.login(username='eric', password='test') + ver = Version.objects.create(project=kong, identifier='test id', + verbose_name='test verbose', privacy_level='private', slug='test-slug', active=True) + + r = self.client.get('/projects/django-kong/builds/') + self.assertTrue(r.content.count('test-slug', 1)) + Build.objects.create(project=kong, version=ver) + r = self.client.get('/projects/django-kong/builds/') + self.assertTrue(r.content.count('test-slug', 2)) + + # Make sure it doesn't show up as tester + self.client.login(username='tester', password='test') + r = self.client.get('/projects/django-kong/builds/') + self.assertTrue('test-slug' not in r.content) From e3c5879296b2f1e298d2e2b77fccea4b460e57e6 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 7 Oct 2015 11:50:23 -0700 Subject: [PATCH 11/12] Render footer properly as a request context --- readthedocs/core/context_processors.py | 3 ++- readthedocs/restapi/views/footer_views.py | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/readthedocs/core/context_processors.py b/readthedocs/core/context_processors.py index 7de2be2a897..f1b4dffe170 100644 --- a/readthedocs/core/context_processors.py +++ b/readthedocs/core/context_processors.py @@ -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 diff --git a/readthedocs/restapi/views/footer_views.py b/readthedocs/restapi/views/footer_views.py index 3513a22e759..f90e5e78e75 100644 --- a/readthedocs/restapi/views/footer_views.py +++ b/readthedocs/restapi/views/footer_views.py @@ -1,7 +1,6 @@ from django.shortcuts import get_object_or_404 -from django.template import loader as template_loader +from django.template import RequestContext, loader as template_loader from django.conf import settings -from django.core.context_processors import csrf from rest_framework import decorators, permissions from rest_framework.renderers import JSONPRenderer, JSONRenderer, BrowsableAPIRenderer @@ -121,8 +120,8 @@ def footer_html(request): 'bitbucket_url': version.get_bitbucket_url(docroot, page_slug, source_suffix), } - context.update(csrf(request)) - html = template_loader.get_template('restapi/footer.html').render(context) + request_context = RequestContext(request, context) + html = template_loader.get_template('restapi/footer.html').render(request_context) resp_data = { 'html': html, 'version_active': version.active, From dd7db9e1f5524e14215ede8e58ecd21ebe3cd223 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 7 Oct 2015 14:04:19 -0700 Subject: [PATCH 12/12] Fix base and include model there --- readthedocs/builds/views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 30d522c075a..c330f625319 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -16,7 +16,8 @@ log = logging.getLogger(__name__) -class BaseBuild(object): +class BuildBase(object): + model = Build def get_queryset(self): self.project_slug = self.kwargs.get('project_slug', None) @@ -29,8 +30,7 @@ def get_queryset(self): return queryset -class BuildList(BaseBuild, ListView): - model = Build +class BuildList(BuildBase, ListView): def get_context_data(self, **kwargs): context = super(BuildList, self).get_context_data(**kwargs) @@ -52,8 +52,7 @@ def get_context_data(self, **kwargs): return context -class BuildDetail(BaseBuild, DetailView): - model = Build +class BuildDetail(BuildBase, DetailView): pk_url_kwarg = 'build_pk' def get_context_data(self, **kwargs):