515 lines
17 KiB
Python
515 lines
17 KiB
Python
# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site
|
|
# Copyright (C) 2017 Evert "Diamond" Prants <evert@lunasqu.ee>, Taizo "Tsa6" Simpson <taizo@tsa6.net>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
from django.db import models
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.core.files.storage import FileSystemStorage
|
|
from django.conf import settings
|
|
import os
|
|
|
|
def name_artwork(inst, name):
|
|
return '%s/artwork.%s'%(inst.abbr,name.split('.')[-1])
|
|
def name_css(inst, name):
|
|
return '%s/style.css'%inst.abbr
|
|
def name_banner(inst, name):
|
|
return '%s/banner.%s'%(inst.abbr,name.split('.')[-1])
|
|
def name_season_artwork(inst, name):
|
|
return '%s/%d/artwork.%s'%(inst.show.abbr,inst.number,name.split('.')[-1])
|
|
show_static_storage = FileSystemStorage(location=os.path.join(os.path.dirname(settings.MEDIA_ROOT), 'uploaded_resources'), base_url='showstatic')
|
|
|
|
# Create your models here.
|
|
class TimestampedModel(models.Model):
|
|
timestamp = models.DateTimeField(
|
|
auto_now=True,
|
|
help_text='The date and time this object was created'
|
|
)
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class Show(TimestampedModel):
|
|
name = models.CharField(
|
|
max_length=40,
|
|
help_text="The full name of the show",
|
|
verbose_name="Full Name"
|
|
)
|
|
abbr = models.SlugField(
|
|
max_length=16,
|
|
unique=True,
|
|
help_text="A short abbreviation of the show, for use in urls",
|
|
verbose_name="Abbreviation"
|
|
)
|
|
description = models.TextField(
|
|
help_text="A description of the show",
|
|
verbose_name="Description"
|
|
)
|
|
release = models.DateField(
|
|
help_text="The release date of the first episode of the show",
|
|
verbose_name="Release Date"
|
|
)
|
|
artwork = models.ImageField(
|
|
storage=show_static_storage,
|
|
upload_to = name_artwork,
|
|
help_text="The artwork associated with the show. Should display the name of the show in a movie-poster esque format. Aspect ration should be about 2:3",
|
|
verbose_name="Artwork"
|
|
)
|
|
imdb = models.URLField(
|
|
help_text="The url of the IMDb page for this show",
|
|
verbose_name="IMDb Page"
|
|
)
|
|
moderated = models.BooleanField(
|
|
help_text="Whether or not this show is user-moderated",
|
|
verbose_name="User Moderated"
|
|
)
|
|
css = models.FileField(
|
|
storage=show_static_storage,
|
|
upload_to=name_css,
|
|
null=True,
|
|
blank=True,
|
|
help_text="The CSS stylesheet applied to this show's page",
|
|
verbose_name="Custom Style"
|
|
)
|
|
banner = models.ImageField(
|
|
storage=show_static_storage,
|
|
upload_to = name_banner,
|
|
help_text="A banner used for the show's page.",
|
|
verbose_name="Banner"
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
('can_create_show_ban', 'Can ban an user from submitting to this show'),
|
|
('can_moderate_show', 'Can add episodes, seasons and unrestricted submissions'),
|
|
('can_moderate_board', 'Can delete and edit boards and replies of this show'),
|
|
)
|
|
|
|
def url(self):
|
|
use_sdms = getattr(settings, "DOMAIN_SUBDOMAIN_SHOWS", False)
|
|
domain = getattr(settings, "DOMAIN", 'localhost')
|
|
|
|
if use_sdms:
|
|
return domain.format(sub=self.abbr + '.',path='')
|
|
|
|
return '/show/%s' % (self.abbr)
|
|
|
|
def __str__(self):
|
|
return '%s [%s]'%(self.name,self.abbr)
|
|
|
|
class User(AbstractUser):
|
|
icy_id = models.CharField(
|
|
max_length=36,
|
|
help_text='The UUID assigned to this user by IcyNet\'s auth servers'
|
|
)
|
|
display_name=models.CharField(
|
|
max_length=20,
|
|
help_text="The name shown to other users",
|
|
verbose_name="Display Name"
|
|
)
|
|
favorites=models.ManyToManyField(
|
|
'Episode',
|
|
related_name='favorited_by',
|
|
through='Favorite'
|
|
)
|
|
watches=models.ManyToManyField(
|
|
'Episode',
|
|
related_name='watched_by',
|
|
through='Watch'
|
|
)
|
|
|
|
class Ban(TimestampedModel):
|
|
user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
help_text="The user this ban applies to",
|
|
verbose_name="Banned User"
|
|
)
|
|
admin = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
help_text='The admin which banned this user',
|
|
verbose_name='Banned By',
|
|
related_name='bans'
|
|
)
|
|
reason = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
help_text='The reason this user was banned',
|
|
verbose_name='Ban Reason'
|
|
)
|
|
expiration = models.DateField(
|
|
help_text='The date this user will become unbanned',
|
|
blank=True,
|
|
verbose_name='Expiration Date'
|
|
)
|
|
permanent = models.BooleanField(
|
|
help_text='If checked, this user will never be unbanned, even if the expiration date passes',
|
|
verbose_name='Permanent'
|
|
)
|
|
scope = models.ManyToManyField(
|
|
Show,
|
|
blank=True,
|
|
help_text='The shows this user is banned from interacting with',
|
|
verbose_name='Banned From',
|
|
related_name='bans'
|
|
)
|
|
site_wide = models.BooleanField(
|
|
help_text='If checked, this is a site-wide ban, and the user is automatically banned from all shows, not just those in the Banned From (scope) paramenter',
|
|
verbose_name = 'Site Wide Ban'
|
|
)
|
|
def __str__(self):
|
|
return ("Permanent" if self.permanent else "Temporary") + " ban of %s"%self.user
|
|
|
|
class Report(TimestampedModel):
|
|
reporter = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='reports',
|
|
help_text='The user who created this report',
|
|
verbose_name='Reporter'
|
|
)
|
|
title = models.CharField(
|
|
max_length=50,
|
|
help_text='A brief summary of the report',
|
|
verbose_name='Title'
|
|
)
|
|
details = models.TextField(
|
|
help_text='The details of the report, preferably including why the content should be removed',
|
|
verbose_name = 'Details'
|
|
)
|
|
url = models.URLField(
|
|
max_length=100,
|
|
help_text='The URL of the content being reported',
|
|
verbose_name = 'Content URL'
|
|
)
|
|
show = models.ForeignKey(
|
|
Show,
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
help_text='The show associated with the reported url',
|
|
)
|
|
read_by = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='reader',
|
|
help_text='The administrator that read this report',
|
|
verbose_name='Read by'
|
|
)
|
|
read_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text='The date and time this report was read'
|
|
)
|
|
resolved = models.BooleanField(
|
|
help_text='Whether or not this report has been marked as resolved',
|
|
default=False
|
|
)
|
|
def __str__(self):
|
|
return "%s's report of %s"%(self.reporter, self.url)
|
|
|
|
class ShowSubmission(TimestampedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='show_submissions',
|
|
help_text='The user who submitted this show',
|
|
verbose_name='Submitter'
|
|
)
|
|
name = models.CharField(
|
|
max_length=40,
|
|
help_text="The full name of the show",
|
|
verbose_name="Full Name"
|
|
)
|
|
details = models.TextField(
|
|
help_text='Some details about the show. Why it should be added, where information about it can be found, etc.',
|
|
verbose_name='Details'
|
|
)
|
|
def __str__(self):
|
|
return '"%s" by %s'%(self.name, self.user)
|
|
|
|
class Season(models.Model):
|
|
show = models.ForeignKey(
|
|
'Show',
|
|
on_delete=models.CASCADE,
|
|
related_name='seasons',
|
|
help_text='The show this season belongs to'
|
|
)
|
|
name = models.CharField(
|
|
max_length=40,
|
|
blank=True,
|
|
help_text='The name given to this season by its producers. Can be blank if no name was given',
|
|
verbose_name='Season Name'
|
|
)
|
|
number = models.IntegerField(
|
|
help_text='The number of this season, starting at 1; For example, the first season to be aired would be number 1, and the second would be number 2'
|
|
)
|
|
description = models.TextField(
|
|
help_text='A description of this season\'s happenings',
|
|
blank=True
|
|
)
|
|
artwork = models.ImageField(
|
|
storage=show_static_storage,
|
|
upload_to = name_season_artwork,
|
|
help_text="The artwork associated with the season. Should display the name of the show in a movie-poster esque format. Aspect ration should be about 2:3",
|
|
verbose_name="Artwork",
|
|
blank=True
|
|
)
|
|
def __str__(self):
|
|
return self.show.name + " S%d"%self.number
|
|
|
|
class Episode(models.Model):
|
|
show = models.ForeignKey(
|
|
'Show',
|
|
on_delete=models.CASCADE,
|
|
related_name='episodes',
|
|
help_text='The show this episode belongs to'
|
|
)
|
|
season = models.ForeignKey(
|
|
Season,
|
|
on_delete=models.CASCADE,
|
|
related_name='episodes',
|
|
help_text='The season this episode is from'
|
|
)
|
|
episode = models.IntegerField(
|
|
help_text='The position of this episode in the season. The first episode of the season to air would be episode number 1',
|
|
verbose_name='Episode Number'
|
|
)
|
|
name = models.CharField(
|
|
max_length=40,
|
|
help_text='The name given to this episode by its producers',
|
|
verbose_name='Episode Name'
|
|
)
|
|
summary = models.TextField(
|
|
help_text='A summary of this episode'
|
|
)
|
|
airdate = models.DateField(
|
|
help_text='The date this episode officially aired for the first time',
|
|
verbose_name='Original Air Date'
|
|
)
|
|
def __str__(self):
|
|
return "[s%dep%d] %s — %s"%(self.season.number,self.episode,self.show.name, self.name)
|
|
|
|
class Submission(TimestampedModel):
|
|
episode = models.ForeignKey(
|
|
Episode,
|
|
on_delete=models.CASCADE,
|
|
related_name='submissions',
|
|
help_text='What episode this link contains a mirror of',
|
|
verbose_name='Submitted For'
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='submissions',
|
|
help_text='The user who submitted this link'
|
|
)
|
|
url = models.URLField(
|
|
help_text='The link that was submitted',
|
|
)
|
|
title = models.TextField(
|
|
help_text='Title of the page',
|
|
blank=True
|
|
)
|
|
embed = models.URLField(
|
|
help_text='Embed player link for this episode',
|
|
blank=True,
|
|
verbose_name='Embed URL'
|
|
)
|
|
pinned = models.BooleanField(
|
|
help_text='Whether or not this submission is pinned',
|
|
default=False
|
|
)
|
|
tags = models.CharField(
|
|
help_text='Tags applied to this link submission',
|
|
max_length=200
|
|
)
|
|
def __str__(self):
|
|
return '%s\'s submission for %s — s%dep%d'%(self.user,self.episode.show.name,self.episode.season.number, self.episode.episode)
|
|
|
|
class SubmissionVote(TimestampedModel):
|
|
submission = models.ForeignKey(
|
|
Submission,
|
|
on_delete=models.CASCADE,
|
|
related_name='votes',
|
|
help_text='What this submission was cast on'
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='votes',
|
|
help_text='The user who cast this vote'
|
|
)
|
|
positive = models.BooleanField(
|
|
help_text='If this is true, the vote is an upvote. Otherwise, it is a downvote'
|
|
)
|
|
def __str__(self):
|
|
return "%s's vote on %s"%(self.user,self.submission)
|
|
|
|
class Favorite(TimestampedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE
|
|
)
|
|
episode = models.ForeignKey(
|
|
Episode,
|
|
on_delete=models.CASCADE
|
|
)
|
|
def __str__(self):
|
|
return "%s \u2665 %s"%(self.user, self.episode)
|
|
|
|
class Watch(TimestampedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE
|
|
)
|
|
episode = models.ForeignKey(
|
|
Episode,
|
|
on_delete=models.CASCADE
|
|
)
|
|
def __str__(self):
|
|
return "%s \U0001f441 %s"%(self.user, self.episode)
|
|
|
|
class DiscussionBoard(TimestampedModel):
|
|
show = models.ForeignKey(
|
|
Show,
|
|
on_delete=models.CASCADE,
|
|
related_name='discussion_boards',
|
|
help_text='The show this discussion was created for'
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='discussion_boards',
|
|
help_text='The user that created this discussion'
|
|
)
|
|
title = models.CharField(
|
|
max_length=100,
|
|
help_text='The title of the discussion'
|
|
)
|
|
views = models.IntegerField(
|
|
help_text='The amount of times this board has been viewed',
|
|
default=0
|
|
)
|
|
pinned = models.BooleanField(
|
|
help_text='Whether or not this board is pinned',
|
|
default=False
|
|
)
|
|
locked = models.BooleanField(
|
|
help_text='Whether or not this board is locked for further replies',
|
|
default=False
|
|
)
|
|
|
|
def latest_reply(self):
|
|
return self.replies.latest('timestamp')
|
|
|
|
def __str__(self):
|
|
return '[%s] "%s" by %s'%(self.show.abbr, self.title, self.user)
|
|
|
|
class DiscussionReply(TimestampedModel):
|
|
board = models.ForeignKey(
|
|
DiscussionBoard,
|
|
on_delete=models.CASCADE,
|
|
related_name='replies',
|
|
help_text='The discussion board this was created in reply to'
|
|
)
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='replies',
|
|
help_text='The user that posted this reply'
|
|
)
|
|
body = models.TextField(
|
|
help_text='The body of the response',
|
|
verbose_name='Body'
|
|
)
|
|
deleted = models.BooleanField(
|
|
help_text='Whether or not the content has been deleted by a moderator',
|
|
default=False
|
|
)
|
|
def __str__(self):
|
|
return '[%s] %s\'s response to "%s"'%(self.board.show.abbr,self.user, self.board.title)
|
|
|
|
class DiscussionVote(TimestampedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name='discussion_votes',
|
|
help_text='The user which cast this vote'
|
|
)
|
|
reply = models.ForeignKey(
|
|
DiscussionReply,
|
|
on_delete=models.CASCADE,
|
|
related_name='votes',
|
|
help_text='The reply this vote was cast on'
|
|
)
|
|
positive = models.BooleanField(
|
|
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
|
)
|
|
def __str__(self):
|
|
return "%s %s reply %d"%(self.user, '\U0001f592' if self.positive else '\U0001f44e', self.reply.pk)
|
|
|
|
class UserAction(TimestampedModel):
|
|
user = models.ForeignKey(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
help_text='The user who committed this action',
|
|
)
|
|
url = models.URLField(
|
|
max_length=100,
|
|
help_text='The URL of the content',
|
|
verbose_name = 'Content URL'
|
|
)
|
|
show = models.ForeignKey(
|
|
Show,
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
help_text='The show associated with the action',
|
|
)
|
|
act_type = models.IntegerField(
|
|
help_text='Type of this action',
|
|
default=0,
|
|
verbose_name = 'Action Type'
|
|
)
|
|
def act_str(self):
|
|
action = 'created'
|
|
|
|
if self.act_type == 1:
|
|
action = 'submitted'
|
|
elif self.act_type == 2:
|
|
action = 'replied'
|
|
elif self.act_type == 3:
|
|
action = 'modified'
|
|
elif self.act_type == 4:
|
|
action = 'deleted'
|
|
elif self.act_type == 5:
|
|
action = 'banned'
|
|
elif self.act_type == 6:
|
|
action = 'voted down'
|
|
elif self.act_type == 7:
|
|
action = 'voted up'
|
|
|
|
return action
|
|
def __str__(self):
|
|
return "%s %s %s"%(self.user, self.act_str(), self.url)
|