Implement Web Assets' theme system and selection, and add 'light' theme #118

Merged
Márton Lente merged 97 commits from martonlente/extensions-website:ui/theme-light into main 2024-05-08 14:20:07 +02:00
13 changed files with 289 additions and 176 deletions
Showing only changes of commit 3b88b0b95a - Show all commits

View File

@ -33,7 +33,8 @@
<body class="has-global-bar"> <body class="has-global-bar">
{% switch "is_alpha" %} {% switch "is_alpha" %}
<div class="site-announcement-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> </div>
{% else %} {% else %}
{% switch "is_beta" %} {% switch "is_beta" %}

View File

@ -61,10 +61,9 @@ def _extract_urls(urlpatterns, parents):
def extract_urls(urlpatterns=None): def extract_urls(urlpatterns=None):
""" """Extract URLEntry objects from the given iterable of Django URL pattern objects.
Extract URLEntry objects from the given iterable
of Django URL pattern objects. If no iterable is given, If no iterable is given, the patterns exposed by the root resolver are used, i.e.
the patterns exposed by the root resolver are used, i.e.
all of the URLs routed in the project. all of the URLs routed in the project.
:param urlpatterns: Iterable of URLPattern objects :param urlpatterns: Iterable of URLPattern objects
:return: Generator of `URLEntry` objects. :return: Generator of `URLEntry` objects.
@ -78,7 +77,10 @@ def extract_urls(urlpatterns=None):
def _get_all_form_errors(response): def _get_all_form_errors(response):
return ( 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() for key in response.context.keys()
if key.endswith('_formset') or key == 'form' or key.endswith('_form') if key.endswith('_formset') or key == 'form' or key.endswith('_form')
} }

View File

@ -2,12 +2,14 @@ import logging
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django.core.exceptions
from files.validators import FileMIMETypeValidator from files.validators import FileMIMETypeValidator
from constants.base import ALLOWED_PREVIEW_MIMETYPES from constants.base import ALLOWED_PREVIEW_MIMETYPES
import extensions.models import extensions.models
import files.models import files.models
import reviewers.models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,7 +43,8 @@ class AddPreviewFileForm(forms.ModelForm):
class Meta: class Meta:
model = files.models.File model = files.models.File
fields = ('caption', 'source') fields = ('caption', 'source', 'original_hash', 'hash')
widgets = {'original_hash': forms.HiddenInput(), 'hash': forms.HiddenInput()}
source = forms.FileField( source = forms.FileField(
allow_empty_file=False, allow_empty_file=False,
@ -64,6 +67,27 @@ class AddPreviewFileForm(forms.ModelForm):
self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'}) self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'})
super().__init__(*args, **kwargs) 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): def save(self, *args, **kwargs):
"""Save Preview from the cleaned form data.""" """Save Preview from the cleaned form data."""
# Fill in missing fields from request and the source file # Fill in missing fields from request and the source file
@ -81,6 +105,8 @@ class AddPreviewFileForm(forms.ModelForm):
class AddPreviewModelFormSet(forms.BaseModelFormSet): class AddPreviewModelFormSet(forms.BaseModelFormSet):
msg_duplicate_file = _('Please select another file instead of the duplicate')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request') self.request = kwargs.pop('request')
self.extension = kwargs.pop('extension') self.extension = kwargs.pop('extension')
@ -94,6 +120,14 @@ class AddPreviewModelFormSet(forms.BaseModelFormSet):
form_kwargs['extension'] = self.extension form_kwargs['extension'] = self.extension
return form_kwargs 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( AddPreviewFormSet = forms.modelformset_factory(
files.models.File, files.models.File,
@ -104,6 +138,16 @@ AddPreviewFormSet = forms.modelformset_factory(
class ExtensionUpdateForm(forms.ModelForm): 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: class Meta:
model = extensions.models.Extension model = extensions.models.Extension
fields = ( fields = (
@ -111,17 +155,79 @@ class ExtensionUpdateForm(forms.ModelForm):
'support', 'support',
) )
def clean(self): def __init__(self, *args, **kwargs):
super().clean() """Pass the request and initialise all the nested form(set)s."""
if ( self.request = kwargs.pop('request')
'convert_to_draft' in self.data super().__init__(*args, **kwargs)
and self.instance.status != self.instance.STATUSES.AWAITING_REVIEW if self.request.POST:
): edit_preview_formset = EditPreviewFormSet(
self.add_error( self.request.POST, self.request.FILES, instance=self.instance
None, 'An extension can be converted to draft only while it is Awating Review'
) )
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 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 ExtensionDeleteForm(forms.ModelForm):
class Meta: class Meta:

View File

@ -69,7 +69,7 @@
highlight what makes your {{ type }} special. highlight what makes your {{ type }} special.
{% endblocktranslate %} {% endblocktranslate %}
</p> </p>
{% include "extensions/manage/components/edit_previews.html" %}
{% include "extensions/manage/components/add_previews.html" %} {% include "extensions/manage/components/add_previews.html" %}
</div> </div>
</section> </section>

View File

@ -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 %}

View File

@ -41,49 +41,7 @@
<section class="mt-4"> <section class="mt-4">
<h2>{% trans 'Previews' %}</h2> <h2>{% trans 'Previews' %}</h2>
<div class="previews-upload"> <div class="previews-upload">
{{ edit_preview_formset.management_form }} {% include "extensions/manage/components/edit_previews.html" %}
{{ 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/add_previews.html" %} {% include "extensions/manage/components/add_previews.html" %}
</div> </div>
</section> </section>

View File

@ -85,6 +85,24 @@ EXPECTED_VALIDATION_ERRORS = {
'invalid-manifest-toml.zip': {'source': ['Could not parse the manifest file.']}, '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.']}, '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): class SubmitFileTest(TestCase):
@ -280,20 +298,19 @@ class SubmitFinaliseTest(TestCase):
def test_post_finalise_addon_validation_errors(self): def test_post_finalise_addon_validation_errors(self):
self.client.force_login(self.file.user) 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.assertEqual(response.status_code, 200)
self.assertDictEqual( self.assertDictEqual(
_get_all_form_errors(response), _get_all_form_errors(response),
{ {
'form': {}, 'form': [{}, None],
'extension_form': { 'extension_form': [{'description': ['This field is required.']}, None],
'description': ['This field is required.'], 'add_preview_formset': [[], ['Please add at least one preview.']],
}, 'edit_preview_formset': [[], []],
'add_preview_formset': [],
}, },
) )
self.assertFalse('TODO: It should include preview as required')
def test_post_finalise_addon_creates_addon_with_version_awaiting_review(self): def test_post_finalise_addon_creates_addon_with_version_awaiting_review(self):
self.assertEqual(File.objects.count(), 1) self.assertEqual(File.objects.count(), 1)
@ -318,6 +335,15 @@ class SubmitFinaliseTest(TestCase):
'form-0-caption': ['First Preview Caption Text'], 'form-0-caption': ['First Preview Caption Text'],
'form-1-id': '', 'form-1-id': '',
'form-1-caption': ['Second Preview Caption Text'], '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 for Approval.
'submit_draft': '', 'submit_draft': '',
} }

View File

@ -3,6 +3,7 @@ from pathlib import Path
from django.test import TestCase from django.test import TestCase
from common.tests.factories.extensions import create_approved_version, create_version 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 common.tests.utils import _get_all_form_errors
from extensions.models import Extension from extensions.models import Extension
from files.models import File from files.models import File
@ -201,6 +202,45 @@ class UpdateTest(TestCase):
response = self.client.post(url, {**data, **files}) response = self.client.post(url, {**data, **files})
self.assertEqual(response.status_code, 200) 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( self.assertEqual(
response.context['add_preview_formset'].forms[0].errors, response.context['add_preview_formset'].forms[0].errors,
{'source': ['File with this Hash already exists.']}, {'source': ['File with this Hash already exists.']},

View File

@ -1,10 +1,8 @@
"""Contains views allowing developers to manage their add-ons.""" """Contains views allowing developers to manage their add-ons."""
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, reverse 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 import DetailView, ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
@ -16,8 +14,6 @@ from .mixins import (
DraftVersionMixin, DraftVersionMixin,
) )
from extensions.forms import ( from extensions.forms import (
EditPreviewFormSet,
AddPreviewFormSet,
ExtensionDeleteForm, ExtensionDeleteForm,
ExtensionUpdateForm, ExtensionUpdateForm,
VersionForm, VersionForm,
@ -26,7 +22,6 @@ from extensions.forms import (
from extensions.models import Extension, Version from extensions.models import Extension, Version
from files.forms import FileForm from files.forms import FileForm
from files.models import File from files.models import File
from reviewers.models import ApprovalActivity
from stats.models import ExtensionView from stats.models import ExtensionView
import ratings.models import ratings.models
@ -109,7 +104,12 @@ class UpdateExtensionView(
template_name = 'extensions/manage/update.html' template_name = 'extensions/manage/update.html'
form_class = ExtensionUpdateForm form_class = ExtensionUpdateForm
success_message = "Updated successfully" 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): def get(self, request, *args, **kwargs):
extension = self.extension extension = self.extension
@ -124,62 +124,13 @@ class UpdateExtensionView(
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
edit_preview_formset = kwargs.pop('edit_preview_formset', None) context['edit_preview_formset'] = context['form'].edit_preview_formset
add_preview_formset = kwargs.pop('add_preview_formset', None) context['add_preview_formset'] = context['form'].add_preview_formset
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 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 @transaction.atomic
def form_valid(self, form): def form_valid(self, *args, **kwargs):
if 'convert_to_draft' in self.request.POST: return super().form_valid(*args, **kwargs)
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)
class DeleteExtensionView( class DeleteExtensionView(
@ -311,14 +262,6 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
template_name = 'extensions/new_version_finalise.html' template_name = 'extensions/new_version_finalise.html'
form_class = VersionForm 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': def _get_extension(self) -> 'Extension':
return get_object_or_404(Extension, slug=self.kwargs['slug']) return get_object_or_404(Extension, slug=self.kwargs['slug'])
@ -374,7 +317,6 @@ class DraftExtensionView(
): ):
template_name = 'extensions/draft_finalise.html' template_name = 'extensions/draft_finalise.html'
form_class = VersionForm form_class = VersionForm
msg_awaiting_review = _('Ready for review')
@property @property
def success_message(self) -> str: def success_message(self) -> str:
@ -398,60 +340,38 @@ class DraftExtensionView(
initial.update(**self.version.file.parsed_version_fields) initial.update(**self.version.file.parsed_version_fields)
return initial 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.""" """Add all the additional forms to the context."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not (add_preview_formset and extension_form): if not extension_form:
extension_form = ExtensionUpdateForm(instance=self.extension) extension_form = ExtensionUpdateForm(instance=self.extension, request=self.request)
add_preview_formset = AddPreviewFormSet(extension=self.extension, request=self.request)
context['extension_form'] = extension_form 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 return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Handle bound forms and valid/invalid logic with the extra forms.""" """Handle bound forms and valid/invalid logic with the extra forms."""
form = self.get_form() form = self.get_form()
extension_form = ExtensionUpdateForm( 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( if form.is_valid() and extension_form.is_valid():
self.request.POST, self.request.FILES, extension=self.extension, request=self.request return self.form_valid(form, extension_form)
) return self.form_invalid(form, extension_form)
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 @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. """Save all the forms in correct order.
Extension must be saved first. 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() extension_form.save()
add_preview_formset.save()
form.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) 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): def form_invalid(self, form, extension_form):
return self.render_to_response( return self.render_to_response(self.get_context_data(form, extension_form))
self.get_context_data(form, extension_form, add_preview_formset)
)
def get_success_url(self): def get_success_url(self):
return self.extension.get_manage_url() return self.extension.get_manage_url()

View File

@ -1,4 +1,5 @@
import os import os
import os.path
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
@ -12,7 +13,7 @@ import files.models
import files.tasks 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/') @override_settings(MEDIA_ROOT='/tmp/')
class FileScanTest(TestCase): class FileScanTest(TestCase):
def setUp(self): def setUp(self):

View File

@ -80,14 +80,16 @@ class Notification(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
if self.action.verb == Verb.RATED_EXTENSION: 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 [ elif self.action.verb in [
Verb.APPROVED, Verb.APPROVED,
Verb.COMMENTED, Verb.COMMENTED,
Verb.REQUESTED_CHANGES, Verb.REQUESTED_CHANGES,
Verb.REQUESTED_REVIEW, 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: elif self.action.action_object is not None:
url = self.action.action_object.get_absolute_url() url = self.action.action_object.get_absolute_url()
else: else:

View File

@ -78,9 +78,9 @@
<hr class="my-4"> <hr class="my-4">
<h2>Activity</h2> <h2>Activity</h2>
{% if extension.review_activity.all %} {% if review_activity %}
<ul class="activity-list"> <ul class="activity-list">
{% for activity in extension.review_activity.all %} {% for activity in review_activity %}
<li id="activity-{{ activity.id }}"> <li id="activity-{{ activity.id }}">
{% if activity.type in status_change_types %} {% if activity.type in status_change_types %}

View File

@ -77,8 +77,18 @@ class ExtensionsApprovalDetailView(DetailView):
template_name = 'reviewers/extensions_review_detail.html' 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**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 ctx['status_change_types'] = STATUS_CHANGE_TYPES
if self.request.user.is_authenticated: if self.request.user.is_authenticated: