diff --git a/pip_requirements.txt b/pip_requirements.txt index c79f2a37b..c6d682759 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -7,20 +7,33 @@ --allow-external lazr.authentication --allow-unverified lazr.authentication -# Basic tools -virtualenv==1.11.1 +## Upgraded packages +pip==1.5.6 +virtualenv==1.11.6 docutils==0.11 -pip==1.5.1 Sphinx==1.2.2 -django==1.4.13 -south==0.7.6 + +django>=1.6 +django-tastypie==0.11.1 +django-haystack==2.1.0 +celery-haystack==0.7.2 +django-guardian==1.2.0 +django-extensions==1.3.8 +South==0.8.4 +djangorestframework==2.3.14 +django-nose==1.2 + +requests==2.3.0 +slumber==0.6.0 +lxml==3.3.5 + +# Basic tools redis==2.7.1 hiredis==0.1.2 -requests==2.0 -slumber==0.6 celery==3.0.24 django-celery==3.0.23 mkdocs==0.4 +django-allauth==0.16.1 # VCS bzr==2.5b4 @@ -30,22 +43,16 @@ httplib2==0.7.7 # Search elasticsearch==0.4.3 -celery-haystack==0.6.2 pyquery==1.2.2 # Utils django-gravatar2==1.0.6 doc2dash==1.1.0 -lxml==3.0.2 pytz==2013b beautifulsoup4==4.1.3 Unipath==0.2.1 -django-extensions==0.7.1 -django-guardian==1.0.4 django-kombu==0.9.4 -django-nose==1.1 -django-profiles==0.2 django-secure==0.1.2 mimeparse==0.1.3 mock==1.0.1 @@ -59,18 +66,12 @@ Distutils2==1.0a3 distlib==0.1.2 # Commenting stuff -djangorestframework==2.3.6 django-cors-headers==0.11 # Pegged git requirements -git+https://github.com/toastdriven/django-haystack@259274e4127f723d76b893c87a82777f9490b960#egg=django_haystack git+https://github.com/alex/django-filter.git#egg=django-filter -git+https://github.com/alex/django-taggit.git@36f6dabcf10e27c7d9442a94243d4189f2a4f121#egg=django_taggit-dev git+https://github.com/ericflo/django-pagination.git@e5f669036c#egg=django_pagination-dev -git+https://github.com/nathanborror/django-basic-apps.git@171fdbe21a0dbbb38919a383cc265cb3cbc73771#egg=django_basic_apps-dev -git+https://github.com/nathanborror/django-registration.git@dc0b564b7bfb79f58592fe8ad836729a85ec17ae#egg=django_registration-dev -git+https://github.com/toastdriven/django-tastypie.git@c5451b90b18b0cb64841b2276d543230d5f58231#egg=django_tastypie-dev git+https://github.com/ericholscher/readthedocs-sphinx-ext#egg=readthedocs_ext -# Websupport -git+https://github.com/ericholscher/django-sphinx-websupport#egg=websupport +git+https://github.com/alex/django-taggit.git#egg=django_taggit-dev + diff --git a/readthedocs/api/base.py b/readthedocs/api/base.py index 6e4e47c87..b87ff2075 100644 --- a/readthedocs/api/base.py +++ b/readthedocs/api/base.py @@ -2,7 +2,7 @@ import logging import json from django.contrib.auth.models import User -from django.conf.urls.defaults import url +from django.conf.urls import url from django.shortcuts import get_object_or_404 from tastypie import fields @@ -18,7 +18,7 @@ from projects.utils import highest_version, mkversion, slugify_uniquely from projects import tasks from djangome import views as djangome -from .utils import SearchMixin, PostAuthentication, EnhancedModelResource +from .utils import SearchMixin, PostAuthentication log = logging.getLogger(__name__) @@ -57,14 +57,14 @@ class ProjectResource(ModelResource, SearchMixin): If a new resource is created, return ``HttpCreated`` (201 Created). """ deserialized = self.deserialize( - request, request.raw_post_data, + request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json') ) # Force this in an ugly way, at least should do "reverse" deserialized["users"] = ["/api/v1/user/%s/" % request.user.id] - bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized)) - self.is_valid(bundle, request) + bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) + self.is_valid(bundle) updated_bundle = self.obj_create(bundle, request=request) return HttpCreated(location=self.get_resource_uri(updated_bundle)) @@ -79,7 +79,7 @@ class ProjectResource(ModelResource, SearchMixin): project = get_object_or_404(Project, pk=kwargs['pk']) try: post_data = self.deserialize( - request, request.raw_post_data, + request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json') ) data = json.loads(post_data) @@ -115,11 +115,10 @@ class ProjectResource(ModelResource, SearchMixin): ] -class VersionResource(EnhancedModelResource): +class VersionResource(ModelResource): project = fields.ForeignKey(ProjectResource, 'project', full=True) class Meta: - queryset = Version.objects.all() allowed_methods = ['get', 'put', 'post'] always_return_data = True queryset = Version.objects.public() @@ -204,7 +203,7 @@ class VersionResource(EnhancedModelResource): ] -class BuildResource(EnhancedModelResource): +class BuildResource(ModelResource): project = fields.ForeignKey('api.base.ProjectResource', 'project') version = fields.ForeignKey('api.base.VersionResource', 'version') @@ -235,7 +234,7 @@ class BuildResource(EnhancedModelResource): ] -class FileResource(EnhancedModelResource, SearchMixin): +class FileResource(ModelResource, SearchMixin): project = fields.ForeignKey(ProjectResource, 'project', full=True) class Meta: diff --git a/readthedocs/api/utils.py b/readthedocs/api/utils.py index a59797ef6..6a3e04a05 100644 --- a/readthedocs/api/utils.py +++ b/readthedocs/api/utils.py @@ -148,7 +148,7 @@ class PostAuthentication(BasicAuthentication): class EnhancedModelResource(ModelResource): - def obj_get_list(self, request=None, **kwargs): + def obj_get_list(self, request=None, *args, **kwargs): """ A ORM-specific implementation of ``obj_get_list``. diff --git a/readthedocs/bookmarks/urls.py b/readthedocs/bookmarks/urls.py index b96933498..c488cd8e8 100644 --- a/readthedocs/bookmarks/urls.py +++ b/readthedocs/bookmarks/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url urlpatterns = patterns( diff --git a/readthedocs/bookmarks/views.py b/readthedocs/bookmarks/views.py index afe39df48..816fb5a68 100644 --- a/readthedocs/bookmarks/views.py +++ b/readthedocs/bookmarks/views.py @@ -2,7 +2,7 @@ import simplejson from django.contrib.auth.decorators import login_required from django.http import HttpResponse -from django.views.generic.list_detail import object_list +from core.generic.list_detail import object_list from bookmarks.models import Bookmark diff --git a/readthedocs/builds/urls.py b/readthedocs/builds/urls.py index 9b6991da0..ca6542ee0 100644 --- a/readthedocs/builds/urls.py +++ b/readthedocs/builds/urls.py @@ -1,22 +1,16 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url + +from .views import BuildList, BuildDetail urlpatterns = patterns( # base view, flake8 complains if it is on the previous line. 'builds.views', - url(r'^$', - 'build_list', - name='builds_list'), - url(r'^(?P[-\w]+)/(?P\d+)/$', - 'build_detail', + BuildDetail.as_view(), name='builds_detail'), - + url(r'^(?P[-\w]+)/$', - 'build_list', + BuildList.as_view(), name='builds_project_list'), - - url(r'^tag/(?P\w+)/$', - 'build_list', - name='builds_tag_list'), ) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 845df69c9..c14426d55 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -1,53 +1,50 @@ from django.shortcuts import get_object_or_404 -from django.views.generic.list_detail import object_list, object_detail - -from taggit.models import Tag +from django.views.generic import ListView, DetailView from builds.models import Build from builds.filters import BuildFilter from projects.models import Project +class BuildList(ListView): + model = Build -def build_list(request, project_slug=None, tag=None): - """Show a list of builds. - """ - project = get_object_or_404(Project.objects.protected(request.user), - slug=project_slug) - queryset = Build.objects.filter(project=project) + def get_queryset(self): + self.project_slug = self.kwargs.get('project_slug', None) - if tag: - tag = get_object_or_404(Tag, slug=tag) - queryset = queryset.filter(project__tags__in=[tag.slug]) - else: - tag = None + self.project = get_object_or_404( + Project.objects.protected(self.request.user), + slug=self.project_slug + ) + queryset = Build.objects.filter(project=self.project) - filter = BuildFilter(request.GET, queryset=queryset) - active_builds = queryset.exclude(state="finished").values('id') + return queryset - return object_list( - request, - queryset=queryset, - extra_context={ - 'project': project, - 'filter': filter, - 'tag': tag, - 'active_builds': active_builds - }, - template_object_name='build', - ) + def get_context_data(self, **kwargs): + context = super(BuildList, self).get_context_data(**kwargs) + filter = BuildFilter(self.request.GET, queryset=self.queryset) + active_builds = self.get_queryset().exclude(state="finished").values('id') -def build_detail(request, project_slug, pk): - """Show the details of a particular build. - """ - project = get_object_or_404(Project.objects.protected(request.user), - slug=project_slug) - queryset = Build.objects.filter(project=project) + context['project'] = self.project + context['filter'] = filter + context['active_builds'] = active_builds + return context - return object_detail( - request, - queryset=queryset, - object_id=pk, - extra_context={'project': project}, - template_object_name='build', - ) +class BuildDetail(DetailView): + 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) + + return queryset + + def get_context_data(self, **kwargs): + context = super(BuildDetail, self).get_context_data(**kwargs) + context['project'] = self.project + return context diff --git a/readthedocs/core/djangome_urls.py b/readthedocs/core/djangome_urls.py index b773d66e5..1fa3ddc51 100644 --- a/readthedocs/core/djangome_urls.py +++ b/readthedocs/core/djangome_urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url urlpatterns = patterns( '', # base view, flake8 complains if it is on the previous line. diff --git a/readthedocs/core/models.py b/readthedocs/core/models.py index 2edcb5e69..ef0879170 100644 --- a/readthedocs/core/models.py +++ b/readthedocs/core/models.py @@ -13,7 +13,7 @@ class UserProfile (models.Model): """ user = models.ForeignKey(User, verbose_name=_('User'), unique=True, related_name='profile') - whitelisted = models.BooleanField(_('Whitelisted')) + whitelisted = models.BooleanField(_('Whitelisted'), default=False) homepage = models.CharField(_('Homepage'), max_length=100, blank=True) allow_email = models.BooleanField(_('Allow email'), help_text=_('Show your email on VCS ' @@ -55,6 +55,6 @@ class UserProfile (models.Model): def create_profile(sender, **kwargs): if kwargs['created'] is True: try: - UserProfile.objects.create(user_id=kwargs['instance'].id) + UserProfile.objects.create(user_id=kwargs['instance'].id, whitelisted=False) except DatabaseError: pass diff --git a/readthedocs/core/single_version_urls.py b/readthedocs/core/single_version_urls.py index e7d706e7d..4d4103289 100644 --- a/readthedocs/core/single_version_urls.py +++ b/readthedocs/core/single_version_urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url urlpatterns = patterns( '', # base view, flake8 complains if it is on the previous line. diff --git a/readthedocs/core/subdomain_urls.py b/readthedocs/core/subdomain_urls.py index 8a1f4a372..4989c3a84 100644 --- a/readthedocs/core/subdomain_urls.py +++ b/readthedocs/core/subdomain_urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import url, patterns +from django.conf.urls import url, patterns from projects.constants import LANGUAGES_REGEX from urls import urlpatterns as main_patterns diff --git a/readthedocs/core/templates/account/login.html b/readthedocs/core/templates/account/login.html new file mode 100644 index 000000000..ec8b6eba7 --- /dev/null +++ b/readthedocs/core/templates/account/login.html @@ -0,0 +1,45 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} +{% load url from future %} + +{% block head_title %}{% trans "Sign In" %}{% endblock %} + +{% block content %} + +

{% trans "Sign In" %}

+ +{% if socialaccount.providers %} +

{% blocktrans with site.name as site_name %}Please sign in with one +of your existing third party accounts. Or, sign up +for a {{site_name}} account and sign in below:{% endblocktrans %}

+ +
+ +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" %} +
+ + + +
+ +{% include "socialaccount/snippets/login_extra.html" %} + +{% else %} +

{% blocktrans %}If you have not created an account yet, then please +sign up first.{% endblocktrans %}

+{% endif %} + + + +{% endblock %} diff --git a/readthedocs/core/templatetags/core_tags.py b/readthedocs/core/templatetags/core_tags.py index 717611506..78ecf3838 100644 --- a/readthedocs/core/templatetags/core_tags.py +++ b/readthedocs/core/templatetags/core_tags.py @@ -1,7 +1,10 @@ import urllib +import hashlib from django import template -from django.utils.hashcompat import hashlib +from django.conf import settings +from django.utils.safestring import mark_safe +from django.utils.encoding import force_bytes, force_text register = template.Library() @@ -36,3 +39,16 @@ def make_document_url(project, version=None, page=None): else: path = "" return base_url + path + +@register.filter(is_safe=True) +def restructuredtext(value): + try: + from docutils.core import publish_parts + except ImportError: + if settings.DEBUG: + raise template.TemplateSyntaxError("Error in 'restructuredtext' filter: The Python docutils library isn't installed.") + return force_text(value) + else: + docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) + parts = publish_parts(source=force_bytes(value), writer_name="html4css1", settings_overrides=docutils_settings) + return mark_safe(force_text(parts["fragment"])) \ No newline at end of file diff --git a/readthedocs/core/views.py b/readthedocs/core/views.py index 43430e9f6..db99b72a5 100644 --- a/readthedocs/core/views.py +++ b/readthedocs/core/views.py @@ -9,7 +9,7 @@ from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseNotFound from django.shortcuts import render_to_response, get_object_or_404, redirect from django.template import RequestContext -from django.views.decorators.csrf import csrf_view_exempt +from django.views.decorators.csrf import csrf_exempt from django.views.static import serve from django.views.generic import TemplateView @@ -107,7 +107,7 @@ def live_builds(request): context_instance=RequestContext(request)) -@csrf_view_exempt +@csrf_exempt def wipe_version(request, project_slug, version_slug): version = get_object_or_404(Version, project__slug=project_slug, slug=version_slug) @@ -191,7 +191,7 @@ def _build_url(url, branches): return HttpResponse(msg) -@csrf_view_exempt +@csrf_exempt def github_build(request): """ A post-commit hook for github. @@ -231,7 +231,7 @@ def github_build(request): return HttpResponse("You must POST to this resource.") -@csrf_view_exempt +@csrf_exempt def bitbucket_build(request): if request.method == 'POST': payload = request.POST.get('payload') @@ -253,7 +253,7 @@ def bitbucket_build(request): return HttpResponse("You must POST to this resource.") -@csrf_view_exempt +@csrf_exempt def generic_build(request, pk=None): try: project = Project.objects.get(pk=pk) diff --git a/readthedocs/manage.py b/readthedocs/manage.py index 5449d1052..c30ca4875 100755 --- a/readthedocs/manage.py +++ b/readthedocs/manage.py @@ -1,5 +1,11 @@ #!/usr/bin/env python +import os +import sys -import settings.sqlite -from django.core.management import execute_manager -execute_manager(settings.sqlite) +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.sqlite") + sys.path.append('readthedocs') + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/readthedocs/profiles/__init__.py b/readthedocs/profiles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/readthedocs/profiles/urls.py b/readthedocs/profiles/urls.py new file mode 100644 index 000000000..12548926c --- /dev/null +++ b/readthedocs/profiles/urls.py @@ -0,0 +1,43 @@ +""" +URLConf for Django user profile management. + +Recommended usage is to use a call to ``include()`` in your project's +root URLConf to include this URLConf for any URL beginning with +'/profiles/'. + +If the default behavior of the profile views is acceptable to you, +simply use a line like this in your root URLConf to set up the default +URLs for profiles:: + + (r'^profiles/', include('profiles.urls')), + +But if you'd like to customize the behavior (e.g., by passing extra +arguments to the various views) or split up the URLs, feel free to set +up your own URL patterns for these views instead. If you do, it's a +good idea to keep the name ``profiles_profile_detail`` for the pattern +which points to the ``profile_detail`` view, since several views use +``reverse()`` with that name to generate a default post-submission +redirect. If you don't use that name, remember to explicitly pass +``success_url`` to those views. + +""" + +from django.conf.urls import * + +from profiles import views + + +urlpatterns = patterns('', + url(r'^create/$', + views.create_profile, + name='profiles_create_profile'), + url(r'^edit/$', + views.edit_profile, + name='profiles_edit_profile'), + url(r'^(?P\w+)/$', + views.profile_detail, + name='profiles_profile_detail'), + url(r'^$', + views.ProfileListView.as_view(), + name='profiles_profile_list'), + ) diff --git a/readthedocs/profiles/utils.py b/readthedocs/profiles/utils.py new file mode 100644 index 000000000..8912cc115 --- /dev/null +++ b/readthedocs/profiles/utils.py @@ -0,0 +1,46 @@ +""" +Utility functions for retrieving and generating forms for the +site-specific user profile model specified in the +``AUTH_PROFILE_MODULE`` setting. + +""" + +from django import forms +from django.conf import settings +from django.db.models import get_model + +from django.contrib.auth.models import SiteProfileNotAvailable + + +def get_profile_model(): + """ + Return the model class for the currently-active user profile + model, as defined by the ``AUTH_PROFILE_MODULE`` setting. If that + setting is missing, raise + ``django.contrib.auth.models.SiteProfileNotAvailable``. + + """ + if (not hasattr(settings, 'AUTH_PROFILE_MODULE')) or \ + (not settings.AUTH_PROFILE_MODULE): + raise SiteProfileNotAvailable + profile_mod = get_model(*settings.AUTH_PROFILE_MODULE.split('.')) + if profile_mod is None: + raise SiteProfileNotAvailable + return profile_mod + + +def get_profile_form(): + """ + Return a form class (a subclass of the default ``ModelForm``) + suitable for creating/editing instances of the site-specific user + profile model, as defined by the ``AUTH_PROFILE_MODULE`` + setting. If that setting is missing, raise + ``django.contrib.auth.models.SiteProfileNotAvailable``. + + """ + profile_mod = get_profile_model() + class _ProfileForm(forms.ModelForm): + class Meta: + model = profile_mod + exclude = ('user',) # User will be filled in by the view. + return _ProfileForm diff --git a/readthedocs/profiles/views.py b/readthedocs/profiles/views.py new file mode 100644 index 000000000..732990105 --- /dev/null +++ b/readthedocs/profiles/views.py @@ -0,0 +1,343 @@ +""" +Views for creating, editing and viewing site-specific user profiles. + +""" + +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.http import Http404 +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.views.generic.list import ListView + +from profiles import utils + + +def create_profile(request, form_class=None, success_url=None, + template_name='profiles/create_profile.html', + extra_context=None): + """ + Create a profile for the current user, if one doesn't already + exist. + + If the user already has a profile, as determined by + ``request.user.get_profile()``, a redirect will be issued to the + :view:`profiles.views.edit_profile` view. If no profile model has + been specified in the ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``form_class`` + The form class to use for validating and creating the user + profile. This form class must define a method named + ``save()``, implementing the same argument signature as the + ``save()`` method of a standard Django ``ModelForm`` (this + view will call ``save(commit=False)`` to obtain the profile + object, and fill in the user before the final save). If the + profile object includes many-to-many relations, the convention + established by ``ModelForm`` of using a method named + ``save_m2m()`` will be used, and so your form class should + also define this method. + + If this argument is not supplied, this view will use a + ``ModelForm`` automatically generated from the model specified + by ``AUTH_PROFILE_MODULE``. + + ``success_url`` + The URL to redirect to after successful profile creation. If + this argument is not supplied, this will default to the URL of + :view:`profiles.views.profile_detail` for the newly-created + profile object. + + ``template_name`` + The template to use when displaying the profile-creation + form. If not supplied, this will default to + :template:`profiles/create_profile.html`. + + **Context:** + + ``form`` + The profile-creation form. + + **Template:** + + ``template_name`` keyword argument, or + :template:`profiles/create_profile.html`. + + """ + try: + profile_obj = request.user.get_profile() + return HttpResponseRedirect(reverse('profiles_edit_profile')) + except ObjectDoesNotExist: + pass + + # + # We set up success_url here, rather than as the default value for + # the argument. Trying to do it as the argument's default would + # mean evaluating the call to reverse() at the time this module is + # first imported, which introduces a circular dependency: to + # perform the reverse lookup we need access to profiles/urls.py, + # but profiles/urls.py in turn imports this module. + # + + if success_url is None: + success_url = reverse('profiles_profile_detail', + kwargs={ 'username': request.user.username }) + if form_class is None: + form_class = utils.get_profile_form() + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES) + if form.is_valid(): + profile_obj = form.save(commit=False) + profile_obj.user = request.user + profile_obj.save() + if hasattr(form, 'save_m2m'): + form.save_m2m() + return HttpResponseRedirect(success_url) + else: + form = form_class() + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'form': form }, + context_instance=context) +create_profile = login_required(create_profile) + +def edit_profile(request, form_class=None, success_url=None, + template_name='profiles/edit_profile.html', + extra_context=None): + """ + Edit the current user's profile. + + If the user does not already have a profile (as determined by + ``User.get_profile()``), a redirect will be issued to the + :view:`profiles.views.create_profile` view; if no profile model + has been specified in the ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``form_class`` + The form class to use for validating and editing the user + profile. This form class must operate similarly to a standard + Django ``ModelForm`` in that it must accept an instance of the + object to be edited as the keyword argument ``instance`` to + its constructor, and it must implement a method named + ``save()`` which will save the updates to the object. If this + argument is not specified, this view will use a ``ModelForm`` + generated from the model specified in the + ``AUTH_PROFILE_MODULE`` setting. + + ``success_url`` + The URL to redirect to following a successful edit. If not + specified, this will default to the URL of + :view:`profiles.views.profile_detail` for the profile object + being edited. + + ``template_name`` + The template to use when displaying the profile-editing + form. If not specified, this will default to + :template:`profiles/edit_profile.html`. + + **Context:** + + ``form`` + The form for editing the profile. + + ``profile`` + The user's current profile. + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/edit_profile.html`. + + """ + try: + profile_obj = request.user.get_profile() + except ObjectDoesNotExist: + return HttpResponseRedirect(reverse('profiles_create_profile')) + + # + # See the comment in create_profile() for discussion of why + # success_url is set up here, rather than as a default value for + # the argument. + # + + if success_url is None: + success_url = reverse('profiles_profile_detail', + kwargs={ 'username': request.user.username }) + if form_class is None: + form_class = utils.get_profile_form() + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES, instance=profile_obj) + if form.is_valid(): + form.save() + return HttpResponseRedirect(success_url) + else: + form = form_class(instance=profile_obj) + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'form': form, + 'profile': profile_obj, }, + context_instance=context) +edit_profile = login_required(edit_profile) + +def profile_detail(request, username, public_profile_field=None, + template_name='profiles/profile_detail.html', + extra_context=None): + """ + Detail view of a user's profile. + + If no profile model has been specified in the + ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + If the user has not yet created a profile, ``Http404`` will be + raised. + + **Required arguments:** + + ``username`` + The username of the user whose profile is being displayed. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``public_profile_field`` + The name of a ``BooleanField`` on the profile model; if the + value of that field on the user's profile is ``False``, the + ``profile`` variable in the template will be ``None``. Use + this feature to allow users to mark their profiles as not + being publicly viewable. + + If this argument is not specified, it will be assumed that all + users' profiles are publicly viewable. + + ``template_name`` + The name of the template to use for displaying the profile. If + not specified, this will default to + :template:`profiles/profile_detail.html`. + + **Context:** + + ``profile`` + The user's profile, or ``None`` if the user's profile is not + publicly viewable (see the description of + ``public_profile_field`` above). + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/profile_detail.html`. + + """ + user = get_object_or_404(User, username=username) + try: + profile_obj = user.get_profile() + except ObjectDoesNotExist: + raise Http404 + if public_profile_field is not None and \ + not getattr(profile_obj, public_profile_field): + profile_obj = None + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'profile': profile_obj }, + context_instance=context) + + +class ProfileListView(ListView): + """ + A list of user profiles. + + If no profile model has been specified in the + ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``public_profile_field`` + The name of a ``BooleanField`` on the profile model; if the + value of that field on a user's profile is ``False``, that + profile will be excluded from the list. Use this feature to + allow users to mark their profiles as not being publicly + viewable. + + If this argument is not specified, it will be assumed that all + users' profiles are publicly viewable. + + ``template_name`` + The name of the template to use for displaying the profiles. If + not specified, this will default to + :template:`profiles/profile_list.html`. + + Additionally, all arguments accepted by the + :view:`django.views.generic.list_detail.object_list` generic view + will be accepted here, and applied in the same fashion, with one + exception: ``queryset`` will always be the ``QuerySet`` of the + model specified by the ``AUTH_PROFILE_MODULE`` setting, optionally + filtered to remove non-publicly-viewable proiles. + + **Context:** + + Same as the :view:`django.views.generic.list_detail.object_list` + generic view. + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/profile_list.html`. + + """ + + public_profile_field = None + template_name = 'profiles/profile_list.html' + + def get_model(self): + return utils.get_profile_model() + + def get_queryset(self): + queryset = self.get_model()._default_manager.all() + if self.public_profile_field is not None: + queryset = queryset.filter(**{self.public_profile_field: True}) + return queryset diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 910230643..861c5a1ff 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -111,11 +111,9 @@ class Project(models.Model): repo_type = models.CharField(_('Repository type'), max_length=10, choices=constants.REPO_CHOICES, default='git') project_url = models.URLField(_('Project URL'), blank=True, - help_text=_('The project\'s homepage'), - verify_exists=False) + help_text=_('The project\'s homepage')) canonical_url = models.URLField(_('Canonical URL'), blank=True, - help_text=_('The official URL that the docs live at. This can be at readthedocs.org, or somewhere else. Ex. http://docs.fabfile.org'), - verify_exists=False) + help_text=_('The official URL that the docs live at. This can be at readthedocs.org, or somewhere else. Ex. http://docs.fabfile.org')) version = models.CharField(_('Version'), max_length=100, blank=True, help_text=_('Project version these docs apply ' 'to, i.e. 1.0a')) @@ -167,13 +165,15 @@ class Project(models.Model): help_text=_('Path from project root to conf.py file (ex. docs/conf.py)' '. Leave blank if you want us to find it for you.')) - featured = models.BooleanField(_('Featured')) - skip = models.BooleanField(_('Skip')) + featured = models.BooleanField(_('Featured'), default=False) + skip = models.BooleanField(_('Skip'), default=False) mirror = models.BooleanField(_('Mirror'), default=False) use_virtualenv = models.BooleanField( _('Use virtualenv'), help_text=_("Install your project inside a virtualenv using setup.py " - "install")) + "install"), + default=False + ) # This model attribute holds the python interpreter used to create the # virtual environment @@ -188,7 +188,9 @@ class Project(models.Model): use_system_packages = models.BooleanField( _('Use system packages'), help_text=_("Give the virtual environment access to the global " - "site-packages dir.")) + "site-packages dir."), + default=False + ) django_packages_url = models.CharField(_('Django Packages URL'), max_length=255, blank=True) privacy_level = models.CharField( @@ -218,9 +220,9 @@ class Project(models.Model): related_name='translations', blank=True, null=True) - # Version State + # Version State num_major = models.IntegerField( - _('Number of Major versions'), + _('Number of Major versions'), max_length=3, default=2, null=True, @@ -228,7 +230,7 @@ class Project(models.Model): help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X") ) num_minor = models.IntegerField( - _('Number of Minor versions'), + _('Number of Minor versions'), max_length=3, default=2, null=True, @@ -236,7 +238,7 @@ class Project(models.Model): help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X") ) num_point = models.IntegerField( - _('Number of Point versions'), + _('Number of Point versions'), max_length=3, default=2, null=True, @@ -481,7 +483,7 @@ class Project(models.Model): # # Paths for symlinks in project doc_path. - # + # def cnames_symlink_path(self, domain): """ Path in the doc_path that we symlink cnames @@ -587,7 +589,7 @@ class Project(models.Model): The path to the static metadata JSON settings file """ return os.path.join(self.doc_path, 'metadata.json') - + def conf_file(self, version='latest'): if self.conf_py_file: log.debug('Inserting conf.py file path from model') @@ -840,7 +842,7 @@ class EmailHook(Notification): class WebHook(Notification): - url = models.URLField(blank=True, verify_exists=False, + url = models.URLField(blank=True, help_text=_('URL to send the webhook to')) def __unicode__(self): diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 9905ce4b4..857cf70d6 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -130,9 +130,12 @@ def move_files(version, results): core_utils.copy(from_path, to_path) # Always move PDF's because the return code lies. if 'pdf' in results: - from_path = version.project.artifact_path(version=version.slug, type='sphinx_pdf') - to_path = os.path.join(settings.MEDIA_ROOT, 'pdf', version.project.slug, version.slug) - core_utils.copy(from_path, to_path) + try: + from_path = version.project.artifact_path(version=version.slug, type='sphinx_pdf') + to_path = os.path.join(settings.MEDIA_ROOT, 'pdf', version.project.slug, version.slug) + core_utils.copy(from_path, to_path) + except: + pass if 'epub' in results and results['epub'][0] == 0: from_path = version.project.artifact_path(version=version.slug, type='sphinx_epub') to_path = os.path.join(settings.MEDIA_ROOT, 'epub', version.project.slug, version.slug) @@ -549,6 +552,7 @@ def create_build(version, api, record): version='/api/v1/version/%s/' % version.pk, type='html', state='triggered', + success=True, )) else: build = {} diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py index 4c06c2584..f46230edd 100644 --- a/readthedocs/projects/urls/private.py +++ b/readthedocs/projects/urls/private.py @@ -1,81 +1,83 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url + +from projects.views.private import AliasList, ProjectDashboard urlpatterns = patterns( # base view, flake8 complains if it is on the previous line. - 'projects.views.private', + '', url(r'^$', - 'project_dashboard', + ProjectDashboard.as_view(), name='projects_dashboard'), url(r'^import/$', - 'project_import', + 'projects.views.private.project_import', name='projects_import'), url(r'^(?P[-\w]+)/$', - 'project_manage', + 'projects.views.private.project_manage', name='projects_manage'), url(r'^(?P[-\w]+)/alias/(?P\d+)/', - 'edit_alias', + 'projects.views.private.edit_alias', name='projects_alias_edit'), url(r'^(?P[-\w]+)/alias/$', - 'edit_alias', + 'projects.views.private.edit_alias', name='projects_alias_create'), url(r'^(?P[-\w]+)/alias/list/$', - 'list_alias', + AliasList.as_view(), name='projects_alias_list'), url(r'^(?P[-\w]+)/edit/$', - 'project_edit', + 'projects.views.private.project_edit', name='projects_edit'), url(r'^(?P[-\w]+)/advanced/$', - 'project_advanced', + 'projects.views.private.project_advanced', name='projects_advanced'), url(r'^(?P[-\w]+)/version/(?P[-\w.]+)/$', - 'project_version_detail', + 'projects.views.private.project_version_detail', name='project_version_detail'), url(r'^(?P[-\w]+)/versions/$', - 'project_versions', + 'projects.views.private.project_versions', name='projects_versions'), url(r'^(?P[-\w]+)/delete/$', - 'project_delete', + 'projects.views.private.project_delete', name='projects_delete'), url(r'^(?P[-\w]+)/subprojects/delete/(?P[-\w]+)/$', # noqa - 'project_subprojects_delete', + 'projects.views.private.project_subprojects_delete', name='projects_subprojects_delete'), url(r'^(?P[-\w]+)/subprojects/$', - 'project_subprojects', + 'projects.views.private.project_subprojects', name='projects_subprojects'), url(r'^(?P[-\w]+)/users/$', - 'project_users', + 'projects.views.private.project_users', name='projects_users'), url(r'^(?P[-\w]+)/users/delete/$', - 'project_users_delete', + 'projects.views.private.project_users_delete', name='projects_users_delete'), url(r'^(?P[-\w]+)/notifications/$', - 'project_notifications', + 'projects.views.private.project_notifications', name='projects_notifications'), url(r'^(?P[-\w]+)/notifications/delete/$', - 'project_notifications_delete', + 'projects.views.private.project_notifications_delete', name='projects_notification_delete'), url(r'^(?P[-\w]+)/translations/$', - 'project_translations', + 'projects.views.private.project_translations', name='projects_translations'), url(r'^(?P[-\w]+)/translations/delete/(?P[-\w]+)/$', # noqa - 'project_translations_delete', + 'projects.views.private.project_translations_delete', name='projects_translations_delete'), ) diff --git a/readthedocs/projects/urls/public.py b/readthedocs/projects/urls/public.py index dac70fd1c..e5caca8f0 100644 --- a/readthedocs/projects/urls/public.py +++ b/readthedocs/projects/urls/public.py @@ -1,49 +1,43 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url + +from projects.views.public import ProjectIndex urlpatterns = patterns( # base view, flake8 complains if it is on the previous line. - 'projects.views.public', + '', url(r'^$', - 'project_index', + ProjectIndex.as_view(), name='projects_list'), - url(r'^tags/$', - 'tag_index', - name='projects_tag_list'), - - url(r'^search/$', - 'search', - name='project_search'), - url(r'^search/autocomplete/$', - 'search_autocomplete', + 'projects.views.public.search_autocomplete', name='search_autocomplete'), url(r'^autocomplete/version/(?P[-\w]+)/$', - 'version_autocomplete', + 'projects.views.public.version_autocomplete', name='version_autocomplete'), url(r'^autocomplete/filter/version/(?P[-\w]+)/$', - 'version_filter_autocomplete', + 'projects.views.public.version_filter_autocomplete', name='version_filter_autocomplete'), url(r'^tags/(?P[-\w]+)/$', - 'project_index', + ProjectIndex.as_view(), name='projects_tag_detail'), url(r'^(?P[-\w]+)/$', - 'project_detail', + 'projects.views.public.project_detail', name='projects_detail'), url(r'^(?P[-\w]+)/downloads/$', - 'project_downloads', + 'projects.views.public.project_downloads', name='project_downloads'), url(r'^(?P[-\w]+)/badge/$', - 'project_badge', + 'projects.views.public.project_badge', name='project_badge'), url(r'^(?P\w+)/$', - 'project_index', + 'projects.views.public.project_index', name='projects_user_list'), ) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 1e470fee1..c661d63c3 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -9,13 +9,14 @@ from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404, render_to_response from django.template import RequestContext -from django.views.generic.list_detail import object_list +from django.views.generic import ListView +from django.utils.decorators import method_decorator from guardian.shortcuts import assign from builds.forms import AliasForm, VersionForm from builds.filters import VersionFilter -from builds.models import Version +from builds.models import VersionAlias, Version from projects.forms import (ImportProjectForm, build_versions_form, build_upload_html_form, SubprojectForm, UserForm, EmailHookForm, TranslationForm, @@ -24,25 +25,28 @@ from projects.models import Project, EmailHook from projects import constants -@login_required -def project_dashboard(request): +class ProjectDashboard(ListView): """ A dashboard! If you aint know what that means you aint need to. Essentially we show you an overview of your content. """ - qs = (Version.objects.active(user=request.user) - .filter(project__users__in=[request.user])) - filter = VersionFilter(constants.IMPORTANT_VERSION_FILTERS, queryset=qs) - return object_list( - request, - queryset=request.user.projects.live(), - page=int(request.GET.get('page', 1)), - template_object_name='project', - template_name='projects/project_dashboard.html', - extra_context={ - 'filter': filter, - } - ) + model = Project + template_name='projects/project_dashboard.html' + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(ProjectDashboard, self).dispatch(*args, **kwargs) + + def get_queryset(self): + return self.request.user.projects.live() + + def get_context_data(self, **kwargs): + context = super(ProjectDashboard, self).get_context_data(**kwargs) + qs = (Version.objects.active(user=self.request.user) + .filter(project__users__in=[self.request.user])) + filter = VersionFilter(constants.IMPORTANT_VERSION_FILTERS, queryset=self.get_queryset()) + context['filter'] = filter + return context @login_required @@ -216,15 +220,18 @@ def edit_alias(request, project_slug, id=None): ) -@login_required -def list_alias(request, project_slug): - proj = get_object_or_404(Project.objects.all(), slug=project_slug) - return object_list( - request, - queryset=proj.aliases.all(), - template_object_name='alias', - template_name='projects/alias_list.html', - ) +class AliasList(ListView): + model = VersionAlias + template_context_name = 'alias' + template_name='projects/alias_list.html', + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(AliasList, self).dispatch(*args, **kwargs) + + def get_queryset(self): + self.project = get_object_or_404(Project.objects.all(), slug=self.kwargs.get('project_slug')) + return self.project.aliases.all() @login_required diff --git a/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py index 9519eb3ae..a1a46084f 100644 --- a/readthedocs/projects/views/public.py +++ b/readthedocs/projects/views/public.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404, render_to_response from django.template import RequestContext -from django.views.generic.list_detail import object_list +from django.views.generic import ListView from django.utils.datastructures import SortedDict from taggit.models import Tag @@ -16,32 +16,32 @@ from builds.models import Version from projects.models import Project -def project_index(request, username=None, tag=None): - """ - The list of projects, which will optionally filter by user or tag, - in which case a 'person' or 'tag' will be added to the context - """ - queryset = Project.objects.public(request.user) - if username: - user = get_object_or_404(User, username=username) - queryset = queryset.filter(user=user) - else: - user = None +class ProjectIndex(ListView): + model = Project - if tag: - tag = get_object_or_404(Tag, slug=tag) - queryset = queryset.filter(tags__name__in=[tag.slug]) - else: - tag = None + def get_queryset(self): + queryset = Project.objects.public(self.request.user) + if self.kwargs.get('username'): + self.user = get_object_or_404(User, username=self.kwargs.get('username')) + queryset = queryset.filter(user=self.user) + else: + self.user = None - return object_list( - request, - queryset=queryset, - extra_context={'person': user, 'tag': tag}, - page=int(request.GET.get('page', 1)), - template_object_name='project', - ) + if self.kwargs.get('tag'): + self.tag = get_object_or_404(Tag, slug=self.kwargs.get('tag')) + queryset = queryset.filter(tags__name__in=[self.tag.slug]) + else: + self.tag = None + return queryset + + def get_context_data(self, **kwargs): + context = super(ProjectIndex, self).get_context_data(**kwargs) + context['person'] = self.user + context['tag'] = self.tag + return context + +project_index = ProjectIndex.as_view() def project_detail(request, project_slug): """ @@ -118,41 +118,6 @@ def project_downloads(request, project_slug): ) -def tag_index(request): - """ - List of all tags by most common - """ - tag_qs = Project.tags.most_common() - return object_list( - request, - queryset=tag_qs, - page=int(request.GET.get('page', 1)), - template_object_name='tag', - template_name='projects/tag_list.html', - ) - - -def search(request): - """ - our ghetto site search. see roadmap. - """ - if 'q' in request.GET: - term = request.GET['q'] - else: - raise Http404 - queryset = Project.objects.live(name__icontains=term) - if queryset.count() == 1: - return HttpResponseRedirect(queryset[0].get_absolute_url()) - - return object_list( - request, - queryset=queryset, - template_object_name='term', - extra_context={'term': term}, - template_name='projects/search.html', - ) - - def search_autocomplete(request): """ return a json list of project names diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py index e8f013efa..eae90a9ea 100644 --- a/readthedocs/restapi/urls.py +++ b/readthedocs/restapi/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import url, patterns, include +from django.conf.urls import url, patterns, include from rest_framework import routers diff --git a/readthedocs/rtd_tests/tests/test_backend.py b/readthedocs/rtd_tests/tests/test_backend.py index 4ef6a67e3..a345a06a5 100644 --- a/readthedocs/rtd_tests/tests/test_backend.py +++ b/readthedocs/rtd_tests/tests/test_backend.py @@ -1,6 +1,6 @@ from os.path import exists -from django.contrib.admin.models import User +from django.contrib.auth.models import User from projects.models import Project from rtd_tests.base import RTDTestCase diff --git a/readthedocs/rtd_tests/tests/test_celery.py b/readthedocs/rtd_tests/tests/test_celery.py index 9cd4b8ed0..92e025ef9 100644 --- a/readthedocs/rtd_tests/tests/test_celery.py +++ b/readthedocs/rtd_tests/tests/test_celery.py @@ -1,7 +1,7 @@ from os.path import exists import shutil from tempfile import mkdtemp -from django.contrib.admin.models import User +from django.contrib.auth.models import User import json from projects.models import Project diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py index 0f58e98c4..1da115519 100644 --- a/readthedocs/rtd_tests/tests/test_doc_building.py +++ b/readthedocs/rtd_tests/tests/test_doc_building.py @@ -1,6 +1,6 @@ import shutil -from django.contrib.admin.models import User +from django.contrib.auth.models import User from projects.models import Project diff --git a/readthedocs/rtd_tests/tests/test_privacy.py b/readthedocs/rtd_tests/tests/test_privacy.py index 1c4fd3545..dad55408c 100644 --- a/readthedocs/rtd_tests/tests/test_privacy.py +++ b/readthedocs/rtd_tests/tests/test_privacy.py @@ -53,7 +53,6 @@ class PrivacyTests(TestCase): 'num_minor': 2, 'num_major': 2, 'num_point': 2, 'version_privacy_level': version_privacy_level, 'documentation_type': 'sphinx'}) - #import ipdb; ipdb.set_trace() self.assertAlmostEqual(Project.objects.count(), 1) r = self.client.get('/projects/django-kong/') diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 0a0d8667a..f0ec4900d 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -100,6 +100,16 @@ MIDDLEWARE_CLASSES = ( #'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', ) +AUTHENTICATION_BACKENDS = ( + # Needed to login by username in Django admin, regardless of `allauth` + "django.contrib.auth.backends.ModelBackend", + # `allauth` specific authentication methods, such as login by e-mail + "allauth.account.auth_backends.AuthenticationBackend", +) + +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "mandatory" + CORS_ORIGIN_REGEX_WHITELIST = ('^http://(.+)\.readthedocs\.org$', '^https://(.+)\.readthedocs\.org$') # So people can post to their accounts CORS_ALLOW_CREDENTIALS = True @@ -115,25 +125,26 @@ TEMPLATE_CONTEXT_PROCESSORS = ( "django.core.context_processors.i18n", "django.core.context_processors.media", "django.core.context_processors.request", + # Read the Docs processor "core.context_processors.readthedocs_processor", + # allauth specific context processors + "allauth.account.context_processors.account", + "allauth.socialaccount.context_processors.socialaccount", ) INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.admin', 'django.contrib.contenttypes', - 'django.contrib.markup', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.staticfiles', # third party apps 'pagination', - 'registration', 'profiles', 'taggit', 'south', - 'basic.flagging', 'djangosecure', 'guardian', 'django_gravatar', @@ -149,16 +160,29 @@ INSTALLED_APPS = [ 'haystack', 'tastypie', + + # our apps 'projects', 'builds', 'core', 'doc_builder', 'rtd_tests', - 'websupport', 'restapi', + + # allauth + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + #'allauth.socialaccount.providers.github', + #'allauth.socialaccount.providers.bitbucket', + #'allauth.socialaccount.providers.twitter', ] +SOUTH_MIGRATION_MODULES = { + 'taggit': 'taggit.south_migrations', +} + REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), @@ -175,6 +199,7 @@ CELERY_SEND_TASK_ERROR_EMAILS = False CELERYD_HIJACK_ROOT_LOGGER = False # Don't queue a bunch of tasks in the workers CELERYD_PREFETCH_MULTIPLIER = 1 +HAYSTACK_SIGNAL_PROCESSOR = 'celery_haystack.signals.CelerySignalProcessor' CELERY_ROUTES = { 'celery_haystack.tasks.CeleryHaystackSignalHandler': { @@ -236,6 +261,11 @@ LOGGING = { 'datefmt': "%d/%b/%Y %H:%M:%S" }, }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, 'handlers': { 'null': { 'level': 'DEBUG', @@ -291,6 +321,7 @@ LOGGING = { }, 'mail_admins': { 'level': 'ERROR', + 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler', }, 'console': { diff --git a/readthedocs/templates/base.html b/readthedocs/templates/base.html index 6c661d06c..58e8a3a75 100644 --- a/readthedocs/templates/base.html +++ b/readthedocs/templates/base.html @@ -10,7 +10,7 @@ - {% block title %}{% endblock %} | {% block branding %}{% trans "Read the Docs" %} {% endblock %} + {% block title %}{% endblock %}{% block head_title %}{% endblock %} | {% block branding %}{% trans "Read the Docs" %} {% endblock %} @@ -146,7 +146,7 @@ })(); $('.rtfd-header-search input:text').autocomplete({ - source: '{% url search_autocomplete %}', + source: '{% url "search_autocomplete" %}', minLength: 2, open: function(event, ui) { ac_top = $('.ui-autocomplete').css('top'); diff --git a/readthedocs/templates/builds/build_list.html b/readthedocs/templates/builds/build_list.html index b72dd68f6..7b01268d2 100644 --- a/readthedocs/templates/builds/build_list.html +++ b/readthedocs/templates/builds/build_list.html @@ -30,7 +30,7 @@
-
+
  • diff --git a/readthedocs/templates/builds/tag_list.html b/readthedocs/templates/builds/tag_list.html index 5e3f72d75..78ddbde72 100644 --- a/readthedocs/templates/builds/tag_list.html +++ b/readthedocs/templates/builds/tag_list.html @@ -7,7 +7,7 @@ {% block content %} {% endblock %} diff --git a/readthedocs/templates/core/bookmark_list_detailed.html b/readthedocs/templates/core/bookmark_list_detailed.html index ee47c4bc1..e0d335ad2 100644 --- a/readthedocs/templates/core/bookmark_list_detailed.html +++ b/readthedocs/templates/core/bookmark_list_detailed.html @@ -1,7 +1,7 @@ {% for bookmark in bookmark_list %}
  • {{ bookmark.url }} - ({{ bookmark.user }}) + ({{ bookmark.user }}) {% if bookmark.project.version %}version {{ bookmark.project.version }} -{% endif %} {{ bookmark.date|timesince }} ago
  • {% endfor %} diff --git a/readthedocs/templates/core/header.html b/readthedocs/templates/core/header.html index 1f01fbcd2..e8539b1b4 100644 --- a/readthedocs/templates/core/header.html +++ b/readthedocs/templates/core/header.html @@ -7,14 +7,14 @@

    {% comment %}Translators: Name of the website{% endcomment %} - {% trans "Read the Docs" %} + {% trans "Read the Docs" %}