Implement Web Assets' theme system and selection, and add 'light' theme #118
@ -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" %}
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
<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>
|
||||||
|
@ -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': '',
|
||||||
}
|
}
|
||||||
|
@ -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.']},
|
||||||
|
@ -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:
|
extension_form.save()
|
||||||
# Send the extension and version to the review
|
form.save()
|
||||||
if 'submit_draft' in self.request.POST:
|
return super().form_valid(form)
|
||||||
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):
|
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()
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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 %}
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user