extensions-website/abuse/models.py
Oleg Komarov 012845c712 Abuse reports: moderator form for resolving/dismissing + notification (#173)
It is possible to submit the form update the note and status multiple times,
e.g. a report that was initially dismissed, can be resolved later.
Each valid form submission will generate a notification for the reporter.

Once a note is saved, it becomes visible to the reporter.

The migration in this PR replaces the AbuseReport status Confirmed(=2)
that wasn't used in production with Dismissed(=2). Assuming that this is
a minor sin while the project is still in beta.

Reviewed-on: #173
Reviewed-by: Anna Sirota <annasirota@noreply.localhost>
2024-06-07 17:04:52 +02:00

119 lines
4.2 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'),
('DISMISSED', 2, 'Dismissed'),
('RESOLVED', 3, 'Resolved'),
)
# 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.CASCADE
)
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.OTHER, 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)
moderator_note = models.TextField(blank=True, null=True)
processed_by = models.ForeignKey(
User,
null=True,
related_name='abuse_reports_processed',
on_delete=models.PROTECT,
)
@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])