650 lines
23 KiB
Python
650 lines
23 KiB
Python
"""Project views for authenticated users"""
|
|
|
|
import logging
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.models import User
|
|
from django.contrib import messages
|
|
from django.core.urlresolvers import reverse
|
|
from django.conf import settings
|
|
from django.http import (HttpResponseRedirect, HttpResponseNotAllowed,
|
|
Http404, HttpResponseBadRequest)
|
|
from django.shortcuts import get_object_or_404, render_to_response, render
|
|
from django.template import RequestContext
|
|
from django.views.generic import View, TemplateView, ListView
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.middleware.csrf import get_token
|
|
from formtools.wizard.views import SessionWizardView
|
|
from allauth.socialaccount.models import SocialAccount
|
|
|
|
from vanilla import CreateView, DeleteView, UpdateView
|
|
|
|
from readthedocs.bookmarks.models import Bookmark
|
|
from readthedocs.builds.models import Version
|
|
from readthedocs.builds.forms import AliasForm, VersionForm
|
|
from readthedocs.builds.filters import VersionFilter
|
|
from readthedocs.builds.models import VersionAlias
|
|
from readthedocs.core.utils import trigger_build
|
|
from readthedocs.core.mixins import ListViewWithForm
|
|
from readthedocs.projects.forms import (
|
|
ProjectBasicsForm, ProjectExtraForm,
|
|
ProjectAdvancedForm, UpdateProjectForm, SubprojectForm,
|
|
build_versions_form, UserForm, EmailHookForm, TranslationForm,
|
|
RedirectForm, WebHookForm, DomainForm)
|
|
from readthedocs.projects.models import Project, EmailHook, WebHook, Domain
|
|
from readthedocs.projects.views.base import ProjectAdminMixin, ProjectSpamMixin
|
|
from readthedocs.projects import constants, tasks
|
|
from readthedocs.projects.exceptions import ProjectSpamError
|
|
from readthedocs.projects.tasks import remove_dir, clear_artifacts
|
|
|
|
from readthedocs.core.mixins import LoginRequiredMixin
|
|
from readthedocs.projects.signals import project_import
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class PrivateViewMixin(LoginRequiredMixin):
|
|
pass
|
|
|
|
|
|
class ProjectDashboard(PrivateViewMixin, ListView):
|
|
|
|
"""Project dashboard"""
|
|
|
|
model = Project
|
|
template_name = 'projects/project_dashboard.html'
|
|
|
|
def get_queryset(self):
|
|
return Project.objects.dashboard(self.request.user)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ProjectDashboard, self).get_context_data(**kwargs)
|
|
version_filter = VersionFilter(constants.IMPORTANT_VERSION_FILTERS,
|
|
queryset=self.get_queryset())
|
|
context['filter'] = version_filter
|
|
|
|
bookmarks = Bookmark.objects.filter(user=self.request.user)
|
|
|
|
if bookmarks.exists:
|
|
context['bookmark_list'] = bookmarks[:3]
|
|
else:
|
|
bookmarks = None
|
|
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def project_manage(__, project_slug):
|
|
"""Project management view
|
|
|
|
Where you will have links to edit the projects' configuration, edit the
|
|
files associated with that project, etc.
|
|
|
|
Now redirects to the normal /projects/<slug> view.
|
|
"""
|
|
return HttpResponseRedirect(reverse('projects_detail',
|
|
args=[project_slug]))
|
|
|
|
|
|
@login_required
|
|
def project_comments_moderation(request, project_slug):
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
return render(
|
|
request,
|
|
'projects/project_comments_moderation.html',
|
|
{'project': project})
|
|
|
|
|
|
class ProjectUpdate(ProjectSpamMixin, PrivateViewMixin, UpdateView):
|
|
|
|
form_class = UpdateProjectForm
|
|
model = Project
|
|
success_message = _('Project settings updated')
|
|
template_name = 'projects/project_edit.html'
|
|
lookup_url_kwarg = 'project_slug'
|
|
lookup_field = 'slug'
|
|
|
|
def get_queryset(self):
|
|
return self.model.objects.for_admin_user(self.request.user)
|
|
|
|
def get_success_url(self):
|
|
return reverse('projects_detail', args=[self.object.slug])
|
|
|
|
|
|
class ProjectAdvancedUpdate(ProjectSpamMixin, PrivateViewMixin, UpdateView):
|
|
|
|
form_class = ProjectAdvancedForm
|
|
model = Project
|
|
success_message = _('Project settings updated')
|
|
template_name = 'projects/project_advanced.html'
|
|
lookup_url_kwarg = 'project_slug'
|
|
lookup_field = 'slug'
|
|
initial = {'num_minor': 2, 'num_major': 2, 'num_point': 2}
|
|
|
|
def get_queryset(self):
|
|
return self.model.objects.for_admin_user(self.request.user)
|
|
|
|
def get_success_url(self):
|
|
return reverse('projects_detail', args=[self.object.slug])
|
|
|
|
|
|
@login_required
|
|
def project_versions(request, project_slug):
|
|
"""Project versions view
|
|
|
|
Shows the available versions and lets the user choose which ones he would
|
|
like to have built.
|
|
"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
if not project.is_imported:
|
|
raise Http404
|
|
|
|
form_class = build_versions_form(project)
|
|
|
|
form = form_class(data=request.POST or None)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
form.save()
|
|
messages.success(request, _('Project versions updated'))
|
|
project_dashboard = reverse('projects_detail', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
return render_to_response(
|
|
'projects/project_versions.html',
|
|
{'form': form, 'project': project},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_version_detail(request, project_slug, version_slug):
|
|
"""Project version detail page"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
version = get_object_or_404(
|
|
Version.objects.public(user=request.user, project=project, only_active=False),
|
|
slug=version_slug)
|
|
|
|
form = VersionForm(request.POST or None, instance=version)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
version = form.save()
|
|
if form.has_changed():
|
|
if 'active' in form.changed_data and version.active is False:
|
|
log.info('Removing files for version %s' % version.slug)
|
|
clear_artifacts.delay(version_pk=version.pk)
|
|
version.built = False
|
|
version.save()
|
|
url = reverse('project_version_list', args=[project.slug])
|
|
return HttpResponseRedirect(url)
|
|
|
|
return render_to_response(
|
|
'projects/project_version_detail.html',
|
|
{'form': form, 'project': project, 'version': version},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_delete(request, project_slug):
|
|
"""Project delete confirmation view
|
|
|
|
Make a project as deleted on POST, otherwise show a form asking for
|
|
confirmation of delete.
|
|
"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
if request.method == 'POST':
|
|
# Support hacky "broadcast" with MULTIPLE_APP_SERVERS setting,
|
|
# otherwise put in normal celery queue
|
|
for server in getattr(settings, "MULTIPLE_APP_SERVERS", ['celery']):
|
|
log.info('Removing files on %s' % server)
|
|
remove_dir.apply_async(
|
|
args=[project.doc_path],
|
|
queue=server,
|
|
)
|
|
|
|
# Delete the project and everything related to it
|
|
project.delete()
|
|
messages.success(request, _('Project deleted'))
|
|
project_dashboard = reverse('projects_dashboard')
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
return render_to_response(
|
|
'projects/project_delete.html',
|
|
{'project': project},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
|
|
|
|
"""Project import wizard"""
|
|
|
|
form_list = [('basics', ProjectBasicsForm),
|
|
('extra', ProjectExtraForm)]
|
|
condition_dict = {'extra': lambda self: self.is_advanced()}
|
|
|
|
def get_form_kwargs(self, step=None):
|
|
"""Get args to pass into form instantiation"""
|
|
kwargs = {}
|
|
kwargs['user'] = self.request.user
|
|
if step == 'basics':
|
|
kwargs['show_advanced'] = True
|
|
if step == 'extra':
|
|
extra_form = self.get_form_from_step('basics')
|
|
project = extra_form.save(commit=False)
|
|
kwargs['instance'] = project
|
|
return kwargs
|
|
|
|
def get_form_from_step(self, step):
|
|
form = self.form_list[step](
|
|
data=self.get_cleaned_data_for_step(step),
|
|
**self.get_form_kwargs(step)
|
|
)
|
|
form.full_clean()
|
|
return form
|
|
|
|
def get_template_names(self):
|
|
"""Return template names based on step name"""
|
|
return 'projects/import_{0}.html'.format(self.steps.current)
|
|
|
|
def done(self, form_list, **kwargs):
|
|
"""Save form data as object instance
|
|
|
|
Don't save form data directly, instead bypass documentation building and
|
|
other side effects for now, by signalling a save without commit. Then,
|
|
finish by added the members to the project and saving.
|
|
"""
|
|
# expect the first form
|
|
basics_form = form_list[0]
|
|
# Save the basics form to create the project instance, then alter
|
|
# attributes directly from other forms
|
|
project = basics_form.save()
|
|
for form in form_list[1:]:
|
|
for (field, value) in form.cleaned_data.items():
|
|
setattr(project, field, value)
|
|
basic_only = True
|
|
project.save()
|
|
project_import.send(sender=project, request=self.request)
|
|
trigger_build(project, basic=basic_only)
|
|
return HttpResponseRedirect(reverse('projects_detail',
|
|
args=[project.slug]))
|
|
|
|
def is_advanced(self):
|
|
"""Determine if the user selected the `show advanced` field"""
|
|
data = self.get_cleaned_data_for_step('basics') or {}
|
|
return data.get('advanced', True)
|
|
|
|
|
|
class ImportDemoView(PrivateViewMixin, View):
|
|
|
|
"""View to pass request on to import form to import demo project"""
|
|
|
|
form_class = ProjectBasicsForm
|
|
request = None
|
|
args = None
|
|
kwargs = None
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Process link request as a form post to the project import form"""
|
|
self.request = request
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
|
|
data = self.get_form_data()
|
|
project = (Project.objects.for_admin_user(request.user)
|
|
.filter(repo=data['repo']).first())
|
|
if project is not None:
|
|
messages.success(
|
|
request, _('The demo project is already imported!'))
|
|
else:
|
|
kwargs = self.get_form_kwargs()
|
|
form = self.form_class(data=data, **kwargs)
|
|
if form.is_valid():
|
|
project = form.save()
|
|
project.save()
|
|
trigger_build(project, basic=True)
|
|
messages.success(
|
|
request, _('Your demo project is currently being imported'))
|
|
else:
|
|
for (__, msg) in form.errors.items():
|
|
log.error(msg)
|
|
messages.error(request,
|
|
_('There was a problem adding the demo project'))
|
|
return HttpResponseRedirect(reverse('projects_dashboard'))
|
|
return HttpResponseRedirect(reverse('projects_detail',
|
|
args=[project.slug]))
|
|
|
|
def get_form_data(self):
|
|
"""Get form data to post to import form"""
|
|
return {
|
|
'name': '{0}-demo'.format(self.request.user.username),
|
|
'repo_type': 'git',
|
|
'repo': 'https://github.com/readthedocs/template.git'
|
|
}
|
|
|
|
def get_form_kwargs(self):
|
|
"""Form kwargs passed in during instantiation"""
|
|
return {'user': self.request.user}
|
|
|
|
|
|
class ImportView(PrivateViewMixin, TemplateView):
|
|
|
|
"""On GET, show the source an import view, on POST, mock out a wizard
|
|
|
|
If we are accepting POST data, use the fields to seed the initial data in
|
|
:py:cls:`ImportWizardView`. The import templates will redirect the form to
|
|
`/dashboard/import`
|
|
"""
|
|
|
|
template_name = 'projects/project_import.html'
|
|
wizard_class = ImportWizardView
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
initial_data = {}
|
|
initial_data['basics'] = {}
|
|
for key in ['name', 'repo', 'repo_type']:
|
|
initial_data['basics'][key] = request.POST.get(key)
|
|
initial_data['extra'] = {}
|
|
for key in ['description', 'project_url']:
|
|
initial_data['extra'][key] = request.POST.get(key)
|
|
request.method = 'GET'
|
|
return self.wizard_class.as_view(initial_dict=initial_data)(request)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ImportView, self).get_context_data(**kwargs)
|
|
context['view_csrf_token'] = get_token(self.request)
|
|
context['has_connected_accounts'] = (SocialAccount
|
|
.objects
|
|
.filter(user=self.request.user)
|
|
.exists())
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def edit_alias(request, project_slug, alias_id=None):
|
|
"""Edit project alias form view"""
|
|
proj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
if alias_id:
|
|
alias = proj.aliases.get(pk=alias_id)
|
|
form = AliasForm(instance=alias, data=request.POST or None)
|
|
else:
|
|
form = AliasForm(request.POST or None)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
alias = form.save()
|
|
return HttpResponseRedirect(alias.project.get_absolute_url())
|
|
return render_to_response(
|
|
'projects/alias_edit.html',
|
|
{'form': form},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
class AliasList(PrivateViewMixin, ListView):
|
|
model = VersionAlias
|
|
template_context_name = 'alias'
|
|
template_name = 'projects/alias_list.html',
|
|
|
|
def get_queryset(self):
|
|
self.project = get_object_or_404(
|
|
Project.objects.for_admin_user(self.request.user),
|
|
slug=self.kwargs.get('project_slug'))
|
|
return self.project.aliases.all()
|
|
|
|
|
|
@login_required
|
|
def project_subprojects(request, project_slug):
|
|
"""Project subprojects view and form view"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
form_kwargs = {
|
|
'parent': project,
|
|
'user': request.user,
|
|
}
|
|
if request.method == 'POST':
|
|
form = SubprojectForm(request.POST, **form_kwargs)
|
|
if form.is_valid():
|
|
form.save()
|
|
project_dashboard = reverse(
|
|
'projects_subprojects', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
else:
|
|
form = SubprojectForm(**form_kwargs)
|
|
|
|
subprojects = project.subprojects.all()
|
|
|
|
return render_to_response(
|
|
'projects/project_subprojects.html',
|
|
{'form': form, 'project': project, 'subprojects': subprojects},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_subprojects_delete(request, project_slug, child_slug):
|
|
parent = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
child = get_object_or_404(Project.objects.all(), slug=child_slug)
|
|
parent.remove_subproject(child)
|
|
return HttpResponseRedirect(reverse('projects_subprojects',
|
|
args=[parent.slug]))
|
|
|
|
|
|
@login_required
|
|
def project_users(request, project_slug):
|
|
"""Project users view and form view"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
form = UserForm(data=request.POST or None, project=project)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
form.save()
|
|
project_dashboard = reverse('projects_users', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
users = project.users.all()
|
|
|
|
return render_to_response(
|
|
'projects/project_users.html',
|
|
{'form': form, 'project': project, 'users': users},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_users_delete(request, project_slug):
|
|
if request.method != 'POST':
|
|
return HttpResponseNotAllowed('Only POST is allowed')
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
user = get_object_or_404(User.objects.all(), username=request.POST.get('username'))
|
|
if user == request.user:
|
|
raise Http404
|
|
project.users.remove(user)
|
|
project_dashboard = reverse('projects_users', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
|
|
@login_required
|
|
def project_notifications(request, project_slug):
|
|
"""Project notification view and form view"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
email_form = EmailHookForm(data=request.POST or None, project=project)
|
|
webhook_form = WebHookForm(data=request.POST or None, project=project)
|
|
|
|
if request.method == 'POST':
|
|
if email_form.is_valid():
|
|
email_form.save()
|
|
if webhook_form.is_valid():
|
|
webhook_form.save()
|
|
project_dashboard = reverse('projects_notifications',
|
|
args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
emails = project.emailhook_notifications.all()
|
|
urls = project.webhook_notifications.all()
|
|
|
|
return render_to_response(
|
|
'projects/project_notifications.html',
|
|
{
|
|
'email_form': email_form,
|
|
'webhook_form': webhook_form,
|
|
'project': project,
|
|
'emails': emails,
|
|
'urls': urls,
|
|
},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_comments_settings(request, project_slug):
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
return render_to_response(
|
|
'projects/project_comments_settings.html',
|
|
{
|
|
'project': project,
|
|
},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_notifications_delete(request, project_slug):
|
|
"""Project notifications delete confirmation view"""
|
|
if request.method != 'POST':
|
|
return HttpResponseNotAllowed('Only POST is allowed')
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
try:
|
|
project.emailhook_notifications.get(email=request.POST.get('email')).delete()
|
|
except EmailHook.DoesNotExist:
|
|
try:
|
|
project.webhook_notifications.get(url=request.POST.get('email')).delete()
|
|
except WebHook.DoesNotExist:
|
|
raise Http404
|
|
project_dashboard = reverse('projects_notifications', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
|
|
@login_required
|
|
def project_translations(request, project_slug):
|
|
"""Project translations view and form view"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
form = TranslationForm(data=request.POST or None, parent=project)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
form.save()
|
|
project_dashboard = reverse('projects_translations',
|
|
args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
lang_projects = project.translations.all()
|
|
|
|
return render_to_response(
|
|
'projects/project_translations.html',
|
|
{'form': form, 'project': project, 'lang_projects': lang_projects},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_translations_delete(request, project_slug, child_slug):
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
subproj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=child_slug)
|
|
project.translations.remove(subproj)
|
|
project_dashboard = reverse('projects_translations', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
|
|
@login_required
|
|
def project_redirects(request, project_slug):
|
|
"""Project redirects view and form view"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
|
|
form = RedirectForm(data=request.POST or None, project=project)
|
|
|
|
if request.method == 'POST' and form.is_valid():
|
|
form.save()
|
|
project_dashboard = reverse('projects_redirects', args=[project.slug])
|
|
return HttpResponseRedirect(project_dashboard)
|
|
|
|
redirects = project.redirects.all()
|
|
|
|
return render_to_response(
|
|
'projects/project_redirects.html',
|
|
{'form': form, 'project': project, 'redirects': redirects},
|
|
context_instance=RequestContext(request)
|
|
)
|
|
|
|
|
|
@login_required
|
|
def project_redirects_delete(request, project_slug):
|
|
"""Project redirect delete view"""
|
|
if request.method != 'POST':
|
|
return HttpResponseNotAllowed('Only POST is allowed')
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
|
slug=project_slug)
|
|
redirect = get_object_or_404(project.redirects,
|
|
pk=request.POST.get('id_pk'))
|
|
if redirect.project == project:
|
|
redirect.delete()
|
|
else:
|
|
raise Http404
|
|
return HttpResponseRedirect(reverse('projects_redirects',
|
|
args=[project.slug]))
|
|
|
|
|
|
@login_required
|
|
def project_version_delete_html(request, project_slug, version_slug):
|
|
"""Project version 'delete' HTML
|
|
|
|
This marks a version as not built
|
|
"""
|
|
project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
|
version = get_object_or_404(
|
|
Version.objects.public(user=request.user, project=project, only_active=False),
|
|
slug=version_slug)
|
|
|
|
if not version.active:
|
|
version.built = False
|
|
version.save()
|
|
tasks.clear_artifacts.delay(version.pk)
|
|
else:
|
|
return HttpResponseBadRequest("Can't delete HTML for an active version.")
|
|
return HttpResponseRedirect(
|
|
reverse('project_version_list', kwargs={'project_slug': project_slug}))
|
|
|
|
|
|
class DomainMixin(ProjectAdminMixin, PrivateViewMixin):
|
|
model = Domain
|
|
form_class = DomainForm
|
|
lookup_url_kwarg = 'domain_pk'
|
|
|
|
|
|
class DomainList(DomainMixin, ListViewWithForm):
|
|
pass
|
|
|
|
|
|
class DomainCreate(DomainMixin, CreateView):
|
|
pass
|
|
|
|
|
|
class DomainUpdate(DomainMixin, UpdateView):
|
|
pass
|
|
|
|
|
|
class DomainDelete(DomainMixin, DeleteView):
|
|
pass
|