295 lines
10 KiB
Python
295 lines
10 KiB
Python
import logging
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from constants.base import (
|
|
ALLOWED_FEATURED_IMAGE_MIMETYPES,
|
|
ALLOWED_ICON_MIMETYPES,
|
|
ALLOWED_PREVIEW_MIMETYPES,
|
|
FILE_STATUS_CHOICES,
|
|
)
|
|
|
|
import extensions.models
|
|
import files.forms
|
|
import files.models
|
|
import reviewers.models
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EditPreviewForm(forms.ModelForm):
|
|
class Meta:
|
|
model = extensions.models.Extension.previews.through
|
|
fields = (
|
|
'caption',
|
|
'position',
|
|
)
|
|
widgets = {
|
|
'position': forms.HiddenInput(attrs={'data-position': ''}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'})
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
EditPreviewFormSet = forms.inlineformset_factory(
|
|
extensions.models.Extension,
|
|
extensions.models.Extension.previews.through,
|
|
form=EditPreviewForm,
|
|
extra=0,
|
|
)
|
|
|
|
|
|
class AddPreviewFileForm(files.forms.BaseMediaFileForm):
|
|
allowed_mimetypes = ALLOWED_PREVIEW_MIMETYPES
|
|
error_messages = {'invalid_mimetype': _('Choose a JPEG, PNG or WebP image, or an MP4 video')}
|
|
|
|
class Meta(files.forms.BaseMediaFileForm.Meta):
|
|
fields = ('caption',) + files.forms.BaseMediaFileForm.Meta.fields
|
|
|
|
caption = forms.CharField(max_length=255, required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'})
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['source'].allow_empty_file = False
|
|
self.fields['source'].required = True
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Save Preview from the cleaned form data."""
|
|
instance = super().save(*args, **kwargs)
|
|
|
|
# Create extension preview and save caption to it
|
|
extensions.models.Preview.objects.create(
|
|
file=instance,
|
|
caption=self.cleaned_data['caption'],
|
|
extension=self.extension,
|
|
)
|
|
return instance
|
|
|
|
|
|
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')
|
|
super().__init__(*args, **kwargs)
|
|
# Make sure formset doesn't attempt to select existing File records
|
|
self.queryset = files.models.File.objects.none()
|
|
|
|
def get_form_kwargs(self, *args, **kwargs):
|
|
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
|
form_kwargs['request'] = self.request
|
|
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,
|
|
form=AddPreviewFileForm,
|
|
formset=AddPreviewModelFormSet,
|
|
extra=1,
|
|
)
|
|
|
|
|
|
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 = (
|
|
'description',
|
|
'support',
|
|
)
|
|
|
|
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,
|
|
)
|
|
featured_image_form = FeaturedImageForm(
|
|
self.request.POST,
|
|
self.request.FILES,
|
|
extension=self.instance,
|
|
request=self.request,
|
|
)
|
|
icon_form = IconForm(
|
|
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)
|
|
featured_image_form = FeaturedImageForm(extension=self.instance, request=self.request)
|
|
icon_form = IconForm(extension=self.instance, request=self.request)
|
|
self.edit_preview_formset = edit_preview_formset
|
|
self.add_preview_formset = add_preview_formset
|
|
self.featured_image_form = featured_image_form
|
|
self.icon_form = icon_form
|
|
|
|
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."""
|
|
if 'submit_draft' in self.data:
|
|
# Require at least one preview image when requesting a review
|
|
if not self.instance.previews.exists():
|
|
self.add_preview_formset.min_num = 1
|
|
self.add_preview_formset.validate_min = True
|
|
# Make feature image and icon required too
|
|
self.featured_image_form.fields['source'].required = True
|
|
self.icon_form.fields['source'].required = True
|
|
|
|
is_valid_flags = [
|
|
self.edit_preview_formset.is_valid(),
|
|
self.add_preview_formset.is_valid(),
|
|
self.featured_image_form.is_valid(),
|
|
self.icon_form.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()
|
|
|
|
# Featured image and icon are only required when ready for review,
|
|
# and can be empty or unchanged.
|
|
if self.featured_image_form.has_changed():
|
|
# Automatically approve featured image of an already approved extension
|
|
if self.instance.is_approved:
|
|
self.featured_image_form.instance.status = FILE_STATUS_CHOICES.APPROVED
|
|
self.featured_image_form.save()
|
|
if self.icon_form.has_changed():
|
|
# Automatically approve icon of an already approved extension
|
|
if self.instance.is_approved:
|
|
self.icon_form.instance.status = FILE_STATUS_CHOICES.APPROVED
|
|
self.icon_form.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:
|
|
model = extensions.models.Extension
|
|
fields = []
|
|
|
|
|
|
class VersionForm(forms.ModelForm):
|
|
class Meta:
|
|
model = extensions.models.Version
|
|
fields = {'file', 'release_notes'}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Limit 'file' choices to the initial file value."""
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Mark 'file' field as disabled so that Django form allows using its initial value.
|
|
self.fields['file'].disabled = True
|
|
|
|
def clean_file(self, *args, **kwargs):
|
|
"""Return file that was passed to the form via the initial values.
|
|
|
|
This ensures that it doesn't have to be supplied by the form data.
|
|
"""
|
|
return self.initial['file']
|
|
|
|
|
|
class VersionDeleteForm(forms.ModelForm):
|
|
class Meta:
|
|
model = extensions.models.Version
|
|
fields = []
|
|
|
|
|
|
class FeaturedImageForm(files.forms.BaseMediaFileForm):
|
|
prefix = 'featured-image'
|
|
to_field = 'featured_image'
|
|
allowed_mimetypes = ALLOWED_FEATURED_IMAGE_MIMETYPES
|
|
error_messages = {'invalid_mimetype': _('Choose a JPEG, PNG or WebP image')}
|
|
|
|
|
|
class IconForm(files.forms.BaseMediaFileForm):
|
|
prefix = 'icon'
|
|
to_field = 'icon'
|
|
allowed_mimetypes = ALLOWED_ICON_MIMETYPES
|
|
error_messages = {
|
|
'invalid_mimetype': _('Choose a PNG image'),
|
|
'invalid_size_px': _('Choose a 256 x 256 PNG image'),
|
|
}
|
|
expected_size_px = 256
|
|
|
|
def clean_source(self):
|
|
"""Check image resolution."""
|
|
source = self.cleaned_data.get('source')
|
|
if not source:
|
|
return
|
|
image = getattr(source, 'image', None)
|
|
if not image:
|
|
return
|
|
if image.width > self.expected_size_px or image.height > self.expected_size_px:
|
|
raise ValidationError(self.error_messages['invalid_size_px'])
|
|
return source
|