Skip to content

Commit

Permalink
Merge pull request #29 from xurble/Add-Subscriptions
Browse files Browse the repository at this point in the history
Add subscriptions
  • Loading branch information
xurble authored Feb 16, 2024
2 parents cd95f2d + 0eb74c0 commit 7c3dca3
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 90 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

### 1.1.0
- Add ability to manage read status, put feeds in folders, have multiple users

### 1.0.9
- Decreases the size of GUID slightly, was not compatible with MySQL

Expand Down
5 changes: 1 addition & 4 deletions feeds/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@




from django.conf import settings

__all__ = []
Expand All @@ -13,7 +11,7 @@
break

_DEFAULTS = {
"FEEDS_USER_AGENT": "django-feed-reader",
"FEEDS_USER_AGENT": "django-feed-reader",
"FEEDS_SERVER": server,
}

Expand All @@ -24,4 +22,3 @@
setattr(settings, key, value)
except ImportError:
pass

5 changes: 5 additions & 0 deletions feeds/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Register your models here.
from feeds import models


class SourceAdmin(admin.ModelAdmin):

readonly_fields = (
Expand All @@ -22,6 +23,7 @@ def posts_link(self, obj=None):
)
posts_link.short_description = 'posts'


class PostAdmin(admin.ModelAdmin):

raw_id_fields = ('source',)
Expand All @@ -45,13 +47,16 @@ def enclosures_link(self, obj=None):
)
enclosures_link.short_description = 'enclosures'


class EnclosureAdmin(admin.ModelAdmin):

raw_id_fields = ('post',)

list_display = ('href', 'type')


admin.site.register(models.Source, SourceAdmin)
admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Enclosure, EnclosureAdmin)
admin.site.register(models.WebProxy)
admin.site.register(models.Subscription)
33 changes: 33 additions & 0 deletions feeds/migrations/0012_source_last_read_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.3 on 2024-02-12 07:45

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('feeds', '0011_alter_post_guid'),
]

operations = [
migrations.AddField(
model_name='source',
name='last_read',
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_read', models.IntegerField(default=0)),
('is_river', models.BooleanField(default=False)),
('name', models.CharField(max_length=255)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='feeds.subscription')),
('source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='feeds.source')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
194 changes: 124 additions & 70 deletions feeds/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from django.db import models

import time
import datetime
from urllib.parse import urlencode
import logging
import sys
import email


from django.conf import settings
from django.db import models
import django.utils as django_utils
from django.utils.deconstruct import deconstructible

Expand All @@ -21,43 +19,58 @@ def __call__(self):
return django_utils.timezone.now() - datetime.timedelta(days=1)



class Source(models.Model):
# This is an actual feed that we poll
name = models.CharField(max_length=255, blank=True, null=True)
site_url = models.CharField(max_length=255, blank=True, null=True)
feed_url = models.CharField(max_length=512)
image_url = models.CharField(max_length=512, blank=True, null=True)

description = models.TextField(null=True, blank=True)

last_polled = models.DateTimeField(blank=True, null=True)
due_poll = models.DateTimeField(default=datetime.datetime(1900, 1, 1)) # default to distant past to put new sources to front of queue
etag = models.CharField(max_length=255, blank=True, null=True)
last_modified = models.CharField(max_length=255, blank=True, null=True) # just pass this back and forward between server and me , no need to parse

last_result = models.CharField(max_length=255,blank=True,null=True)
interval = models.PositiveIntegerField(default=400)
last_success = models.DateTimeField(blank=True, null=True)
last_change = models.DateTimeField(blank=True, null=True)
live = models.BooleanField(default=True)
status_code = models.PositiveIntegerField(default=0)
last_302_url = models.CharField(max_length=512, null=True, blank=True)
name = models.CharField(max_length=255, blank=True, null=True)
site_url = models.CharField(max_length=255, blank=True, null=True)
feed_url = models.CharField(max_length=512)
image_url = models.CharField(max_length=512, blank=True, null=True)

description = models.TextField(null=True, blank=True)

last_polled = models.DateTimeField(blank=True, null=True)
due_poll = models.DateTimeField(default=datetime.datetime(1900, 1, 1)) # default to distant past to put new sources to front of queue
etag = models.CharField(max_length=255, blank=True, null=True)
last_modified = models.CharField(max_length=255, blank=True, null=True) # just pass this back and forward between server and me , no need to parse

last_result = models.CharField(max_length=255, blank=True, null=True)
interval = models.PositiveIntegerField(default=400)
last_success = models.DateTimeField(blank=True, null=True)
last_change = models.DateTimeField(blank=True, null=True)
live = models.BooleanField(default=True)
status_code = models.PositiveIntegerField(default=0)
last_302_url = models.CharField(max_length=512, null=True, blank=True)
last_302_start = models.DateTimeField(null=True, blank=True)

max_index = models.IntegerField(default=0)

num_subs = models.IntegerField(default=1)

is_cloudflare = models.BooleanField(default=False)
max_index = models.IntegerField(default=0)
last_read = models.IntegerField(default=0)
num_subs = models.IntegerField(default=1)

is_cloudflare = models.BooleanField(default=False)

def __str__(self):
return self.display_name

def mark_read(self):
"""
In a single user system, marm this feed as read
"""
self.last_read = self.max_index
self.save()

@property
def unread_count(self):
"""
In a single user system how many unread articles are there?
If you need more than one user, or want to arrange feeds
into folders, use a Subscription (below)
"""
return self.max_index - self.last_read

@property
def best_link(self):
#the html link else hte feed link
# the html link else the feed link
if self.site_url is None or self.site_url == '':
return self.feed_url
else:
Expand All @@ -80,12 +93,13 @@ def garden_style(self):
else:
dd = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) - self.last_change

days = int (dd.days / 2)
days = int(dd.days / 2)

col = 255 - days
if col < 0: col = 0
if col < 0:
col = 0

css = "background-color:#ff%02x%02x" % (col,col)
css = "background-color:#ff%02x%02x" % (col, col)

if col < 128:
css += ";color:white"
Expand All @@ -96,23 +110,23 @@ def garden_style(self):
def health_box(self):

if not self.live:
css="#ccc;"
elif self.last_change == None or self.last_success == None:
css="#F00;"
css = "#ccc;"
elif self.last_change is None or self.last_success is None:
css = "#F00;"
else:
dd = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) - self.last_change

days = int (dd.days/2)
days = int(dd.days/2)

red = days
if red > 255:
red = 255

green = 255-days;
green = 255 - days
if green < 0:
green = 0

css = "#%02x%02x00" % (red,green)
css = "#%02x%02x00" % (red, green)

return css

Expand All @@ -121,24 +135,24 @@ class Post(models.Model):
GUID_MAX_LENGTH = 768
# an entry in a feed

source = models.ForeignKey(Source, on_delete=models.CASCADE, related_name='posts')
title = models.TextField(blank=True)
body = models.TextField()
link = models.CharField(max_length=512, blank=True, null=True)
found = models.DateTimeField(auto_now_add=True)
created = models.DateTimeField(db_index=True)
guid = models.CharField(max_length=GUID_MAX_LENGTH, blank=True, null=True, db_index=True)
author = models.CharField(max_length=255, blank=True, null=True)
index = models.IntegerField(db_index=True)
image_url = models.CharField(max_length=512, blank=True,null=True)

source = models.ForeignKey(Source, on_delete=models.CASCADE, related_name='posts')
title = models.TextField(blank=True)
body = models.TextField()
link = models.CharField(max_length=512, blank=True, null=True)
found = models.DateTimeField(auto_now_add=True)
created = models.DateTimeField(db_index=True)
guid = models.CharField(max_length=GUID_MAX_LENGTH, blank=True, null=True, db_index=True)
author = models.CharField(max_length=255, blank=True, null=True)
index = models.IntegerField(db_index=True)
image_url = models.CharField(max_length=512, blank=True, null=True)

@property
def title_url_encoded(self):
try:
ret = urlencode({"X":self.title})
if len(ret) > 2: ret = ret[2:]
except:
ret = urlencode({"X": self.title})
if len(ret) > 2:
ret = ret[2:]
except Exception:
logging.info("Failed to url encode title of post {}".format(self.id))
ret = ""

Expand All @@ -149,38 +163,80 @@ def __str__(self):
def recast_link(self):

# TODO: This needs to come out, it's just for recast

#if "?" in self.link:
# return self.link + ("&recast_id=%d" % self.id)
#else:
# return self.link + ("?recast_id=%d" % self.id)current_subscription

return "/post/%d/" % self.id

class Meta:
ordering = ["index"]


class Enclosure(models.Model):

post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='enclosures')
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='enclosures')
length = models.IntegerField(default=0)
href = models.CharField(max_length=512)
type = models.CharField(max_length=256)
href = models.CharField(max_length=512)
type = models.CharField(max_length=256)
medium = models.CharField(max_length=25, null=True, blank=True)
description = models.CharField(max_length=512, null= True, blank=True)
description = models.CharField(max_length=512, null=True, blank=True)

@property
def recast_link(self):

# TODO: This needs to come out, it's just for recast

#if "?" in self.href:
# return self.href + ("&recast_id=%d" % self.id)
#else:
# return self.href + ("?recast_id=%d" % self.id)

return "/enclosure/%d/" % self.id

@property
def is_image(self):
if self.medium == "image":
return True
return "image/" in self.type and not self.medium

@property
def is_audio(self):
if self.medium == "audio":
return True
return "audio/" in self.type and not self.medium

@property
def is_video(self):
if self.medium == "video":
return True
return "video/" in self.type and not self.medium


# A user subscription
class Subscription(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
source = models.ForeignKey(Source, blank=True, null=True, on_delete=models.CASCADE, related_name='subscriptions') # null source means we are a folder
parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE, related_name='subscriptions')
last_read = models.IntegerField(default=0)
is_river = models.BooleanField(default=False)
name = models.CharField(max_length=255)

def __str__(self):
return "'%s' for user %s" % (self.name, str(self.user))

def mark_read(self):
if self.source:
self.last_read = self.source.max_index
self.save()
else:
# I am a folder
for child in Subscription.objects.filter(parent=self):
child.mark_read()

@property
def unread_count(self):
if self.source:
return self.source.max_index - self.last_read
else:
if not hasattr(self, "_unread_count"):
self._unread_count = 0
for child in Subscription.objects.filter(parent=self):
self._unread_count += child.unread_count

return self._unread_count


class WebProxy(models.Model):
# this class if for Cloudflare avoidance and contains a list of potential
Expand All @@ -189,5 +245,3 @@ class WebProxy(models.Model):

def __str__(self):
return "Proxy:{}".format(self.address)


Loading

0 comments on commit 7c3dca3

Please sign in to comment.