Implement Web Assets' theme system and selection, and add 'light' theme #118
@ -33,7 +33,8 @@
|
||||
<body class="has-global-bar">
|
||||
{% switch "is_alpha" %}
|
||||
<div class="site-announcement-alpha">
|
||||
This is a test instance. Add-ons and other data might be deleted at any time. <a class="text-underline" href="https://devtalk.blender.org/tag/extensions" target="_blank">Learn more</a>
|
||||
This platform is currently in alpha.
|
||||
<a class="text-underline" href="https://projects.blender.org/infrastructure/extensions-website/issues" target="_blank">Please report any issues you may find</a>, thanks! <a class="text-underline" href="https://devtalk.blender.org/tag/extensions" target="_blank">Learn more</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% switch "is_beta" %}
|
||||
|
@ -61,10 +61,9 @@ def _extract_urls(urlpatterns, parents):
|
||||
|
||||
|
||||
def extract_urls(urlpatterns=None):
|
||||
"""
|
||||
Extract URLEntry objects from the given iterable
|
||||
of Django URL pattern objects. If no iterable is given,
|
||||
the patterns exposed by the root resolver are used, i.e.
|
||||
"""Extract URLEntry objects from the given iterable of Django URL pattern objects.
|
||||
|
||||
If no iterable is given, the patterns exposed by the root resolver are used, i.e.
|
||||
all of the URLs routed in the project.
|
||||
:param urlpatterns: Iterable of URLPattern objects
|
||||
:return: Generator of `URLEntry` objects.
|
||||
@ -78,7 +77,10 @@ def extract_urls(urlpatterns=None):
|
||||
def _get_all_form_errors(response):
|
||||
return (
|
||||
{
|
||||
key: response.context[key].errors
|
||||
key: [
|
||||
response.context[key].errors,
|
||||
getattr(response.context[key], 'non_form_errors', lambda: None)(),
|
||||
]
|
||||
for key in response.context.keys()
|
||||
if key.endswith('_formset') or key == 'form' or key.endswith('_form')
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ import logging
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django.core.exceptions
|
||||
|
||||
from files.validators import FileMIMETypeValidator
|
||||
from constants.base import ALLOWED_PREVIEW_MIMETYPES
|
||||
|
||||
import extensions.models
|
||||
import files.models
|
||||
import reviewers.models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -41,7 +43,8 @@ class AddPreviewFileForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = files.models.File
|
||||
fields = ('caption', 'source')
|
||||
fields = ('caption', 'source', 'original_hash', 'hash')
|
||||
widgets = {'original_hash': forms.HiddenInput(), 'hash': forms.HiddenInput()}
|
||||
|
||||
source = forms.FileField(
|
||||
allow_empty_file=False,
|
||||
@ -64,6 +67,27 @@ class AddPreviewFileForm(forms.ModelForm):
|
||||
self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_original_hash(self, *args, **kwargs):
|
||||
"""Calculate original hash of the uploaded file."""
|
||||
if 'source' not in self.cleaned_data:
|
||||
return
|
||||
source = self.cleaned_data['source']
|
||||
return files.models.File.generate_hash(source)
|
||||
|
||||
def clean_hash(self, *args, **kwargs):
|
||||
return self.cleaned_data['original_hash']
|
||||
|
||||
def add_error(self, field, error):
|
||||
"""Add hidden `original_hash`/`hash` errors to the visible `source` field instead."""
|
||||
if isinstance(error, django.core.exceptions.ValidationError):
|
||||
if getattr(error, 'error_dict', None):
|
||||
hash_error = error.error_dict.pop('hash', None)
|
||||
if hash_error:
|
||||
error.error_dict['source'] = hash_error
|
||||
# `original_hash` is treated identically to `hash`, so its errors can be discarded
|
||||
error.error_dict.pop('original_hash', None)
|
||||
super().add_error(field, error)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save Preview from the cleaned form data."""
|
||||
# Fill in missing fields from request and the source file
|
||||
@ -81,6 +105,8 @@ class AddPreviewFileForm(forms.ModelForm):
|
||||
|
||||
|
||||
class AddPreviewModelFormSet(forms.BaseModelFormSet):
|
||||
msg_duplicate_file = _('Please select another file instead of the duplicate')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
self.extension = kwargs.pop('extension')
|
||||
@ -94,6 +120,14 @@ class AddPreviewModelFormSet(forms.BaseModelFormSet):
|
||||
form_kwargs['extension'] = self.extension
|
||||
return form_kwargs
|
||||
|
||||
def get_unique_error_message(self, unique_check):
|
||||
"""Replace duplicate `original_hash`/`hash` message with a more meaningful one."""
|
||||
if len(unique_check) == 1:
|
||||
field = unique_check[0]
|
||||
if field in ('original_hash', 'hash'):
|
||||
return self.msg_duplicate_file
|
||||
return super().get_unique_error_message(unique_check)
|
||||
|
||||
|
||||
AddPreviewFormSet = forms.modelformset_factory(
|
||||
files.models.File,
|
||||
@ -104,6 +138,16 @@ AddPreviewFormSet = forms.modelformset_factory(
|
||||
|
||||
|
||||
class ExtensionUpdateForm(forms.ModelForm):
|
||||
# Messages for auto-generated activity
|
||||
msg_converted_to_draft = _('Converted to Draft')
|
||||
msg_awaiting_review = _('Ready for review')
|
||||
|
||||
# Messages for additional validation
|
||||
msg_cannot_convert_to_draft = _(
|
||||
'An extension can be converted to draft only while it is Awating Review'
|
||||
)
|
||||
msg_need_previews = _('Please add at least one preview.')
|
||||
|
||||
class Meta:
|
||||
model = extensions.models.Extension
|
||||
fields = (
|
||||
@ -111,17 +155,79 @@ class ExtensionUpdateForm(forms.ModelForm):
|
||||
'support',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if (
|
||||
'convert_to_draft' in self.data
|
||||
and self.instance.status != self.instance.STATUSES.AWAITING_REVIEW
|
||||
):
|
||||
self.add_error(
|
||||
None, 'An extension can be converted to draft only while it is Awating Review'
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Pass the request and initialise all the nested form(set)s."""
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request.POST:
|
||||
edit_preview_formset = EditPreviewFormSet(
|
||||
self.request.POST, self.request.FILES, instance=self.instance
|
||||
)
|
||||
add_preview_formset = AddPreviewFormSet(
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
extension=self.instance,
|
||||
request=self.request,
|
||||
)
|
||||
else:
|
||||
edit_preview_formset = EditPreviewFormSet(instance=self.instance)
|
||||
add_preview_formset = AddPreviewFormSet(extension=self.instance, request=self.request)
|
||||
self.edit_preview_formset = edit_preview_formset
|
||||
self.add_preview_formset = add_preview_formset
|
||||
self.add_preview_formset.error_messages['too_few_forms'] = self.msg_need_previews
|
||||
|
||||
def is_valid(self, *args, **kwargs) -> bool:
|
||||
"""Validate all nested forms and form(set)s first."""
|
||||
# Require at least one preview image when requesting a review
|
||||
if 'submit_draft' in self.data:
|
||||
if not self.instance.previews.exists():
|
||||
self.add_preview_formset.min_num = 1
|
||||
self.add_preview_formset.validate_min = True
|
||||
|
||||
is_valid_flags = [
|
||||
self.edit_preview_formset.is_valid(),
|
||||
self.add_preview_formset.is_valid(),
|
||||
super().is_valid(*args, **kwargs),
|
||||
]
|
||||
return all(is_valid_flags)
|
||||
|
||||
def clean(self):
|
||||
"""Perform additional validation and status changes."""
|
||||
super().clean()
|
||||
# Convert to draft, if possible
|
||||
if 'convert_to_draft' in self.data:
|
||||
if self.instance.status != self.instance.STATUSES.AWAITING_REVIEW:
|
||||
self.add_error(None, self.msg_cannot_convert_to_draft)
|
||||
else:
|
||||
self.instance.status = self.instance.STATUSES.INCOMPLETE
|
||||
self.instance.converted_to_draft = True
|
||||
|
||||
# Send the extension and version to the review, if possible
|
||||
if 'submit_draft' in self.data:
|
||||
self.instance.status = self.instance.STATUSES.AWAITING_REVIEW
|
||||
self.instance.sent_to_review = True
|
||||
return self.cleaned_data
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save the nested form(set)s, then the main form."""
|
||||
self.edit_preview_formset.save()
|
||||
self.add_preview_formset.save()
|
||||
if getattr(self.instance, 'converted_to_draft', False):
|
||||
reviewers.models.ApprovalActivity(
|
||||
user=self.request.user,
|
||||
extension=self.instance,
|
||||
type=reviewers.models.ApprovalActivity.ActivityType.AWAITING_CHANGES,
|
||||
message=self.msg_converted_to_draft,
|
||||
).save()
|
||||
if getattr(self.instance, 'sent_to_review', False):
|
||||
reviewers.models.ApprovalActivity(
|
||||
user=self.request.user,
|
||||
extension=self.instance,
|
||||
type=reviewers.models.ApprovalActivity.ActivityType.AWAITING_REVIEW,
|
||||
message=self.msg_awaiting_review,
|
||||
).save()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ExtensionDeleteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -69,7 +69,7 @@
|
||||
highlight what makes your {{ type }} special.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
{% include "extensions/manage/components/edit_previews.html" %}
|
||||
{% include "extensions/manage/components/add_previews.html" %}
|
||||
</div>
|
||||
</section>
|
||||
|
@ -0,0 +1,47 @@
|
||||
{% load common %}
|
||||
{% comment %}
|
||||
Handles displaying existing previews, updating their captions
|
||||
and deleting them on Extension Update and Extension Draft pages
|
||||
{% endcomment %}
|
||||
{{ edit_preview_formset.management_form }}
|
||||
{{ edit_preview_formset.non_form_errors }}
|
||||
{% if edit_preview_formset.forms|length %}
|
||||
{# View or delete existing preview files #}
|
||||
<div class="previews-list js-previews-drag-container">
|
||||
{% for newform in edit_preview_formset %}
|
||||
{% with inlineform=newform|add_form_classes %}
|
||||
{% with file=inlineform.instance.file %}
|
||||
<div class="previews-list-item" data-preview-position="{{ inlineform.instance.position }}">
|
||||
<div class="js-preview-drag drag-widget is-draggable">
|
||||
<i class="i-menu"></i>
|
||||
<div class="previews-list-item-thumbnail">
|
||||
<div class="previews-list-item-thumbnail-img" style="background-image: url('{% if file.is_image %}{{ file.source.url }}{% elif file.is_video and file.thumbnail %}{{ file.thumbnail.url }}{% endif %}');" title="Preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<div>
|
||||
{% include "common/components/field.html" with field=inlineform.id %}
|
||||
{% include "common/components/field.html" with field=inlineform.caption %}
|
||||
{% include "common/components/field.html" with field=inlineform.position %}
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ inlineform.instance.file.source.url }}" target="_blank">
|
||||
<small>Source File</small>
|
||||
</a>
|
||||
</li>
|
||||
<li class="ms-auto">
|
||||
{% include "common/components/field.html" with field=inlineform.DELETE %}
|
||||
</li>
|
||||
<li>
|
||||
{{ inlineform.non_form_errors }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr class="my-4"/>
|
||||
{% endif %}
|
@ -41,49 +41,7 @@
|
||||
<section class="mt-4">
|
||||
<h2>{% trans 'Previews' %}</h2>
|
||||
<div class="previews-upload">
|
||||
{{ edit_preview_formset.management_form }}
|
||||
{{ edit_preview_formset.non_form_errors }}
|
||||
{% if edit_preview_formset.forms|length %}
|
||||
{# View or delete existing preview files #}
|
||||
<div class="previews-list js-previews-drag-container">
|
||||
{% for newform in edit_preview_formset %}
|
||||
{% with inlineform=newform|add_form_classes %}
|
||||
{% with file=inlineform.instance.file %}
|
||||
<div class="previews-list-item" data-preview-position="{{ inlineform.instance.position }}">
|
||||
<div class="js-preview-drag drag-widget is-draggable">
|
||||
<i class="i-menu"></i>
|
||||
<div class="previews-list-item-thumbnail">
|
||||
<div class="previews-list-item-thumbnail-img" style="background-image: url('{% if file.is_image %}{{ file.source.url }}{% elif file.is_video and file.thumbnail %}{{ file.thumbnail.url }}{% endif %}');" title="Preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<div>
|
||||
{% include "common/components/field.html" with field=inlineform.id %}
|
||||
{% include "common/components/field.html" with field=inlineform.caption %}
|
||||
{% include "common/components/field.html" with field=inlineform.position %}
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ inlineform.instance.file.source.url }}" target="_blank">
|
||||
<small>Source File</small>
|
||||
</a>
|
||||
</li>
|
||||
<li class="ms-auto">
|
||||
{% include "common/components/field.html" with field=inlineform.DELETE %}
|
||||
</li>
|
||||
<li>
|
||||
{{ inlineform.non_form_errors }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr class="my-4"/>
|
||||
{% endif %}
|
||||
|
||||
{% include "extensions/manage/components/edit_previews.html" %}
|
||||
{% include "extensions/manage/components/add_previews.html" %}
|
||||
</div>
|
||||
</section>
|
||||
|
@ -85,6 +85,24 @@ EXPECTED_VALIDATION_ERRORS = {
|
||||
'invalid-manifest-toml.zip': {'source': ['Could not parse the manifest file.']},
|
||||
'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']},
|
||||
}
|
||||
POST_DATA = {
|
||||
'preview_set-TOTAL_FORMS': ['0'],
|
||||
'preview_set-INITIAL_FORMS': ['0'],
|
||||
'preview_set-MIN_NUM_FORMS': ['0'],
|
||||
'preview_set-MAX_NUM_FORMS': ['1000'],
|
||||
'preview_set-0-id': [''],
|
||||
# 'preview_set-0-extension': [str(extension.pk)],
|
||||
'preview_set-1-id': [''],
|
||||
# 'preview_set-1-extension': [str(extension.pk)],
|
||||
'form-TOTAL_FORMS': ['0'],
|
||||
'form-INITIAL_FORMS': ['0'],
|
||||
'form-MIN_NUM_FORMS': ['0'],
|
||||
'form-MAX_NUM_FORMS': ['1000'],
|
||||
'form-0-id': '',
|
||||
# 'form-0-caption': ['First Preview Caption Text'],
|
||||
'form-1-id': '',
|
||||
# 'form-1-caption': ['Second Preview Caption Text'],
|
||||
}
|
||||
|
||||
|
||||
class SubmitFileTest(TestCase):
|
||||
@ -280,20 +298,19 @@ class SubmitFinaliseTest(TestCase):
|
||||
|
||||
def test_post_finalise_addon_validation_errors(self):
|
||||
self.client.force_login(self.file.user)
|
||||
response = self.client.post(self.file.get_submit_url(), {})
|
||||
data = {**POST_DATA, 'submit_draft': ''}
|
||||
response = self.client.post(self.file.get_submit_url(), data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
_get_all_form_errors(response),
|
||||
{
|
||||
'form': {},
|
||||
'extension_form': {
|
||||
'description': ['This field is required.'],
|
||||
},
|
||||
'add_preview_formset': [],
|
||||
'form': [{}, None],
|
||||
'extension_form': [{'description': ['This field is required.']}, None],
|
||||
'add_preview_formset': [[], ['Please add at least one preview.']],
|
||||
'edit_preview_formset': [[], []],
|
||||
},
|
||||
)
|
||||
self.assertFalse('TODO: It should include preview as required')
|
||||
|
||||
def test_post_finalise_addon_creates_addon_with_version_awaiting_review(self):
|
||||
self.assertEqual(File.objects.count(), 1)
|
||||
@ -318,6 +335,15 @@ class SubmitFinaliseTest(TestCase):
|
||||
'form-0-caption': ['First Preview Caption Text'],
|
||||
'form-1-id': '',
|
||||
'form-1-caption': ['Second Preview Caption Text'],
|
||||
# Edit form for existing previews
|
||||
'preview_set-TOTAL_FORMS': ['0'],
|
||||
'preview_set-INITIAL_FORMS': ['0'],
|
||||
'preview_set-MIN_NUM_FORMS': ['0'],
|
||||
'preview_set-MAX_NUM_FORMS': ['1000'],
|
||||
'preview_set-0-id': [''],
|
||||
# 'preview_set-0-extension': [str(extension.pk)],
|
||||
'preview_set-1-id': [''],
|
||||
# 'preview_set-1-extension': [str(extension.pk)],
|
||||
# Submit for Approval.
|
||||
'submit_draft': '',
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||
from django.test import TestCase
|
||||
|
||||
from common.tests.factories.extensions import create_approved_version, create_version
|
||||
from common.tests.factories.files import FileFactory
|
||||
from common.tests.utils import _get_all_form_errors
|
||||
from extensions.models import Extension
|
||||
from files.models import File
|
||||
@ -201,6 +202,45 @@ class UpdateTest(TestCase):
|
||||
response = self.client.post(url, {**data, **files})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.maxDiff = None
|
||||
self.assertEqual(
|
||||
[
|
||||
response.context['add_preview_formset'].forms[0].errors,
|
||||
response.context['add_preview_formset'].forms[1].errors,
|
||||
response.context['add_preview_formset'].non_form_errors(),
|
||||
],
|
||||
[
|
||||
{},
|
||||
{'__all__': ['Please correct the duplicate values below.']},
|
||||
[
|
||||
'Please select another file instead of the duplicate',
|
||||
'Please select another file instead of the duplicate',
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
def test_post_upload_validation_error_image_already_exists(self):
|
||||
FileFactory(
|
||||
original_hash='sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340',
|
||||
hash='sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340',
|
||||
source='file/original_image_source.jpg',
|
||||
)
|
||||
extension = create_approved_version().extension
|
||||
|
||||
data = {
|
||||
**POST_DATA,
|
||||
'form-TOTAL_FORMS': ['1'],
|
||||
}
|
||||
file_name1 = 'test_preview_image_0001.png'
|
||||
url = extension.get_manage_url()
|
||||
user = extension.authors.first()
|
||||
self.client.force_login(user)
|
||||
with open(TEST_FILES_DIR / file_name1, 'rb') as fp1:
|
||||
files = {'form-0-source': fp1}
|
||||
response = self.client.post(url, {**data, **files})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.maxDiff = None
|
||||
self.assertEqual(
|
||||
response.context['add_preview_formset'].forms[0].errors,
|
||||
{'source': ['File with this Hash already exists.']},
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""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, redirect, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
||||
|
||||
@ -16,8 +14,6 @@ from .mixins import (
|
||||
DraftVersionMixin,
|
||||
)
|
||||
from extensions.forms import (
|
||||
EditPreviewFormSet,
|
||||
AddPreviewFormSet,
|
||||
ExtensionDeleteForm,
|
||||
ExtensionUpdateForm,
|
||||
VersionForm,
|
||||
@ -26,7 +22,6 @@ from extensions.forms import (
|
||||
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
|
||||
|
||||
@ -109,7 +104,12 @@ class UpdateExtensionView(
|
||||
template_name = 'extensions/manage/update.html'
|
||||
form_class = ExtensionUpdateForm
|
||||
success_message = "Updated successfully"
|
||||
msg_converted_to_draft = _('Converted to Draft')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass request object to the form."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
extension = self.extension
|
||||
@ -124,62 +124,13 @@ class UpdateExtensionView(
|
||||
|
||||
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
|
||||
context['edit_preview_formset'] = context['form'].edit_preview_formset
|
||||
context['add_preview_formset'] = context['form'].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):
|
||||
if 'convert_to_draft' in self.request.POST:
|
||||
form.instance.status = form.instance.STATUSES.INCOMPLETE
|
||||
form.save()
|
||||
ApprovalActivity(
|
||||
user=self.request.user,
|
||||
extension=form.instance,
|
||||
type=ApprovalActivity.ActivityType.AWAITING_CHANGES,
|
||||
message=self.msg_converted_to_draft,
|
||||
).save()
|
||||
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)
|
||||
def form_valid(self, *args, **kwargs):
|
||||
return super().form_valid(*args, **kwargs)
|
||||
|
||||
|
||||
class DeleteExtensionView(
|
||||
@ -311,14 +262,6 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
|
||||
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'])
|
||||
|
||||
@ -374,7 +317,6 @@ class DraftExtensionView(
|
||||
):
|
||||
template_name = 'extensions/draft_finalise.html'
|
||||
form_class = VersionForm
|
||||
msg_awaiting_review = _('Ready for review')
|
||||
|
||||
@property
|
||||
def success_message(self) -> str:
|
||||
@ -398,60 +340,38 @@ class DraftExtensionView(
|
||||
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):
|
||||
def get_context_data(self, form=None, extension_form=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)
|
||||
if not extension_form:
|
||||
extension_form = ExtensionUpdateForm(instance=self.extension, request=self.request)
|
||||
context['extension_form'] = extension_form
|
||||
context['add_preview_formset'] = add_preview_formset
|
||||
context['edit_preview_formset'] = extension_form.edit_preview_formset
|
||||
context['add_preview_formset'] = extension_form.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
|
||||
self.request.POST, self.request.FILES, instance=self.extension, request=self.request
|
||||
)
|
||||
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)
|
||||
if form.is_valid() and extension_form.is_valid():
|
||||
return self.form_valid(form, extension_form)
|
||||
return self.form_invalid(form, extension_form)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form, extension_form, add_preview_formset):
|
||||
def form_valid(self, form, extension_form):
|
||||
"""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=self.msg_awaiting_review,
|
||||
).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 form_invalid(self, form, extension_form):
|
||||
return self.render_to_response(self.get_context_data(form, extension_form))
|
||||
|
||||
def get_success_url(self):
|
||||
return self.extension.get_manage_url()
|
||||
|
@ -1,4 +1,5 @@
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
@ -12,7 +13,7 @@ import files.models
|
||||
import files.tasks
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which('clamd'), 'requires clamd')
|
||||
@unittest.skipUnless(os.path.exists('/var/run/clamav/clamd.ctl'), 'requires clamd running')
|
||||
@override_settings(MEDIA_ROOT='/tmp/')
|
||||
class FileScanTest(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -80,14 +80,16 @@ class Notification(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.action.verb == Verb.RATED_EXTENSION:
|
||||
url = self.action.target.get_ratings_url()
|
||||
fragment = f'#review-{self.action.action_object.pk}'
|
||||
url = self.action.target.get_ratings_url() + fragment
|
||||
elif self.action.verb in [
|
||||
Verb.APPROVED,
|
||||
Verb.COMMENTED,
|
||||
Verb.REQUESTED_CHANGES,
|
||||
Verb.REQUESTED_REVIEW,
|
||||
]:
|
||||
url = self.action.target.get_review_url()
|
||||
fragment = f'#activity-{self.action.action_object.pk}'
|
||||
url = self.action.target.get_review_url() + fragment
|
||||
elif self.action.action_object is not None:
|
||||
url = self.action.action_object.get_absolute_url()
|
||||
else:
|
||||
|
@ -78,9 +78,9 @@
|
||||
<hr class="my-4">
|
||||
<h2>Activity</h2>
|
||||
|
||||
{% if extension.review_activity.all %}
|
||||
{% if review_activity %}
|
||||
<ul class="activity-list">
|
||||
{% for activity in extension.review_activity.all %}
|
||||
{% for activity in review_activity %}
|
||||
<li id="activity-{{ activity.id }}">
|
||||
|
||||
{% if activity.type in status_change_types %}
|
||||
|
@ -77,8 +77,18 @@ class ExtensionsApprovalDetailView(DetailView):
|
||||
|
||||
template_name = 'reviewers/extensions_review_detail.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.prefetch_related(
|
||||
'authors',
|
||||
'previews',
|
||||
'versions',
|
||||
).all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['review_activity'] = (
|
||||
self.object.review_activity.select_related('user').order_by('date_created').all()
|
||||
)
|
||||
ctx['status_change_types'] = STATUS_CHANGE_TYPES
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
|
Loading…
Reference in New Issue
Block a user