extensions-website/abuse/models.py
Anna Sirota caae613747 Make it possible to fully delete unlisted/unrated extensions and versions (#81)
* 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
2024-04-19 11:00:13 +02:00

112 lines
4.0 KiB
Python

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from django.core.validators import validate_ipv46_address
from django.db import models
from django.urls import reverse
from extended_choices import Choices
from geoip2.errors import GeoIP2Error
from constants.base import ABUSE_TYPE, ABUSE_TYPE_EXTENSION, ABUSE_TYPE_RATING
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin
import extensions.fields
User = get_user_model()
class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
TYPE = ABUSE_TYPE
REASONS = Choices(
('OTHER', 127, 'Other'),
('DAMAGE', 1, 'Damages computer and/or data'),
('SPAM', 2, 'Creates spam or advertising'),
('BROKEN', 3, "Doesn't work, breaks Blender, or slows it down"),
('POLICY', 4, 'Hateful, violent, or illegal content'),
('DECEPTIVE', 5, "Pretends to be something it's not"),
)
STATUSES = Choices(
('UNTRIAGED', 1, 'Untriaged'),
('VALID', 2, 'Valid'),
('SUSPICIOUS', 3, 'Suspicious'),
)
# NULL if the reporter is anonymous.
# FIXME? make non-null
reporter = models.ForeignKey(
User,
null=True,
blank=True,
related_name='abuse_reported',
on_delete=models.SET_NULL,
)
# An abuse report can be for an extension or a user.
# If user is set then extension should be null.
# If extension is null then user should be set.
user = models.ForeignKey(
User, null=True, related_name='abuse_reports', on_delete=models.SET_NULL
)
extension = models.ForeignKey(
'extensions.Extension', blank=True, null=True, on_delete=models.CASCADE
)
extension_version = extensions.fields.VersionStringField(max_length=64, null=True, blank=True)
rating = models.ForeignKey('ratings.Rating', blank=True, null=True, on_delete=models.CASCADE)
message = models.TextField(blank=True)
reason = models.PositiveSmallIntegerField(
default=REASONS[0][1], choices=REASONS.choices, blank=False, null=False
)
version = extensions.fields.VersionStringField(
max_length=64,
null=True,
blank=True,
help_text=('Version of Blender affected by this report, if applicable.'),
)
type = models.PositiveSmallIntegerField(
default=ABUSE_TYPE_EXTENSION, choices=TYPE.choices, blank=False, null=False
)
status = models.PositiveSmallIntegerField(default=STATUSES.UNTRIAGED, choices=STATUSES.choices)
@classmethod
def lookup_country_code_from_ip(cls, ip):
try:
# Early check to avoid initializing GeoIP2 on invalid addresses
if not ip:
raise forms.ValidationError('No IP')
validate_ipv46_address(ip)
geoip = GeoIP2()
value = geoip.country_code(ip)
# Annoyingly, we have to catch both django's GeoIP2Exception (setup
# issue) and geoip2's GeoIP2Error (lookup issue)
except (forms.ValidationError, GeoIP2Exception, GeoIP2Error):
value = ''
return value
@property
def name(self) -> str:
return self.extension.name if self.extension else self.user
def __str__(self) -> str:
return f'Abuse Report for {self.type} {self.name}'
@classmethod
def exists(cls, user_id: int, extension_id: int, rating_id: int = None) -> bool:
if rating_id is None:
return cls.objects.filter(
reporter_id=user_id, extension_id=extension_id, type=ABUSE_TYPE_EXTENSION
).exists()
return cls.objects.filter(
reporter_id=user_id,
extension_id=extension_id,
rating_id=rating_id,
type=ABUSE_TYPE_RATING,
).exists()
def get_absolute_url(self):
return reverse('abuse:view-report', args=[self.pk])
def get_admin_url(self):
return reverse('admin:abuse_abusereport_change', args=[self.pk])