Rewriting models
This commit is contained in:
parent
224c6d620b
commit
dfeb368be5
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.home.models import RSSFeed, FeedChannel
|
from apps.home.models import Subscription, TrackedContent
|
||||||
|
# from apps.home.models import RSSFeed, FeedChannel
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -103,21 +105,25 @@ class DynamicModelSerializer(serializers.ModelSerializer):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class RssFeedSerializer(DynamicModelSerializer):
|
class SubscriptionSerializer(DynamicModelSerializer):
|
||||||
|
"""Serializer for the Subscription Model."""
|
||||||
|
|
||||||
image = serializers.ImageField()
|
image = serializers.ImageField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RSSFeed
|
model = Subscription
|
||||||
fields = (
|
fields = (
|
||||||
"uuid", "name", "url", "image", "discord_server_id", "created_at"
|
"uuid", "name", "url", "image", "server",
|
||||||
|
"channels", "creation_datetime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FeedChannelSerializer(DynamicModelSerializer):
|
class TrackedContent(DynamicModelSerializer):
|
||||||
feeds = RssFeedSerializer(many=True)
|
"""Serializer for the TrackedContent Model."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FeedChannel
|
model = TrackedContent
|
||||||
fields = (
|
fields = (
|
||||||
"uuid", "discord_server_id", "discord_channel_id", "feeds", "created_at"
|
"uuid", "content_url", "subscription",
|
||||||
|
"creation_datetime"
|
||||||
)
|
)
|
||||||
|
@ -10,9 +10,20 @@ urlpatterns = [
|
|||||||
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
|
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
|
||||||
path("api-token-auth/", obtain_auth_token),
|
path("api-token-auth/", obtain_auth_token),
|
||||||
|
|
||||||
path("rssfeed/", include([
|
path("subscription/", include([
|
||||||
path("", views.RSSFeedList.as_view(), name="rssfeed"),
|
path("", views.SubscriptionList.as_view(), name="subscription"),
|
||||||
path("<str:pk>/", views.RSSFeedDetail.as_view(), name="rssfeed-detail")
|
path("<str:pk>/", views.SubscriptionDetail.as_view(), name="subscription-detail")
|
||||||
])),
|
])),
|
||||||
path("feedchannel/", views.FeedChannelListApiView.as_view(), name="feedchannel")
|
|
||||||
|
path("tracked/", include([
|
||||||
|
path("", views.TrackedContentList.as_view(), name="tracked"),
|
||||||
|
path("<str:pk>/", views.TrackedContentDetail.as_view(), name="tracked-detail"),
|
||||||
|
# path("")
|
||||||
|
])),
|
||||||
|
|
||||||
|
# path("rssfeed/", include([
|
||||||
|
# path("", views.RSSFeedList.as_view(), name="rssfeed"),
|
||||||
|
# path("<str:pk>/", views.RSSFeedDetail.as_view(), name="rssfeed-detail")
|
||||||
|
# ])),
|
||||||
|
# path("feedchannel/", views.FeedChannelListApiView.as_view(), name="feedchannel")
|
||||||
]
|
]
|
@ -1,4 +1 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
"""
|
|
||||||
Copyright (c) 2019 - present AppSeed.us
|
|
||||||
"""
|
|
||||||
|
@ -3,17 +3,23 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import RSSFeed, FeedChannel
|
from .models import RSSFeed, FeedChannel
|
||||||
|
from .models import Subscription, TrackedContent
|
||||||
|
|
||||||
|
|
||||||
class RSSFeedAdmin(admin.ModelAdmin):
|
class SubscriptionAdmin(admin.ModelAdmin):
|
||||||
list_display = ["uuid", "name", "url", "discord_server_id", "created_at"]
|
list_display = [
|
||||||
|
"uuid", "name", "url", "server",
|
||||||
|
"channels", "creation_datetime"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(RSSFeed, RSSFeedAdmin)
|
class TrackedContentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"uuid", "content_url", "subscription",
|
||||||
|
"creation_datetime"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FeedChannelAdmin(admin.ModelAdmin):
|
admin.site.register(Subscription, SubscriptionAdmin)
|
||||||
list_display = ["uuid", "discord_server_id", "discord_channel_id", "created_at"]
|
admin.site.register(TrackedContent, TrackedContentAdmin)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(FeedChannel, FeedChannelAdmin)
|
|
||||||
|
@ -8,6 +8,8 @@ import httpx
|
|||||||
import feedparser
|
import feedparser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
@ -29,126 +31,112 @@ class OverwriteStorage(FileSystemStorage):
|
|||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
class RSSFeedIconPathGenerator:
|
class Subscription_IconPathGenerator:
|
||||||
"""Icon path generator for RSS Feed."""
|
"""Icon path generator for Subscriptions."""
|
||||||
|
|
||||||
def __call__(self, instance, filename: str) -> str:
|
def __call__(self, instance, filename: str) -> str:
|
||||||
return os.path.join("rssfeed", str(instance.uuid), "icon.webp")
|
return os.path.join("subscriptions", str(instance.uuid), "icon.webp")
|
||||||
|
|
||||||
|
|
||||||
@async_to_sync
|
class Subscription(models.Model):
|
||||||
async def validate_rss_url(url: str):
|
"""Stores relevant data for a user submitted RSS Feed."""
|
||||||
|
|
||||||
log.debug("validating RSS url: %s" % url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
validator = URLValidator(schemes=["https"])
|
|
||||||
validator(url)
|
|
||||||
except ValidationError as exc:
|
|
||||||
raise ValidationError("URL scheme must be 'https'") from exc
|
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(url)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except httpx.HTTPStatusError as error:
|
|
||||||
log.error(error)
|
|
||||||
raise ValidationError(error)
|
|
||||||
|
|
||||||
content = response.text
|
|
||||||
feed = feedparser.parse(content)
|
|
||||||
|
|
||||||
if not feed.version:
|
|
||||||
log.error("not a feed")
|
|
||||||
raise ValidationError(f"{url} does not point to a valid RSS feed")
|
|
||||||
|
|
||||||
log.debug("success")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class RSSFeed(models.Model):
|
|
||||||
"""Represents an RSS Feed."""
|
|
||||||
|
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
|
||||||
|
|
||||||
|
uuid = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
|
default=uuid4,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_("name"),
|
verbose_name=_("name"),
|
||||||
help_text=_("a human readable nickname for this item"),
|
help_text=_("Reference name for this subscription (max %(max_length)s chars)."),
|
||||||
null=False, blank=False,
|
max_length=32,
|
||||||
max_length=120
|
null=False,
|
||||||
|
blank=False
|
||||||
)
|
)
|
||||||
|
rss_url = models.URLField(
|
||||||
url = models.URLField(
|
verbose_name=_("rss url"),
|
||||||
verbose_name=_("url"),
|
help_text=_("URL of the subscribed to RSS feed.")
|
||||||
help_text=_("url to the RSS feed"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
verbose_name=_("image"),
|
verbose_name=_("image"),
|
||||||
help_text=_("image of the RSS feed"),
|
help_text=_("image of the RSS feed."),
|
||||||
upload_to=RSSFeedIconPathGenerator(),
|
upload_to=Subscription_IconPathGenerator(),
|
||||||
storage=OverwriteStorage(),
|
storage=OverwriteStorage(),
|
||||||
|
default="../static/images/defaultuser.webp",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
creation_datetime = models.DateTimeField(
|
||||||
created_at = models.DateTimeField(
|
verbose_name=_("creation datetime"),
|
||||||
verbose_name=_("creation date & time"),
|
help_text=_("when this instance was created."),
|
||||||
help_text=_("when this item was created"),
|
|
||||||
default=timezone.now,
|
default=timezone.now,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
|
server = models.PositiveBigIntegerField(
|
||||||
discord_server_id = models.PositiveBigIntegerField(
|
verbose_name=_("server id"),
|
||||||
verbose_name=_("discord server id"),
|
help_text=_("Identifier for the discord server that owns this subscription.")
|
||||||
help_text=_("the discord server id of this item")
|
|
||||||
)
|
)
|
||||||
|
channels = models.ManyToManyField(int)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("RSS Feed")
|
verbose_name = "subscription"
|
||||||
verbose_name_plural = _("RSS Feeds")
|
verbose_name_plural = "subscriptions"
|
||||||
|
get_latest_by = "-creation_datetime"
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=["name", "discord_server_id"], name="unique name & server pair")
|
# Prevent servers from having subscriptions with duplicate names
|
||||||
|
models.UniqueConstraint(fields=["name", "server_id"], name="unique name & server pair")
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class FeedChannel(models.Model):
|
class TrackedContent(models.Model):
|
||||||
"""Represents a Discord Text Channel to act as a feed."""
|
"""Tracks content shared from an RSS Feed, to prevent duplicates."""
|
||||||
|
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
uuid = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
discord_server_id = models.PositiveBigIntegerField(
|
default=uuid4,
|
||||||
verbose_name=_("discord server id"),
|
editable=False
|
||||||
help_text=_("the discord server id of this item")
|
|
||||||
)
|
)
|
||||||
|
subscription = models.ForeignKey(
|
||||||
discord_channel_id = models.PositiveBigIntegerField(
|
verbose_name=_("subscription"),
|
||||||
verbose_name=_("discord channel id"),
|
help_text=_("The subscription that this content originated from."),
|
||||||
help_text=_("the discord channel id of this item"),
|
to=Subscription,
|
||||||
null=False, blank=False
|
on_delete=models.CASCADE,
|
||||||
|
related_name="tracked_content"
|
||||||
)
|
)
|
||||||
|
content_url = models.URLField(
|
||||||
feeds = models.ManyToManyField(
|
verbose_name=_("content url"),
|
||||||
to=RSSFeed,
|
help_text=_("URL of the tracked content.")
|
||||||
verbose_name=_("feeds"),
|
|
||||||
help_text=_("the feeds to include in this item"),
|
|
||||||
related_name=_("queues")
|
|
||||||
)
|
)
|
||||||
|
creation_datetime = models.DatetimeField(
|
||||||
created_at = models.DateTimeField(
|
verbose_name=_("creation datetime"),
|
||||||
verbose_name=_("creation date & time"),
|
help_text=_("when this instance was created."),
|
||||||
help_text=_("when this item was created"),
|
|
||||||
default=timezone.now,
|
default=timezone.now,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Feed Channel")
|
|
||||||
verbose_name_plural = _("Feed Channels")
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.discord_channel_id)
|
return str(self.content_url)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=TrackedContent)
|
||||||
|
def maintain_item_cap(sender, instance, **kwargs):
|
||||||
|
""""Delete the latest tracked content, if the total under
|
||||||
|
the same subscription reaches 100."""
|
||||||
|
|
||||||
|
log.debug("checking if tracked content can be deleted.")
|
||||||
|
|
||||||
|
queryset = sender.objects.filter(subscription=instance.subscription)
|
||||||
|
total_tracked = queryset.count()
|
||||||
|
|
||||||
|
if total_tracked < 100:
|
||||||
|
log.debug("tracked content cannot be deleted, less than 100 items: %s", total_tracked)
|
||||||
|
return
|
||||||
|
|
||||||
|
oldest = queryset.earliest()
|
||||||
|
|
||||||
|
log.info("tracked content limit exceeded, deleting oldest item with uuid: %s", oldest.uuid)
|
||||||
|
|
||||||
|
oldest.delete()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user