aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api/models
diff options
context:
space:
mode:
authorGravatar Numerlor <[email protected]>2021-03-14 23:49:00 +0100
committerGravatar Numerlor <[email protected]>2021-03-15 00:39:58 +0100
commitf9a5c68a4f5400f6963f0795071110ebdc4eebbc (patch)
treee8534db8528370145a42f3e6340daf6f3de6a8cb /pydis_site/apps/api/models
parentCreate migration for doc package name validator. (diff)
parentDockerfile optimisations (diff)
Merge branch 'main' into doc-validator
Diffstat (limited to 'pydis_site/apps/api/models')
-rw-r--r--pydis_site/apps/api/models/__init__.py3
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py3
-rw-r--r--pydis_site/apps/api/models/bot/deleted_message.py4
-rw-r--r--pydis_site/apps/api/models/bot/documentation_link.py5
-rw-r--r--pydis_site/apps/api/models/bot/infraction.py3
-rw-r--r--pydis_site/apps/api/models/bot/message.py10
-rw-r--r--pydis_site/apps/api/models/bot/message_deletion_context.py11
-rw-r--r--pydis_site/apps/api/models/bot/metricity.py91
-rw-r--r--pydis_site/apps/api/models/bot/nomination.py58
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py5
-rw-r--r--pydis_site/apps/api/models/bot/offensive_message.py9
-rw-r--r--pydis_site/apps/api/models/bot/role.py8
-rw-r--r--pydis_site/apps/api/models/bot/user.py17
-rw-r--r--pydis_site/apps/api/models/log_entry.py55
-rw-r--r--pydis_site/apps/api/models/utils.py (renamed from pydis_site/apps/api/models/bot/tag.py)43
15 files changed, 207 insertions, 118 deletions
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index 1d0ab7ea..fd5bf220 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -8,11 +8,10 @@ from .bot import (
Message,
MessageDeletionContext,
Nomination,
+ NominationEntry,
OffensiveMessage,
OffTopicChannelName,
Reminder,
Role,
- Tag,
User
)
-from .log_entry import LogEntry
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index efd98184..ac864de3 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -6,10 +6,9 @@ from .documentation_link import DocumentationLink
from .infraction import Infraction
from .message import Message
from .message_deletion_context import MessageDeletionContext
-from .nomination import Nomination
+from .nomination import Nomination, NominationEntry
from .off_topic_channel_name import OffTopicChannelName
from .offensive_message import OffensiveMessage
from .reminder import Reminder
from .role import Role
-from .tag import Tag
from .user import User
diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py
index 1eb4516e..50b70d8c 100644
--- a/pydis_site/apps/api/models/bot/deleted_message.py
+++ b/pydis_site/apps/api/models/bot/deleted_message.py
@@ -14,6 +14,6 @@ class DeletedMessage(Message):
)
class Meta:
- """Sets the default ordering for list views to oldest first."""
+ """Sets the default ordering for list views to newest first."""
- ordering = ["id"]
+ ordering = ("-id",)
diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py
index 4f2bd2ab..529d26d1 100644
--- a/pydis_site/apps/api/models/bot/documentation_link.py
+++ b/pydis_site/apps/api/models/bot/documentation_link.py
@@ -33,3 +33,8 @@ class DocumentationLink(ModelReprMixin, models.Model):
def __str__(self):
"""Returns the package and URL for the current documentation link, for display purposes."""
return f"{self.package} - {self.base_url}"
+
+ class Meta:
+ """Defines the meta options for the documentation link model."""
+
+ ordering = ['package']
diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py
index 7660cbba..60c1e8dd 100644
--- a/pydis_site/apps/api/models/bot/infraction.py
+++ b/pydis_site/apps/api/models/bot/infraction.py
@@ -15,7 +15,8 @@ class Infraction(ModelReprMixin, models.Model):
("mute", "Mute"),
("kick", "Kick"),
("ban", "Ban"),
- ("superstar", "Superstar")
+ ("superstar", "Superstar"),
+ ("voice_ban", "Voice Ban"),
)
inserted_at = models.DateTimeField(
default=timezone.now,
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index 78dcbf1d..ff06de21 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -5,9 +5,9 @@ from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
-from pydis_site.apps.api.models.bot.tag import validate_tag_embed
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.models.mixins import ModelReprMixin
+from pydis_site.apps.api.models.utils import validate_embed
class Message(ModelReprMixin, models.Model):
@@ -21,7 +21,8 @@ class Message(ModelReprMixin, models.Model):
limit_value=0,
message="Message IDs cannot be negative."
),
- )
+ ),
+ verbose_name="ID"
)
author = models.ForeignKey(
User,
@@ -38,7 +39,8 @@ class Message(ModelReprMixin, models.Model):
limit_value=0,
message="Channel IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Channel ID"
)
content = models.CharField(
max_length=2_000,
@@ -47,7 +49,7 @@ class Message(ModelReprMixin, models.Model):
)
embeds = pgfields.ArrayField(
pgfields.JSONField(
- validators=(validate_tag_embed,)
+ validators=(validate_embed,)
),
blank=True,
help_text="Embeds attached to this message."
diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py
index 04ae8d34..1410250a 100644
--- a/pydis_site/apps/api/models/bot/message_deletion_context.py
+++ b/pydis_site/apps/api/models/bot/message_deletion_context.py
@@ -1,4 +1,5 @@
from django.db import models
+from django_hosts.resolvers import reverse
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.models.mixins import ModelReprMixin
@@ -28,3 +29,13 @@ class MessageDeletionContext(ModelReprMixin, models.Model):
# the deletion context does not take place in the future.
help_text="When this deletion took place."
)
+
+ @property
+ def log_url(self) -> str:
+ """Create the url for the deleted message logs."""
+ return reverse('logs', host="staff", args=(self.id,))
+
+ class Meta:
+ """Set the ordering for list views to newest first."""
+
+ ordering = ("-creation",)
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
new file mode 100644
index 00000000..cae630f1
--- /dev/null
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -0,0 +1,91 @@
+from django.db import connections
+
+BLOCK_INTERVAL = 10 * 60 # 10 minute blocks
+
+EXCLUDE_CHANNELS = [
+ "267659945086812160", # Bot commands
+ "607247579608121354" # SeasonalBot commands
+]
+
+
+class NotFound(Exception):
+ """Raised when an entity cannot be found."""
+
+ pass
+
+
+class Metricity:
+ """Abstraction for a connection to the metricity database."""
+
+ def __init__(self):
+ self.cursor = connections['metricity'].cursor()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_):
+ self.cursor.close()
+
+ def user(self, user_id: str) -> dict:
+ """Query a user's data."""
+ # TODO: Swap this back to some sort of verified at date
+ columns = ["joined_at"]
+ query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'"
+ self.cursor.execute(query, [user_id])
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return dict(zip(columns, values))
+
+ def total_messages(self, user_id: str) -> int:
+ """Query total number of messages for a user."""
+ self.cursor.execute(
+ """
+ SELECT
+ COUNT(*)
+ FROM messages
+ WHERE
+ author_id = '%s'
+ AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
+ """,
+ [user_id, EXCLUDE_CHANNELS]
+ )
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return values[0]
+
+ def total_message_blocks(self, user_id: str) -> int:
+ """
+ Query number of 10 minute blocks during which the user has been active.
+
+ This metric prevents users from spamming to achieve the message total threshold.
+ """
+ self.cursor.execute(
+ """
+ SELECT
+ COUNT(*)
+ FROM (
+ SELECT
+ (floor((extract('epoch' from created_at) / %s )) * %s) AS interval
+ FROM messages
+ WHERE
+ author_id='%s'
+ AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
+ GROUP BY interval
+ ) block_query;
+ """,
+ [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS]
+ )
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return values[0]
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index 21e34e87..221d8534 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -5,21 +5,12 @@ from pydis_site.apps.api.models.mixins import ModelReprMixin
class Nomination(ModelReprMixin, models.Model):
- """A helper nomination created by staff."""
+ """A general helper nomination information created by staff."""
active = models.BooleanField(
default=True,
help_text="Whether this nomination is still relevant."
)
- actor = models.ForeignKey(
- User,
- on_delete=models.CASCADE,
- help_text="The staff member that nominated this user.",
- related_name='nomination_set'
- )
- reason = models.TextField(
- help_text="Why this user was nominated."
- )
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
@@ -32,15 +23,60 @@ class Nomination(ModelReprMixin, models.Model):
)
end_reason = models.TextField(
help_text="Why the nomination was ended.",
- default=""
+ default="",
+ blank=True
)
ended_at = models.DateTimeField(
auto_now_add=False,
help_text="When the nomination was ended.",
null=True
)
+ reviewed = models.BooleanField(
+ default=False,
+ help_text="Whether a review was made."
+ )
def __str__(self):
"""Representation that makes the target and state of the nomination immediately evident."""
status = "active" if self.active else "ended"
return f"Nomination of {self.user} ({status})"
+
+ class Meta:
+ """Set the ordering of nominations to most recent first."""
+
+ ordering = ("-inserted_at",)
+
+
+class NominationEntry(ModelReprMixin, models.Model):
+ """A nomination entry created by a single staff member."""
+
+ nomination = models.ForeignKey(
+ Nomination,
+ on_delete=models.CASCADE,
+ help_text="The nomination this entry belongs to.",
+ related_name="entries"
+ )
+ actor = models.ForeignKey(
+ User,
+ on_delete=models.CASCADE,
+ help_text="The staff member that nominated this user.",
+ related_name='nomination_set'
+ )
+ reason = models.TextField(
+ help_text="Why the actor nominated this user.",
+ default="",
+ blank=True
+ )
+ inserted_at = models.DateTimeField(
+ auto_now_add=True,
+ help_text="The creation date of this nomination entry."
+ )
+
+ class Meta:
+ """Meta options for NominationEntry model."""
+
+ verbose_name_plural = "nomination entries"
+
+ # Set default ordering here to latest first
+ # so we don't need to define it everywhere
+ ordering = ("-inserted_at",)
diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
index 20e77b9f..403c7465 100644
--- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
@@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
help_text="The actual channel name that will be used on our Discord server."
)
+ used = models.BooleanField(
+ default=False,
+ help_text="Whether or not this name has already been used during this rotation",
+ )
+
def __str__(self):
"""Returns the current off-topic name, for display purposes."""
return self.name
diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py
index 6c0e5ffb..74dab59b 100644
--- a/pydis_site/apps/api/models/bot/offensive_message.py
+++ b/pydis_site/apps/api/models/bot/offensive_message.py
@@ -24,7 +24,8 @@ class OffensiveMessage(ModelReprMixin, models.Model):
limit_value=0,
message="Message IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Message ID"
)
channel_id = models.BigIntegerField(
help_text=(
@@ -36,11 +37,13 @@ class OffensiveMessage(ModelReprMixin, models.Model):
limit_value=0,
message="Channel IDs cannot be negative."
),
- )
+ ),
+ verbose_name="Channel ID"
)
delete_date = models.DateTimeField(
help_text="The date on which the message will be auto-deleted.",
- validators=(future_date_validator,)
+ validators=(future_date_validator,),
+ verbose_name="To Be Deleted"
)
def __str__(self):
diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py
index 721e4815..cfadfec4 100644
--- a/pydis_site/apps/api/models/bot/role.py
+++ b/pydis_site/apps/api/models/bot/role.py
@@ -22,7 +22,8 @@ class Role(ModelReprMixin, models.Model):
message="Role IDs cannot be negative."
),
),
- help_text="The role ID, taken from Discord."
+ help_text="The role ID, taken from Discord.",
+ verbose_name="ID"
)
name = models.CharField(
max_length=100,
@@ -65,3 +66,8 @@ class Role(ModelReprMixin, models.Model):
def __le__(self, other: Role) -> bool:
"""Compares the roles based on their position in the role hierarchy of the guild."""
return self.position <= other.position
+
+ class Meta:
+ """Set role ordering from highest to lowest position."""
+
+ ordering = ("-position",)
diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py
index cd2d58b9..afc5ba1e 100644
--- a/pydis_site/apps/api/models/bot/user.py
+++ b/pydis_site/apps/api/models/bot/user.py
@@ -26,11 +26,12 @@ class User(ModelReprMixin, models.Model):
message="User IDs cannot be negative."
),
),
+ verbose_name="ID",
help_text="The ID of this user, taken from Discord."
)
name = models.CharField(
max_length=32,
- help_text="The username, taken from Discord."
+ help_text="The username, taken from Discord.",
)
discriminator = models.PositiveSmallIntegerField(
validators=(
@@ -57,12 +58,13 @@ class User(ModelReprMixin, models.Model):
)
in_guild = models.BooleanField(
default=True,
- help_text="Whether this user is in our server."
+ help_text="Whether this user is in our server.",
+ verbose_name="In Guild"
)
def __str__(self):
"""Returns the name and discriminator for the current user, for display purposes."""
- return f"{self.name}#{self.discriminator:0>4}"
+ return f"{self.name}#{self.discriminator:04d}"
@property
def top_role(self) -> Role:
@@ -75,3 +77,12 @@ class User(ModelReprMixin, models.Model):
if not roles:
return Role.objects.get(name="Developers")
return max(roles)
+
+ @property
+ def username(self) -> str:
+ """
+ Returns the display version with name and discriminator as a standard attribute.
+
+ For usability in read-only fields such as Django Admin.
+ """
+ return str(self)
diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py
deleted file mode 100644
index 752cd2ca..00000000
--- a/pydis_site/apps/api/models/log_entry.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from django.db import models
-from django.utils import timezone
-
-from pydis_site.apps.api.models.mixins import ModelReprMixin
-
-
-class LogEntry(ModelReprMixin, models.Model):
- """A log entry generated by one of the PyDis applications."""
-
- application = models.CharField(
- max_length=20,
- help_text="The application that generated this log entry.",
- choices=(
- ('bot', 'Bot'),
- ('seasonalbot', 'Seasonalbot'),
- ('site', 'Website')
- )
- )
- logger_name = models.CharField(
- max_length=100,
- help_text="The name of the logger that generated this log entry."
- )
- timestamp = models.DateTimeField(
- default=timezone.now,
- help_text="The date and time when this entry was created."
- )
- level = models.CharField(
- max_length=8, # 'critical'
- choices=(
- ('debug', 'Debug'),
- ('info', 'Info'),
- ('warning', 'Warning'),
- ('error', 'Error'),
- ('critical', 'Critical')
- ),
- help_text=(
- "The logger level at which this entry was emitted. The levels "
- "correspond to the Python `logging` levels."
- )
- )
- module = models.CharField(
- max_length=100,
- help_text="The fully qualified path of the module generating this log line."
- )
- line = models.PositiveSmallIntegerField(
- help_text="The line at which the log line was emitted."
- )
- message = models.TextField(
- help_text="The textual content of the log line."
- )
-
- class Meta:
- """Customizes the default generated plural name to valid English."""
-
- verbose_name_plural = 'Log entries'
diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/utils.py
index 5e53582f..107231ba 100644
--- a/pydis_site/apps/api/models/bot/tag.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -1,12 +1,8 @@
from collections.abc import Mapping
from typing import Any, Dict
-from django.contrib.postgres import fields as pgfields
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
-from django.db import models
-
-from pydis_site.apps.api.models.mixins import ModelReprMixin
def is_bool_validator(value: Any) -> None:
@@ -15,7 +11,7 @@ def is_bool_validator(value: Any) -> None:
raise ValidationError(f"This field must be of type bool, not {type(value)}.")
-def validate_tag_embed_fields(fields: dict) -> None:
+def validate_embed_fields(fields: dict) -> None:
"""Raises a ValidationError if any of the given embed fields is invalid."""
field_validators = {
'name': (MaxLengthValidator(limit_value=256),),
@@ -42,7 +38,7 @@ def validate_tag_embed_fields(fields: dict) -> None:
validator(value)
-def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
+def validate_embed_footer(footer: Dict[str, str]) -> None:
"""Raises a ValidationError if the given footer is invalid."""
field_validators = {
'text': (
@@ -67,7 +63,7 @@ def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
validator(value)
-def validate_tag_embed_author(author: Any) -> None:
+def validate_embed_author(author: Any) -> None:
"""Raises a ValidationError if the given author is invalid."""
field_validators = {
'name': (
@@ -93,7 +89,7 @@ def validate_tag_embed_author(author: Any) -> None:
validator(value)
-def validate_tag_embed(embed: Any) -> None:
+def validate_embed(embed: Any) -> None:
"""
Validate a JSON document containing an embed as possible to send on Discord.
@@ -109,11 +105,11 @@ def validate_tag_embed(embed: Any) -> None:
>>> from django.contrib.postgres import fields as pgfields
>>> from django.db import models
- >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed
+ >>> from pydis_site.apps.api.models.utils import validate_embed
>>> class MyMessage(models.Model):
... embed = pgfields.JSONField(
... validators=(
- ... validate_tag_embed,
+ ... validate_embed,
... )
... )
... # ...
@@ -149,10 +145,10 @@ def validate_tag_embed(embed: Any) -> None:
'description': (MaxLengthValidator(limit_value=2048),),
'fields': (
MaxLengthValidator(limit_value=25),
- validate_tag_embed_fields
+ validate_embed_fields
),
- 'footer': (validate_tag_embed_footer,),
- 'author': (validate_tag_embed_author,)
+ 'footer': (validate_embed_footer,),
+ 'author': (validate_embed_author,)
}
if not embed:
@@ -175,24 +171,3 @@ def validate_tag_embed(embed: Any) -> None:
if field_name in field_validators:
for validator in field_validators[field_name]:
validator(value)
-
-
-class Tag(ModelReprMixin, models.Model):
- """A tag providing (hopefully) useful information."""
-
- title = models.CharField(
- max_length=100,
- help_text=(
- "The title of this tag, shown in searches and providing "
- "a quick overview over what this embed contains."
- ),
- primary_key=True
- )
- embed = pgfields.JSONField(
- help_text="The actual embed shown by this tag.",
- validators=(validate_tag_embed,)
- )
-
- def __str__(self):
- """Returns the title of this tag, for display purposes."""
- return self.title