Merge pull request #197 from mouse-reeve/model-files-redo

Separates out models into more files
This commit is contained in:
Mouse Reeve 2020-09-17 13:28:03 -07:00 committed by GitHub
commit b42faad556
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 217 additions and 197 deletions

View file

@ -48,7 +48,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='connector', model_name='connector',
constraint=models.CheckConstraint(check=models.Q(connector_file__in=fedireads.models.book.ConnectorFiles), name='connector_file_valid'), constraint=models.CheckConstraint(check=models.Q(connector_file__in=fedireads.models.connector.ConnectorFiles), name='connector_file_valid'),
), ),
migrations.AddField( migrations.AddField(
model_name='book', model_name='book',

View file

@ -2,12 +2,15 @@
import inspect import inspect
import sys import sys
from .book import Connector, Book, Work, Edition, Author from .book import Book, Work, Edition, Author
from .connector import Connector
from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .shelf import Shelf, ShelfBook from .shelf import Shelf, ShelfBook
from .status import Status, Review, Comment, Quotation from .status import Status, Review, Comment, Quotation
from .status import Favorite, Boost, Tag, Notification, ReadThrough from .status import Favorite, Boost, Notification, ReadThrough
from .user import User, UserFollows, UserFollowRequest, UserBlocks from .tag import Tag
from .user import FederatedServer from .user import User
from .federated_server import FederatedServer
from .import_job import ImportJob, ImportItem from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite from .site import SiteSettings, SiteInvite

View file

@ -7,45 +7,10 @@ from model_utils.managers import InheritanceManager
from fedireads import activitypub from fedireads import activitypub
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.utils.fields import ArrayField from fedireads.utils.fields import ArrayField
from fedireads.connectors.settings import CONNECTORS
from .base_model import ActivityMapping, ActivitypubMixin, FedireadsModel from .base_model import ActivityMapping, ActivitypubMixin, FedireadsModel
ConnectorFiles = models.TextChoices('ConnectorFiles', CONNECTORS)
class Connector(FedireadsModel):
''' book data source connectors '''
identifier = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True)
local = models.BooleanField(default=False)
connector_file = models.CharField(
max_length=255,
choices=ConnectorFiles.choices
)
api_key = models.CharField(max_length=255, null=True)
base_url = models.CharField(max_length=255)
books_url = models.CharField(max_length=255)
covers_url = models.CharField(max_length=255)
search_url = models.CharField(max_length=255, null=True)
politeness_delay = models.IntegerField(null=True) #seconds
max_query_count = models.IntegerField(null=True)
# how many queries executed in a unit of time, like a day
query_count = models.IntegerField(default=0)
# when to reset the query count back to 0 (ie, after 1 day)
query_count_expiry = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(connector_file__in=ConnectorFiles),
name='connector_file_valid'
)
]
class Book(ActivitypubMixin, FedireadsModel): class Book(ActivitypubMixin, FedireadsModel):
''' a generic book, which can mean either an edition or a work ''' ''' a generic book, which can mean either an edition or a work '''
# these identifiers apply to both works and editions # these identifiers apply to both works and editions
@ -89,6 +54,7 @@ class Book(ActivitypubMixin, FedireadsModel):
@property @property
def ap_authors(self): def ap_authors(self):
''' the activitypub serialization should be a list of author ids '''
return [a.remote_id for a in self.authors.all()] return [a.remote_id for a in self.authors.all()]
activity_mappings = [ activity_mappings = [
@ -168,11 +134,13 @@ class Work(Book):
@property @property
def editions_path(self): def editions_path(self):
''' it'd be nice to serialize the edition instead but, recursion '''
return self.remote_id + '/editions' return self.remote_id + '/editions'
@property @property
def default_edition(self): def default_edition(self):
''' best-guess attempt at picking the default edition for this work '''
ed = Edition.objects.filter(parent_work=self, default=True).first() ed = Edition.objects.filter(parent_work=self, default=True).first()
if not ed: if not ed:
ed = Edition.objects.filter(parent_work=self).first() ed = Edition.objects.filter(parent_work=self).first()

View file

@ -0,0 +1,40 @@
''' manages interfaces with external sources of book data '''
from django.db import models
from fedireads.connectors.settings import CONNECTORS
from .base_model import FedireadsModel
ConnectorFiles = models.TextChoices('ConnectorFiles', CONNECTORS)
class Connector(FedireadsModel):
''' book data source connectors '''
identifier = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True)
local = models.BooleanField(default=False)
connector_file = models.CharField(
max_length=255,
choices=ConnectorFiles.choices
)
api_key = models.CharField(max_length=255, null=True)
base_url = models.CharField(max_length=255)
books_url = models.CharField(max_length=255)
covers_url = models.CharField(max_length=255)
search_url = models.CharField(max_length=255, null=True)
politeness_delay = models.IntegerField(null=True) #seconds
max_query_count = models.IntegerField(null=True)
# how many queries executed in a unit of time, like a day
query_count = models.IntegerField(default=0)
# when to reset the query count back to 0 (ie, after 1 day)
query_count_expiry = models.DateTimeField(auto_now_add=True)
class Meta:
''' check that there's code to actually use this connector '''
constraints = [
models.CheckConstraint(
check=models.Q(connector_file__in=ConnectorFiles),
name='connector_file_valid'
)
]

View file

@ -0,0 +1,15 @@
''' connections to external ActivityPub servers '''
from django.db import models
from .base_model import FedireadsModel
class FederatedServer(FedireadsModel):
''' store which server's we federate with '''
server_name = models.CharField(max_length=255, unique=True)
# federated, blocked, whatever else
status = models.CharField(max_length=255, default='federated')
# is it mastodon, fedireads, etc
application_type = models.CharField(max_length=255, null=True)
application_version = models.CharField(max_length=255, null=True)
# TODO: blocked servers

View file

@ -0,0 +1,89 @@
''' defines relationships between users '''
from django.db import models
from fedireads import activitypub
from .base_model import FedireadsModel
class UserRelationship(FedireadsModel):
''' many-to-many through table for followers '''
user_subject = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='%(class)s_user_subject'
)
user_object = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='%(class)s_user_object'
)
# follow or follow_request for pending TODO: blocking?
relationship_id = models.CharField(max_length=100)
class Meta:
''' relationships should be unique '''
abstract = True
constraints = [
models.UniqueConstraint(
fields=['user_subject', 'user_object'],
name='%(class)s_unique'
),
models.CheckConstraint(
check=~models.Q(user_subject=models.F('user_object')),
name='%(class)s_no_self'
)
]
def get_remote_id(self):
''' use shelf identifier in remote_id '''
base_path = self.user_subject.remote_id
return '%s#%s/%d' % (base_path, self.status, self.id)
class UserFollows(UserRelationship):
''' Following a user '''
status = 'follows'
@classmethod
def from_request(cls, follow_request):
''' converts a follow request into a follow relationship '''
return cls(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
relationship_id=follow_request.relationship_id,
)
class UserFollowRequest(UserRelationship):
''' following a user requires manual or automatic confirmation '''
status = 'follow_request'
def to_activity(self):
''' request activity '''
return activitypub.Follow(
id=self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
def to_accept_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Accept(
id='%s#accepts/follows/' % self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
def to_reject_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Reject(
id='%s#rejects/follows/' % self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
class UserBlocks(UserRelationship):
''' prevent another user from following you and seeing your posts '''
# TODO: not implemented
status = 'blocks'

View file

@ -1,6 +1,4 @@
''' models for storing different kinds of Activities ''' ''' models for storing different kinds of Activities '''
import urllib.parse
from django.utils import timezone from django.utils import timezone
from django.utils.http import http_date from django.utils.http import http_date
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -8,9 +6,7 @@ from django.db import models
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from fedireads import activitypub from fedireads import activitypub
from fedireads.settings import DOMAIN from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, OrderedCollectionMixin, \
OrderedCollectionPageMixin
from .base_model import ActivityMapping, FedireadsModel from .base_model import ActivityMapping, FedireadsModel
@ -205,59 +201,6 @@ class Boost(Status):
# unique_together = ('user', 'boosted_status') # unique_together = ('user', 'boosted_status')
class Tag(OrderedCollectionMixin, FedireadsModel):
''' freeform tags for books '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=100)
@classmethod
def book_queryset(cls, identifier):
''' county of books associated with this tag '''
return cls.objects.filter(identifier=identifier)
@property
def collection_queryset(self):
''' books associated with this tag '''
return self.book_queryset(self.identifier)
def get_remote_id(self):
''' tag should use identifier not id in remote_id '''
base_path = 'https://%s' % DOMAIN
return '%s/tag/%s' % (base_path, self.identifier)
def to_add_activity(self, user):
''' AP for shelving a book'''
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.to_activity(),
).serialize()
def to_remove_activity(self, user):
''' AP for un-shelving a book'''
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.to_activity(),
).serialize()
def save(self, *args, **kwargs):
''' create a url-safe lookup key for the tag '''
if not self.id:
# add identifiers to new tags
self.identifier = urllib.parse.quote_plus(self.name)
super().save(*args, **kwargs)
class Meta:
''' unqiueness constraint '''
unique_together = ('user', 'book', 'name')
class ReadThrough(FedireadsModel): class ReadThrough(FedireadsModel):
''' Store progress through a book in the database. ''' ''' Store progress through a book in the database. '''
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)

60
fedireads/models/tag.py Normal file
View file

@ -0,0 +1,60 @@
''' models for storing different kinds of Activities '''
import urllib.parse
from django.db import models
from fedireads import activitypub
from fedireads.settings import DOMAIN
from .base_model import OrderedCollectionMixin, FedireadsModel
class Tag(OrderedCollectionMixin, FedireadsModel):
''' freeform tags for books '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=100)
@classmethod
def book_queryset(cls, identifier):
''' county of books associated with this tag '''
return cls.objects.filter(identifier=identifier)
@property
def collection_queryset(self):
''' books associated with this tag '''
return self.book_queryset(self.identifier)
def get_remote_id(self):
''' tag should use identifier not id in remote_id '''
base_path = 'https://%s' % DOMAIN
return '%s/tag/%s' % (base_path, self.identifier)
def to_add_activity(self, user):
''' AP for shelving a book'''
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.to_activity(),
).serialize()
def to_remove_activity(self, user):
''' AP for un-shelving a book'''
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=user.remote_id,
object=self.book.to_activity(),
target=self.to_activity(),
).serialize()
def save(self, *args, **kwargs):
''' create a url-safe lookup key for the tag '''
if not self.id:
# add identifiers to new tags
self.identifier = urllib.parse.quote_plus(self.name)
super().save(*args, **kwargs)
class Meta:
''' unqiueness constraint '''
unique_together = ('user', 'book', 'name')

View file

@ -11,7 +11,7 @@ from fedireads.models.status import Status
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.signatures import create_key_pair from fedireads.signatures import create_key_pair
from .base_model import OrderedCollectionPageMixin from .base_model import OrderedCollectionPageMixin
from .base_model import ActivityMapping, FedireadsModel from .base_model import ActivityMapping
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
@ -168,104 +168,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
return activity_object return activity_object
class UserRelationship(FedireadsModel):
''' many-to-many through table for followers '''
user_subject = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='%(class)s_user_subject'
)
user_object = models.ForeignKey(
'User',
on_delete=models.PROTECT,
related_name='%(class)s_user_object'
)
# follow or follow_request for pending TODO: blocking?
relationship_id = models.CharField(max_length=100)
class Meta:
''' relationships should be unique '''
abstract = True
constraints = [
models.UniqueConstraint(
fields=['user_subject', 'user_object'],
name='%(class)s_unique'
),
models.CheckConstraint(
check=~models.Q(user_subject=models.F('user_object')),
name='%(class)s_no_self'
)
]
def get_remote_id(self):
''' use shelf identifier in remote_id '''
base_path = self.user_subject.remote_id
return '%s#%s/%d' % (base_path, self.status, self.id)
class UserFollows(UserRelationship):
''' Following a user '''
@property
def status(self):
return 'follows'
@classmethod
def from_request(cls, follow_request):
''' converts a follow request into a follow relationship '''
return cls(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
relationship_id=follow_request.relationship_id,
)
class UserFollowRequest(UserRelationship):
''' following a user requires manual or automatic confirmation '''
@property
def status(self):
return 'follow_request'
def to_activity(self):
''' request activity '''
return activitypub.Follow(
id=self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
def to_accept_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Accept(
id='%s#accepts/follows/' % self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
def to_reject_activity(self):
''' generate an Accept for this follow request '''
return activitypub.Reject(
id='%s#rejects/follows/' % self.remote_id,
actor=self.user_subject.remote_id,
object=self.user_object.remote_id,
).serialize()
class UserBlocks(UserRelationship):
@property
def status(self):
return 'blocks'
class FederatedServer(FedireadsModel):
''' store which server's we federate with '''
server_name = models.CharField(max_length=255, unique=True)
# federated, blocked, whatever else
status = models.CharField(max_length=255, default='federated')
# is it mastodon, fedireads, etc
application_type = models.CharField(max_length=255, null=True)
application_version = models.CharField(max_length=255, null=True)
@receiver(models.signals.pre_save, sender=User) @receiver(models.signals.pre_save, sender=User)
def execute_before_save(sender, instance, *args, **kwargs): def execute_before_save(sender, instance, *args, **kwargs):
''' populate fields for new local users ''' ''' populate fields for new local users '''