diff --git a/extensions/models.py b/extensions/models.py
index 2e01e9c8..a6521a13 100644
--- a/extensions/models.py
+++ b/extensions/models.py
@@ -237,6 +237,9 @@ class Extension(
def get_absolute_url(self):
return reverse('extensions:detail', args=[self.type_slug, self.slug])
+ def get_draft_url(self):
+ return reverse('extensions:draft', args=[self.type_slug, self.slug])
+
def get_manage_url(self):
return reverse('extensions:manage', args=[self.type_slug, self.slug])
diff --git a/extensions/templates/extensions/submit_finalise.html b/extensions/templates/extensions/draft_finalise.html
similarity index 100%
rename from extensions/templates/extensions/submit_finalise.html
rename to extensions/templates/extensions/draft_finalise.html
diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html
index ffa766c0..8087b57e 100644
--- a/extensions/templates/extensions/manage/update.html
+++ b/extensions/templates/extensions/manage/update.html
@@ -100,21 +100,12 @@
- {% if extension.status == extension. %}
-
- {% elif 'Incomplete' != extension.get_status_display %}
- {% endif %}
-
{% trans 'Upload New Version' %}
diff --git a/extensions/tests/test_submit.py b/extensions/tests/test_submit.py
index a69853ba..d91f39cc 100644
--- a/extensions/tests/test_submit.py
+++ b/extensions/tests/test_submit.py
@@ -224,12 +224,24 @@ class SubmitFinaliseTest(TestCase):
hash=file_data['file_hash'],
metadata=file_data['metadata'],
)
+ self.version = create_version(
+ file=self.file,
+ extension__name=file_data['metadata']['name'],
+ extension__slug=file_data['metadata']['id'].replace("_", "-"),
+ extension__website=None,
+ tagline=file_data['metadata']['tagline'],
+ version=file_data['metadata']['version'],
+ blender_version_min=file_data['metadata']['blender_version_min'],
+ schema_version=file_data['metadata']['schema_version'],
+ )
def test_get_finalise_addon_redirects_if_anonymous(self):
response = self.client.post(self.file.get_submit_url(), {})
self.assertEqual(response.status_code, 302)
- self.assertEqual(response['Location'], f'/oauth/login?next=/submit/{self.file.pk}/')
+ self.assertEqual(
+ response['Location'], f'/oauth/login?next=/add-ons/{self.file.extension.slug}/draft/'
+ )
def test_get_finalise_addon_not_allowed_if_different_user(self):
user = UserFactory()
@@ -237,7 +249,9 @@ class SubmitFinaliseTest(TestCase):
response = self.client.post(self.file.get_submit_url(), {})
- self.assertEqual(response.status_code, 403)
+ # Technically this could (should) be a 403, but changing this means changing
+ # the MaintainedExtensionMixin which is used in multiple places.
+ self.assertEqual(response.status_code, 404)
def test_post_finalise_addon_validation_errors(self):
self.client.force_login(self.file.user)
@@ -258,8 +272,8 @@ class SubmitFinaliseTest(TestCase):
def test_post_finalise_addon_creates_addon_with_version_awaiting_review(self):
self.assertEqual(File.objects.count(), 1)
- self.assertEqual(Extension.objects.count(), 0)
- self.assertEqual(Version.objects.count(), 0)
+ self.assertEqual(Extension.objects.count(), 1)
+ self.assertEqual(Version.objects.count(), 1)
self.assertEqual(File.objects.filter(type=File.TYPES.IMAGE).count(), 0)
self.client.force_login(self.file.user)
diff --git a/extensions/urls.py b/extensions/urls.py
index 4a7c870e..d03358d6 100644
--- a/extensions/urls.py
+++ b/extensions/urls.py
@@ -7,7 +7,6 @@ from extensions.views import api, public, submit, manage
app_name = 'extensions'
urlpatterns = [
path('submit/', submit.UploadFileView.as_view(), name='submit'),
- path('submit//', submit.SubmitFileView.as_view(), name='submit-finalise'),
# TODO: move /accounts pages under its own app when User model is replaced
path('accounts/extensions/', manage.ManageListView.as_view(), name='manage-list'),
# FIXME: while there's no profile page, redirect to My Extensions instead
@@ -35,6 +34,11 @@ urlpatterns = [
rf'^(?P{EXTENSION_SLUGS_PATH})/',
include(
[
+ path(
+ '/draft/',
+ manage.DraftExtensionView.as_view(),
+ name='draft',
+ ),
path(
'/manage/',
manage.UpdateExtensionView.as_view(),
diff --git a/extensions/views/manage.py b/extensions/views/manage.py
index 1fc187bf..c50b174b 100644
--- a/extensions/views/manage.py
+++ b/extensions/views/manage.py
@@ -5,10 +5,16 @@ 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
+from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
-from .mixins import ExtensionQuerysetMixin, OwnsFileMixin, MaintainedExtensionMixin
+from .mixins import (
+ ExtensionQuerysetMixin,
+ OwnsFileMixin,
+ MaintainedExtensionMixin,
+ DraftVersionMixin,
+ DraftMixin,
+)
from extensions.forms import (
EditPreviewFormSet,
AddPreviewFormSet,
@@ -91,6 +97,7 @@ class UpdateExtensionView(
MaintainedExtensionMixin,
SuccessMessageMixin,
UpdateView,
+ DraftMixin,
):
model = Extension
template_name = 'extensions/manage/update.html'
@@ -293,3 +300,78 @@ class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
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,
+ FormView,
+):
+ template_name = 'extensions/draft_finalise.html'
+ form_class = VersionForm
+
+ 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()
+ 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
+ extension_form.instance.status = extension_form.instance.STATUSES.AWAITING_REVIEW
+ extension_form.save()
+ add_preview_formset.save()
+ form.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()
diff --git a/extensions/views/mixins.py b/extensions/views/mixins.py
index ab1ff8d5..5060a492 100644
--- a/extensions/views/mixins.py
+++ b/extensions/views/mixins.py
@@ -1,5 +1,6 @@
from django.contrib.auth.mixins import UserPassesTestMixin
-from django.shortcuts import get_object_or_404
+from django.shortcuts import get_object_or_404, redirect
+from django.core.exceptions import BadRequest
from extensions.models import Extension
from files.models import File
@@ -54,3 +55,30 @@ class ExtensionMixin:
Extension.objects.exclude_deleted, slug=self.kwargs['slug']
)
return super().dispatch(*args, **kwargs)
+
+
+class DraftVersionMixin:
+ """Fetch the version object which is being edited as a draft."""
+
+ def dispatch(self, *args, **kwargs):
+ self.version = self.extension.versions.first()
+ if self.version is None:
+ error_message = f'Extension "{self.extension.name}" has no versions.'
+ raise BadRequest(error_message)
+ return super().dispatch(*args, **kwargs)
+
+
+class DraftMixin:
+ """If the extension is incomplete, returns the FinalizeDraftView"""
+
+ def dispatch(self, request, *args, **kwargs):
+ extension = (
+ Extension.objects.listed_or_authored_by(user_id=self.request.user.pk)
+ .filter(status=Extension.STATUSES.INCOMPLETE)
+ .first()
+ )
+
+ if not extension:
+ return super().dispatch(request, *args, **kwargs)
+
+ return redirect(extension.get_draft_url())
diff --git a/extensions/views/submit.py b/extensions/views/submit.py
index d9787e89..ef980657 100644
--- a/extensions/views/submit.py
+++ b/extensions/views/submit.py
@@ -1,25 +1,18 @@
import logging
-from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
-from django.shortcuts import reverse
-from django.views.generic.edit import CreateView, FormView
+from django.views.generic.edit import CreateView
-from .mixins import OwnsFileMixin
+from .mixins import DraftMixin
from extensions.models import Version, Extension
-from extensions.forms import (
- ExtensionUpdateForm,
- VersionForm,
- AddPreviewFormSet,
-)
from files.forms import FileForm
from files.models import File
logger = logging.getLogger(__name__)
-class UploadFileView(LoginRequiredMixin, CreateView):
+class UploadFileView(LoginRequiredMixin, DraftMixin, CreateView):
model = File
template_name = 'extensions/submit.html'
form_class = FileForm
@@ -30,30 +23,12 @@ class UploadFileView(LoginRequiredMixin, CreateView):
return kwargs
def get_success_url(self):
- return reverse('extensions:submit-finalise', kwargs={'pk': self.object.pk})
+ return self.extension.get_draft_url()
-
-class SubmitFileView(LoginRequiredMixin, OwnsFileMixin, FormView):
- template_name = 'extensions/submit_finalise.html'
- form_class = VersionForm
-
- def _get_extension(self) -> 'Extension':
- # Get the existing Extension and Version, if they were partially saved already
- existing_version = getattr(self.file, 'version', None)
- if existing_version:
- extension = existing_version.extension
- assert (
- extension.can_request_review
- ), 'TODO: cannot request review for extension: {extension.get_status_display}'
- assert not extension.authors.count() or extension.authors.filter(
- user__id=self.request.user.pk
- ), 'TODO: cannot request review for extension: maintained by someone else'
- logger.warning(
- 'Found existing version pk=%s for file pk=%s',
- existing_version.pk,
- self.file.pk,
- )
- return extension
+ @transaction.atomic
+ def form_valid(self, form):
+ """Create an extension and a version already, associated with the user."""
+ self.file = form.instance
parsed_extension_fields = self.file.parsed_extension_fields
if parsed_extension_fields:
@@ -69,72 +44,21 @@ class SubmitFileView(LoginRequiredMixin, OwnsFileMixin, FormView):
extension.pk,
self.file.pk,
)
- return extension
- return Extension.objects.update_or_create(type=self.file.type, **parsed_extension_fields)[0]
+ return False
- def _get_version(self, extension) -> 'Version':
- return Version.objects.update_or_create(
- extension=extension, file=self.file, **self.file.parsed_version_fields
+ # Make sure an extension has a user associated to it from the beginning, otherwise
+ # it will prevent it from being re-uploaded and yet not show on My Extensions.
+ self.extension = Extension.objects.update_or_create(
+ type=self.file.type, **parsed_extension_fields
+ )[0]
+ self.extension.authors.add(self.request.user)
+ self.extension.save()
+
+ # Need to save the form to be able to use the file to create the version.
+ self.object = self.file = form.save()
+
+ Version.objects.update_or_create(
+ extension=self.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_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
- extension_form.instance.status = extension_form.instance.STATUSES.AWAITING_REVIEW
- extension_form.save()
- self.extension.authors.add(self.request.user)
- add_preview_formset.save()
- form.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.file.extension.get_manage_url()
+ return super().form_valid(form)
diff --git a/files/models.py b/files/models.py
index 2fc6d221..1598988d 100644
--- a/files/models.py
+++ b/files/models.py
@@ -6,7 +6,6 @@ import re
from django.contrib.auth import get_user_model
from django.db import models
-from django.urls import reverse
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin
from files.utils import get_sha256
@@ -209,7 +208,7 @@ class File(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mode
}
def get_submit_url(self) -> str:
- return reverse('extensions:submit-finalise', kwargs={'pk': self.pk})
+ return self.extension.get_draft_url()
class FileValidation(CreatedModifiedMixin, TrackChangesMixin, models.Model):