Anna Sirota
caae613747
* removes all soft-deletion; * shows a "Delete extension" button on the draft page in case it can be deleted; * shows a "Delete version" button on the version page in case it can be deleted; * a version can be deleted if * its file isn't approved, and it doesn't have any ratings; * an extension can be deleted if * it's not listed, and doesn't have any ratings or abuse reports; * all it's versions can also be deleted; * changes default `File.status` from `APPROVED` to `AWAITING_REVIEW` With version's file status being `APPROVED` by default, a version can never be deleted, even when the extension is still a draft. This change doesn't affect the approval process because * when an extension is approved its latest version becomes approved automatically (no change here); * when a new version is uploaded to an approved extension, it's approved automatically (this is new). This allows authors to delete their drafts, freeing the extension slug and making it possible to re-upload the same file. This also makes it possible to easily fix mistakes during the drafting of a new extension (e.g. delete a version and re-upload it without bumping a version for each typo/mistake in packaging and so on). (see #78 and #63) Reviewed-on: #81
436 lines
15 KiB
Python
436 lines
15 KiB
Python
"""Contains views allowing developers to manage their add-ons."""
|
|
from django import forms
|
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
|
from django.contrib.messages.views import SuccessMessageMixin
|
|
from django.db import transaction
|
|
from django.shortcuts import get_object_or_404, reverse
|
|
from django.views.generic import DetailView, ListView
|
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
|
|
|
|
|
from .mixins import (
|
|
ExtensionQuerysetMixin,
|
|
OwnsFileMixin,
|
|
MaintainedExtensionMixin,
|
|
DraftVersionMixin,
|
|
DraftMixin,
|
|
)
|
|
from extensions.forms import (
|
|
EditPreviewFormSet,
|
|
AddPreviewFormSet,
|
|
ExtensionDeleteForm,
|
|
ExtensionUpdateForm,
|
|
VersionForm,
|
|
VersionDeleteForm,
|
|
)
|
|
from extensions.models import Extension, Version
|
|
from files.forms import FileForm
|
|
from files.models import File
|
|
from reviewers.models import ApprovalActivity
|
|
from stats.models import ExtensionView
|
|
import ratings.models
|
|
|
|
|
|
class ExtensionDetailView(ExtensionQuerysetMixin, DetailView):
|
|
model = Extension
|
|
context_object_name = 'extension'
|
|
template_name = 'extensions/detail.html'
|
|
|
|
def get_queryset(self):
|
|
"""Allow logged in users to view unlisted add-ons in certain conditions.
|
|
|
|
* maintainers should be able to preview their yet unlisted add-ons;
|
|
* staff should be able to preview yet unlisted add-ons;
|
|
"""
|
|
return self.get_extension_queryset()
|
|
|
|
def get_object(self, queryset=None):
|
|
"""Record a page view when returning the Extension object."""
|
|
obj = super().get_object(queryset=queryset)
|
|
if obj.is_listed and (
|
|
self.request.user.is_anonymous or not obj.has_maintainer(self.request.user)
|
|
):
|
|
ExtensionView.create_from_request(self.request, object_id=obj.pk)
|
|
return obj
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
if self.request.user.is_authenticated:
|
|
context['my_rating'] = ratings.models.Rating.get_for(
|
|
self.request.user.pk, self.object.pk
|
|
)
|
|
return context
|
|
|
|
|
|
class VersionsView(ExtensionQuerysetMixin, ListView):
|
|
model = Version
|
|
paginate_by = 15
|
|
|
|
def get_queryset(self):
|
|
"""Allow logged in users to view unlisted versions in certain conditions.
|
|
|
|
* maintainers should be able to preview their yet unlisted versions;
|
|
* staff should be able to preview yet unlisted versions;
|
|
"""
|
|
self.extension_queryset = self.get_extension_queryset()
|
|
self.extension = get_object_or_404(self.extension_queryset, slug=self.kwargs['slug'])
|
|
queryset = self.extension.versions
|
|
if self.request.user.is_staff or self.extension.has_maintainer(self.request.user):
|
|
return queryset.all()
|
|
return queryset.listed
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['extension'] = self.extension
|
|
return context
|
|
|
|
|
|
class ManageListView(LoginRequiredMixin, ListView):
|
|
model = Extension
|
|
paginate_by = 15
|
|
template_name = 'extensions/manage/list.html'
|
|
|
|
def get_queryset(self):
|
|
return Extension.objects.authored_by(user_id=self.request.user.pk)
|
|
|
|
|
|
class UpdateExtensionView(
|
|
LoginRequiredMixin,
|
|
MaintainedExtensionMixin,
|
|
SuccessMessageMixin,
|
|
DraftMixin,
|
|
UpdateView,
|
|
):
|
|
model = Extension
|
|
template_name = 'extensions/manage/update.html'
|
|
form_class = ExtensionUpdateForm
|
|
success_message = "Updated successfully"
|
|
|
|
def get_success_url(self):
|
|
self.object.refresh_from_db()
|
|
return self.object.get_manage_url()
|
|
|
|
def get_context_data(self, *args, **kwargs):
|
|
context = super().get_context_data(*args, **kwargs)
|
|
edit_preview_formset = kwargs.pop('edit_preview_formset', None)
|
|
add_preview_formset = kwargs.pop('add_preview_formset', None)
|
|
if not (edit_preview_formset and add_preview_formset):
|
|
if self.request.POST:
|
|
edit_preview_formset = EditPreviewFormSet(
|
|
self.request.POST, self.request.FILES, instance=self.object
|
|
)
|
|
add_preview_formset = AddPreviewFormSet(
|
|
self.request.POST,
|
|
self.request.FILES,
|
|
extension=self.object,
|
|
request=self.request,
|
|
)
|
|
else:
|
|
edit_preview_formset = EditPreviewFormSet(instance=self.object)
|
|
add_preview_formset = AddPreviewFormSet(extension=self.object, request=self.request)
|
|
context['edit_preview_formset'] = edit_preview_formset
|
|
context['add_preview_formset'] = add_preview_formset
|
|
return context
|
|
|
|
def form_invalid(self, form, edit_preview_formset=None, add_preview_formset=None):
|
|
return self.render_to_response(
|
|
self.get_context_data(
|
|
form=form,
|
|
edit_preview_formset=edit_preview_formset,
|
|
add_preview_formset=add_preview_formset,
|
|
)
|
|
)
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form):
|
|
edit_preview_formset = EditPreviewFormSet(
|
|
self.request.POST, self.request.FILES, instance=self.object
|
|
)
|
|
add_preview_formset = AddPreviewFormSet(
|
|
self.request.POST, self.request.FILES, extension=self.object, request=self.request
|
|
)
|
|
if edit_preview_formset.is_valid() and add_preview_formset.is_valid():
|
|
try:
|
|
edit_preview_formset.save()
|
|
add_preview_formset.save()
|
|
response = super().form_valid(form)
|
|
return response
|
|
except forms.ValidationError as e:
|
|
if 'hash' in e.error_dict:
|
|
add_preview_formset.forms[0].add_error('source', e.error_dict['hash'])
|
|
return self.form_invalid(form, edit_preview_formset, add_preview_formset)
|
|
|
|
|
|
class DeleteExtensionView(
|
|
LoginRequiredMixin,
|
|
UserPassesTestMixin,
|
|
DeleteView,
|
|
):
|
|
model = Extension
|
|
template_name = 'extensions/confirm_delete.html'
|
|
form_class = ExtensionDeleteForm
|
|
|
|
def get_success_url(self):
|
|
return reverse('extensions:manage-list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['extension_name'] = self.object.name
|
|
context['confirm_url'] = self.object.get_delete_url()
|
|
return context
|
|
|
|
def test_func(self) -> bool:
|
|
obj = self.get_object()
|
|
# Only maintainers allowed
|
|
if not obj.has_maintainer(self.request.user):
|
|
return False
|
|
# Unless this extension cannot be deleted anymore
|
|
cannot_be_deleted_reasons = obj.cannot_be_deleted_reasons
|
|
if len(cannot_be_deleted_reasons) > 0:
|
|
return False
|
|
return True
|
|
|
|
|
|
class VersionDeleteView(
|
|
LoginRequiredMixin,
|
|
MaintainedExtensionMixin,
|
|
UserPassesTestMixin,
|
|
DeleteView,
|
|
):
|
|
model = Version
|
|
template_name = 'extensions/version_confirm_delete.html'
|
|
form_class = VersionDeleteForm
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
'extensions:manage-versions',
|
|
kwargs={
|
|
'type_slug': self.kwargs['type_slug'],
|
|
'slug': self.kwargs['slug'],
|
|
},
|
|
)
|
|
|
|
def get_object(self, queryset=None):
|
|
return get_object_or_404(
|
|
Version,
|
|
extension__slug=self.kwargs['slug'],
|
|
pk=self.kwargs['pk'],
|
|
)
|
|
|
|
def _get_version_from_id(self):
|
|
version_id = self.kwargs['pk']
|
|
version = self.extension.versions.filter(id=version_id).first()
|
|
if version is None:
|
|
raise RuntimeError(
|
|
f'Could not find version {version_id} for extension {self.extension.id}'
|
|
)
|
|
return version
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
version = self._get_version_from_id()
|
|
context['extension_name'] = self.extension.name
|
|
context['version'] = version.version
|
|
context['confirm_url'] = version.get_delete_url()
|
|
return context
|
|
|
|
def test_func(self) -> bool:
|
|
obj = self.get_object()
|
|
# Unless this version cannot be deleted anymore
|
|
cannot_be_deleted_reasons = obj.cannot_be_deleted_reasons
|
|
if len(cannot_be_deleted_reasons) > 0:
|
|
return False
|
|
return True
|
|
|
|
|
|
class ManageVersionsView(
|
|
LoginRequiredMixin,
|
|
MaintainedExtensionMixin,
|
|
VersionsView,
|
|
):
|
|
pass
|
|
|
|
|
|
class NewVersionView(
|
|
LoginRequiredMixin,
|
|
MaintainedExtensionMixin,
|
|
CreateView,
|
|
):
|
|
"""Upload a file for a new version of existing extension."""
|
|
|
|
model = File
|
|
template_name = 'extensions/submit.html'
|
|
form_class = FileForm
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['extension'] = self.extension
|
|
return ctx
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['request'] = self.request
|
|
kwargs['extension'] = self.extension
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
'extensions:new-version-finalise',
|
|
kwargs={
|
|
'type_slug': self.extension.type_slug,
|
|
'slug': self.extension.slug,
|
|
'pk': self.object.pk,
|
|
},
|
|
)
|
|
|
|
|
|
class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
|
|
"""Finalise a new version of existing extension and send it to review."""
|
|
|
|
template_name = 'extensions/new_version_finalise.html'
|
|
form_class = VersionForm
|
|
|
|
def form_valid(self, form):
|
|
# Automatically approve new versions, for now.
|
|
# We rely on approval of the first submission, for now.
|
|
self.file.status = self.file.STATUSES.APPROVED
|
|
self.file.save()
|
|
response = super().form_valid(form)
|
|
return response
|
|
|
|
def _get_extension(self) -> 'Extension':
|
|
return get_object_or_404(Extension, slug=self.kwargs['slug'])
|
|
|
|
def _get_version(self, extension) -> 'Version':
|
|
return Version.objects.update_or_create(
|
|
extension=extension, file=self.file, **self.file.parsed_version_fields
|
|
)[0]
|
|
|
|
def get_form_kwargs(self):
|
|
form_kwargs = super().get_form_kwargs()
|
|
form_kwargs['instance'] = self._get_version(self.extension)
|
|
return form_kwargs
|
|
|
|
def get_initial(self):
|
|
"""Return initial values for the version, based on the file."""
|
|
initial = super().get_initial()
|
|
initial['file'] = self.file
|
|
initial.update(**self.file.parsed_version_fields)
|
|
return initial
|
|
|
|
def get_success_url(self):
|
|
return self.extension.get_manage_versions_url()
|
|
|
|
|
|
class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
|
"""Update release notes for an existing version."""
|
|
|
|
template_name = 'extensions/new_version_finalise.html'
|
|
model = Version
|
|
fields = ['release_notes']
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
'extensions:versions',
|
|
kwargs={
|
|
'type_slug': self.object.extension.type_slug,
|
|
'slug': self.object.extension.slug,
|
|
},
|
|
)
|
|
|
|
def test_func(self) -> bool:
|
|
# Only maintainers are allowed to perform this
|
|
return self.get_object().extension.has_maintainer(self.request.user)
|
|
|
|
|
|
class DraftExtensionView(
|
|
LoginRequiredMixin,
|
|
MaintainedExtensionMixin,
|
|
DraftVersionMixin,
|
|
UserPassesTestMixin,
|
|
SuccessMessageMixin,
|
|
FormView,
|
|
):
|
|
template_name = 'extensions/draft_finalise.html'
|
|
form_class = VersionForm
|
|
|
|
@property
|
|
def success_message(self) -> str:
|
|
if self.extension.status == Extension.STATUSES.INCOMPLETE:
|
|
return "Updated successfully"
|
|
return "Submitted to the Approval Queue"
|
|
|
|
def test_func(self) -> bool:
|
|
return self.extension.status == Extension.STATUSES.INCOMPLETE
|
|
|
|
def get_form_kwargs(self):
|
|
form_kwargs = super().get_form_kwargs()
|
|
form_kwargs['instance'] = self.extension.versions.first()
|
|
return form_kwargs
|
|
|
|
def get_initial(self):
|
|
"""Return initial values for the version, based on the file."""
|
|
initial = super().get_initial()
|
|
if self.version:
|
|
initial['file'] = self.version.file
|
|
initial.update(**self.version.file.parsed_version_fields)
|
|
return initial
|
|
|
|
def get_context_data(self, form=None, extension_form=None, add_preview_formset=None, **kwargs):
|
|
"""Add all the additional forms to the context."""
|
|
context = super().get_context_data(**kwargs)
|
|
if not (add_preview_formset and extension_form):
|
|
extension_form = ExtensionUpdateForm(instance=self.extension)
|
|
add_preview_formset = AddPreviewFormSet(extension=self.extension, request=self.request)
|
|
context['extension_form'] = extension_form
|
|
context['add_preview_formset'] = add_preview_formset
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
"""Handle bound forms and valid/invalid logic with the extra forms."""
|
|
form = self.get_form()
|
|
extension_form = ExtensionUpdateForm(
|
|
self.request.POST, self.request.FILES, instance=self.extension
|
|
)
|
|
add_preview_formset = AddPreviewFormSet(
|
|
self.request.POST, self.request.FILES, extension=self.extension, request=self.request
|
|
)
|
|
if form.is_valid() and extension_form.is_valid() and add_preview_formset.is_valid():
|
|
return self.form_valid(form, extension_form, add_preview_formset)
|
|
return self.form_invalid(form, extension_form, add_preview_formset)
|
|
|
|
@transaction.atomic
|
|
def form_valid(self, form, extension_form, add_preview_formset):
|
|
"""Save all the forms in correct order.
|
|
|
|
Extension must be saved first.
|
|
"""
|
|
try:
|
|
# Send the extension and version to the review
|
|
if 'submit_draft' in self.request.POST:
|
|
extension_form.instance.status = extension_form.instance.STATUSES.AWAITING_REVIEW
|
|
extension_form.save()
|
|
add_preview_formset.save()
|
|
form.save()
|
|
if 'submit_draft' in self.request.POST:
|
|
# TODO allow to submit a custom message via the form
|
|
ApprovalActivity(
|
|
user=self.request.user,
|
|
extension=extension_form.instance,
|
|
type=ApprovalActivity.ActivityType.AWAITING_REVIEW,
|
|
message="initial submission",
|
|
).save()
|
|
return super().form_valid(form)
|
|
except forms.ValidationError as e:
|
|
if 'hash' in e.error_dict:
|
|
add_preview_formset.forms[0].add_error('source', e.error_dict['hash'])
|
|
return self.form_invalid(form, extension_form, add_preview_formset)
|
|
|
|
def form_invalid(self, form, extension_form, add_preview_formset):
|
|
return self.render_to_response(
|
|
self.get_context_data(form, extension_form, add_preview_formset)
|
|
)
|
|
|
|
def get_success_url(self):
|
|
return self.extension.get_manage_url()
|