Anna Sirota
caae613747
* removes all soft-deletion; * shows a "Delete extension" button on the draft page in case it can be deleted; * shows a "Delete version" button on the version page in case it can be deleted; * a version can be deleted if * its file isn't approved, and it doesn't have any ratings; * an extension can be deleted if * it's not listed, and doesn't have any ratings or abuse reports; * all it's versions can also be deleted; * changes default `File.status` from `APPROVED` to `AWAITING_REVIEW` With version's file status being `APPROVED` by default, a version can never be deleted, even when the extension is still a draft. This change doesn't affect the approval process because * when an extension is approved its latest version becomes approved automatically (no change here); * when a new version is uploaded to an approved extension, it's approved automatically (this is new). This allows authors to delete their drafts, freeing the extension slug and making it possible to re-upload the same file. This also makes it possible to easily fix mistakes during the drafting of a new extension (e.g. delete a version and re-upload it without bumping a version for each typo/mistake in packaging and so on). (see #78 and #63) Reviewed-on: #81
169 lines
6.2 KiB
Python
169 lines
6.2 KiB
Python
from typing import Union
|
|
import logging
|
|
|
|
from actstream.actions import follow, unfollow
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Group
|
|
from django.db.models.signals import m2m_changed, pre_save, post_save, pre_delete, post_delete
|
|
from django.dispatch import receiver
|
|
|
|
from constants.activity import Flag
|
|
import extensions.models
|
|
import files.models
|
|
|
|
logger = logging.getLogger(__name__)
|
|
User = get_user_model()
|
|
|
|
|
|
@receiver(pre_delete, sender=extensions.models.Extension)
|
|
@receiver(pre_delete, sender=extensions.models.Preview)
|
|
@receiver(pre_delete, sender=extensions.models.Version)
|
|
def _log_extension_delete(sender: object, instance: object, **kwargs: object) -> None:
|
|
cannot_be_deleted_reasons = instance.cannot_be_deleted_reasons
|
|
if len(cannot_be_deleted_reasons) > 0:
|
|
# This shouldn't happen: prior validation steps should have taken care of this.
|
|
# raise ValidationError({'__all__': cannot_be_deleted_reasons})
|
|
args = {'sender': sender, 'pk': instance.pk, 'reasons': cannot_be_deleted_reasons}
|
|
logger.error("%(sender)s pk=%(pk)s is being deleted but it %(reasons)s", args)
|
|
|
|
logger.info('Deleting %s pk=%s "%s"', sender, instance.pk, str(instance))
|
|
|
|
|
|
@receiver(post_delete, sender=extensions.models.Preview)
|
|
@receiver(post_delete, sender=extensions.models.Version)
|
|
def _delete_file(sender: object, instance: object, **kwargs: object) -> None:
|
|
f = instance.file
|
|
args = {'f_id': f.pk, 'h': f.hash, 'pk': instance.pk, 'sender': sender, 's': f.source.name}
|
|
logger.info('Deleting file pk=%(f_id)s s=%(s)s hash=%(h)s linked to %(sender)s pk=%(pk)s', args)
|
|
f.delete()
|
|
# TODO: this doesn't mean that the file was deleted from disk
|
|
|
|
|
|
@receiver(pre_save, sender=extensions.models.Extension)
|
|
@receiver(pre_save, sender=extensions.models.Version)
|
|
def _record_changes(
|
|
sender: object,
|
|
instance: Union[extensions.models.Extension, extensions.models.Version],
|
|
update_fields: object,
|
|
**kwargs: object,
|
|
) -> None:
|
|
was_changed, old_state = instance.pre_save_record(update_fields=update_fields)
|
|
|
|
if hasattr(instance, 'name'):
|
|
instance.sanitize('name', was_changed, old_state, **kwargs)
|
|
if hasattr(instance, 'description'):
|
|
instance.sanitize('description', was_changed, old_state, **kwargs)
|
|
|
|
instance.record_status_change(was_changed, old_state, **kwargs)
|
|
|
|
|
|
@receiver(post_save, sender=extensions.models.Extension)
|
|
def _update_search_index(sender, instance, **kw):
|
|
pass # TODO: update search index
|
|
|
|
|
|
def extension_should_be_listed(extension):
|
|
return (
|
|
extension.latest_version is not None
|
|
and extension.latest_version.is_listed
|
|
and extension.status == extension.STATUSES.APPROVED
|
|
)
|
|
|
|
|
|
@receiver(post_save, sender=extensions.models.Extension)
|
|
@receiver(post_save, sender=extensions.models.Version)
|
|
@receiver(post_save, sender=files.models.File)
|
|
def _set_is_listed(
|
|
sender: object,
|
|
instance: Union[extensions.models.Extension, extensions.models.Version, files.models.File],
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> None:
|
|
if isinstance(instance, extensions.models.Extension):
|
|
extension = instance
|
|
elif isinstance(instance, extensions.models.Version):
|
|
extension = instance.extension
|
|
else:
|
|
# Some file types (e.g., image or video) have no version associated to them.
|
|
# But also files which were created but have not yet being related to the versions.
|
|
# Since signals is called very early on, we can't assume file.extension will be available.
|
|
if not hasattr(instance, 'version'):
|
|
return
|
|
extension = instance.extension
|
|
|
|
old_is_listed = extension.is_listed
|
|
new_is_listed = extension_should_be_listed(extension)
|
|
|
|
if old_is_listed == new_is_listed:
|
|
return
|
|
|
|
if extension.status == extensions.models.Extension.STATUSES.APPROVED and not new_is_listed:
|
|
extension.status = extensions.models.Extension.STATUSES.INCOMPLETE
|
|
|
|
logger.info('Extension pk=%s becomes listed', extension.pk)
|
|
extension.is_listed = new_is_listed
|
|
extension.save()
|
|
|
|
|
|
@receiver(post_save, sender=extensions.models.Extension)
|
|
def _setup_followers(
|
|
sender: object,
|
|
instance: extensions.models.Extension,
|
|
created: bool,
|
|
**kwargs: object,
|
|
) -> None:
|
|
if not created:
|
|
return
|
|
|
|
for user in instance.authors.all():
|
|
follow(user, instance, send_action=False, flag=Flag.AUTHOR)
|
|
for user in Group.objects.get(name='moderators').user_set.all():
|
|
follow(user, instance, send_action=False, flag=Flag.MODERATOR)
|
|
|
|
|
|
@receiver(m2m_changed, sender=extensions.models.Extension.authors.through)
|
|
def _update_authors_follow(instance, action, model, reverse, pk_set, **kwargs):
|
|
if action not in ['post_add', 'post_remove']:
|
|
return
|
|
|
|
if model == extensions.models.Extension and not reverse:
|
|
targets = extensions.models.Extension.objects.filter(pk__in=pk_set)
|
|
users = [instance]
|
|
else:
|
|
targets = [instance]
|
|
users = User.objects.filter(pk__in=pk_set)
|
|
|
|
for user in users:
|
|
for extension in targets:
|
|
if action == 'post_remove':
|
|
unfollow(user, extension, send_action=False, flag=Flag.AUTHOR)
|
|
elif action == 'post_add':
|
|
follow(user, extension, send_action=False, flag=Flag.AUTHOR)
|
|
|
|
|
|
@receiver(post_save, sender=extensions.models.Preview)
|
|
@receiver(post_save, sender=extensions.models.Version)
|
|
def _auto_approve_subsequent_uploads(
|
|
sender: object,
|
|
instance: Union[extensions.models.Preview, extensions.models.Version],
|
|
created: bool,
|
|
raw: bool,
|
|
**kwargs: object,
|
|
):
|
|
if raw:
|
|
return
|
|
if not created:
|
|
return
|
|
if not instance.file_id:
|
|
return
|
|
|
|
# N.B.: currently, subsequent version and preview uploads get approved automatically,
|
|
# if extension is currently listed (meaning, it was approved by a human already).
|
|
extension = instance.extension
|
|
file = instance.file
|
|
if extension.is_listed:
|
|
file.status = files.models.File.STATUSES.APPROVED
|
|
args = {'f_id': file.pk, 'pk': instance.pk, 'sender': sender, 's': file.source.name}
|
|
logger.info('Auto-approving file pk=%(f_id)s of %(sender)s pk=%(pk)s source=%(s)s', args)
|
|
file.save(update_fields={'status', 'date_modified'})
|