Oleg Komarov
012845c712
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>
119 lines
4.2 KiB
Python
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])
|