예제 #1
0
class Migration(migrations.Migration):
    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('reader', '0001_squashed'),
        ('socialaccount', '0003_extra_data_default_dict'),
    ]

    operations = [
        migrations.CreateModel(
            name='Bookmark',
            fields=[
                ('id',
                 models.AutoField(auto_created=True,
                                  primary_key=True,
                                  serialize=False,
                                  verbose_name='ID')),
                ('series',
                 models.ForeignKey(on_delete=models.deletion.CASCADE,
                                   to='reader.Series')),
                ('user',
                 models.ForeignKey(on_delete=models.deletion.CASCADE,
                                   related_name='bookmarks',
                                   to=settings.AUTH_USER_MODEL)),
            ],
        ),
        migrations.CreateModel(
            name='UserProfile',
            fields=[
                ('id',
                 models.AutoField(auto_created=True,
                                  primary_key=True,
                                  serialize=False,
                                  verbose_name='ID')),
                ('bio',
                 models.TextField(blank=True,
                                  verbose_name='biography',
                                  help_text="The user's biography.")),
                ('avatar',
                 models.ImageField(
                     blank=True,
                     help_text=(
                         "The user's avatar image. Must be up to 2 MBs."),
                     upload_to=_avatar_uploader,
                     storage=storage.CDNStorage((150, 150)),
                     validators=(validators.FileSizeValidator(2), ))),
                ('user',
                 models.OneToOneField(on_delete=models.deletion.CASCADE,
                                      related_name='profile',
                                      to=settings.AUTH_USER_MODEL)),
            ],
        ),
        migrations.AlterUniqueTogether(
            name='bookmark',
            unique_together={('series', 'user')},
        ),
    ]
예제 #2
0
class UserProfile(models.Model):
    """
    A model representing a user's profile.

    .. admonition:: TODO
       :class: warning

       Add links and let users hide their e-mail.
    """
    #: The user this profile belongs to.
    user = models.OneToOneField(User,
                                on_delete=models.CASCADE,
                                related_name='profile')
    #: The bio of the user.
    bio = models.TextField(blank=True,
                           verbose_name='biography',
                           help_text="The user's biography.")
    #: The avatar of the user.
    avatar = models.ImageField(
        help_text="The user's avatar image. Must be up to 2 MBs.",
        validators=(validators.FileSizeValidator(2), ),
        storage=storage.CDNStorage((150, 150)),
        upload_to=_avatar_uploader,
        blank=True)
    #: The token of the user. Used in the bookmarks feed.
    token = models.CharField(auto_created=True,
                             max_length=32,
                             unique=True,
                             editable=False)

    def save(self, *args, **kwargs):
        """Save the current instance."""
        data = f'{self.user.username}:{self.user.password}'
        self.token = blake2b(data.encode(),
                             digest_size=16,
                             key=settings.SECRET_KEY.encode()).hexdigest()
        super(UserProfile, self).save(*args, **kwargs)

    def get_directory(self) -> PurePath:
        """
        Get the storage directory of the object.

        :return: A path relative to
                 :const:`~MangAdventure.settings.MEDIA_ROOT`.
        """
        return PurePath('users', str(self.id))

    def __str__(self) -> str:
        """
        Return a string representing the object.

        :return: The user as a string.
        """
        return str(self.user)

    def __hash__(self) -> int:
        """
        Return the hash of the object.

        :return: An integer hash value.
        """
        return int(self.token, 16) & 0x7FFFFFFF
예제 #3
0
class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('groups', '0001_initial'),
    ]

    replaces = [
        ('reader', '0001_initial'),
        ('reader', '0002_reader_dates'),
        ('reader', '0003_chapter_groups'),
        ('reader', '0004_float_numbers'),
        ('reader', '0005_categories'),
        ('reader', '0006_remove_chapter_url'),
        ('reader', '0007_editable_slugs'),
        ('reader', '0008_add_indexes'),
        ('reader', '0009_cdn_storage'),
    ]

    operations = [
        migrations.CreateModel(
            name='Artist',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('name', models.CharField(
                    help_text="The artist's full name.",
                    max_length=100, db_index=True
                )),
            ],
        ),
        migrations.CreateModel(
            name='ArtistAlias',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('alias', models.CharField(
                    blank=True, help_text='Another name for the artist.',
                    max_length=100, unique=True, db_index=True
                )),
                ('artist', models.ForeignKey(
                    on_delete=models.deletion.CASCADE,
                    related_name='aliases', to='reader.Artist'
                )),
            ],
            options={
                'verbose_name': 'alias',
                'verbose_name_plural': 'aliases',
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Author',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('name', models.CharField(
                    help_text="The author's full name.",
                    max_length=100, db_index=True
                )),
            ],
        ),
        migrations.CreateModel(
            name='AuthorAlias',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('alias', models.CharField(
                    blank=True, help_text='Another name for the author.',
                    max_length=100, unique=True, db_index=True
                )),
                ('author', models.ForeignKey(
                    on_delete=models.deletion.CASCADE,
                    related_name='aliases', to='reader.Author'
                )),
            ],
            options={
                'verbose_name': 'alias',
                'verbose_name_plural': 'aliases',
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Chapter',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('title', models.CharField(
                    help_text='The title of the chapter.', max_length=250
                )),
                ('number', models.FloatField(
                    default=0, help_text='The number of the chapter.'
                )),
                ('volume', models.PositiveSmallIntegerField(
                    default=0, help_text=(
                        'The volume of the chapter. Leave '
                        'as 0 if the series has no volumes.'
                    )
                )),
                ('file', models.FileField(
                    blank=True, help_text=(
                        'Upload a zip or cbz file containing the '
                        'chapter pages. Its size cannot exceed 50 MBs '
                        'and it must not contain more than 1 subfolder.'
                    ), upload_to='', validators=(
                        validators.FileSizeValidator(50),
                        validators.zipfile_validator
                    )
                )),
                ('final', models.BooleanField(
                    default=False, help_text='Is this the final chapter?'
                )),
                ('modified', models.DateTimeField(
                    default=datetime.now(tz=timezone.utc)
                )),
                ('uploaded', models.DateTimeField(
                    default=datetime.now(tz=timezone.utc)
                )),
            ],
            options={
                'get_latest_by': ('uploaded', 'modified'),
                'ordering': ('series', 'volume', 'number'),
            },
        ),
        migrations.CreateModel(
            name='Page',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('image', models.ImageField(
                    storage=storage.CDNStorage(),
                    upload_to='', max_length=255
                )),
                ('number', _PageNumberField()),
                ('chapter', models.ForeignKey(
                    on_delete=models.deletion.CASCADE,
                    related_name='pages', to='reader.Chapter'
                )),
            ],
            options={
                'ordering': ('chapter', 'number')
            },
        ),
        migrations.CreateModel(
            name='Series',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('title', models.CharField(
                    help_text='The title of the series.',
                    max_length=250, db_index=True
                )),
                ('description', models.TextField(
                    blank=True, help_text='The description of the series.'
                )),
                ('cover', models.ImageField(
                    help_text=(
                        'Upload a cover image for the series.'
                        ' Its size must not exceed 2 MBs.'
                    ), upload_to=_cover_uploader,
                    storage=storage.CDNStorage((300, 300)),
                    validators=(validators.FileSizeValidator(2),)
                )),
                ('slug', models.SlugField(
                    primary_key=False, unique=True,
                    blank=True, help_text=(
                        'The unique slug of the series.'
                        ' Will be used in the URL.'
                    ), verbose_name='Custom slug',
                )),
                ('completed', models.BooleanField(
                    default=False, help_text='Is the series completed?'
                )),
                ('artists', models.ManyToManyField(
                    blank=True, to='reader.Artist'
                )),
                ('authors', models.ManyToManyField(
                    blank=True, to='reader.Author'
                )),
                ('modified', models.DateTimeField(
                    default=datetime.now(tz=timezone.utc)
                )),
            ],
            options={
                'get_latest_by': 'modified',
                'verbose_name_plural': 'series',
            },
        ),
        migrations.CreateModel(
            name='SeriesAlias',
            fields=[
                ('id', models.AutoField(
                    auto_created=True, primary_key=True,
                    serialize=False, verbose_name='ID'
                )),
                ('alias', models.CharField(
                    blank=True, help_text='Another title for the series.',
                    max_length=250, unique=True, db_index=True
                )),
                ('series', models.ForeignKey(
                    on_delete=models.deletion.CASCADE,
                    related_name='aliases', to='reader.Series'
                )),
            ],
            options={
                'verbose_name': 'alias',
                'verbose_name_plural': 'aliases',
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.CharField(
                    auto_created=True, primary_key=True,
                    max_length=25, serialize=False, default=''
                )),
                ('name', models.CharField(
                    max_length=25, help_text=(
                        'The name of the category. Must be '
                        'unique and cannot be changed once set.'
                    ), unique=True
                )),
                ('description', models.TextField(
                    help_text='A description for the category.'
                )),
            ],
            options={'verbose_name_plural': 'categories'},
        ),
        migrations.AddField(
            model_name='chapter',
            name='series',
            field=models.ForeignKey(
                help_text='The series this chapter belongs to.',
                on_delete=models.deletion.CASCADE,
                related_name='chapters', to='reader.Series'
            ),
        ),
        migrations.AddField(
            model_name='chapter',
            name='groups',
            field=models.ManyToManyField(
                blank=True, related_name='releases', to='groups.Group'
            ),
        ),
        migrations.AddField(
            model_name='series',
            name='categories',
            field=models.ManyToManyField(
                blank=True, to='reader.Category'
            ),
        ),
        migrations.AlterField(
            model_name='chapter',
            name='modified',
            field=models.DateTimeField(auto_now=True),
        ),
        migrations.AlterField(
            model_name='chapter',
            name='uploaded',
            field=models.DateTimeField(auto_now_add=True, db_index=True),
        ),
        migrations.AlterField(
            model_name='series',
            name='modified',
            field=models.DateTimeField(auto_now=True),
        ),
        migrations.AlterUniqueTogether(
            name='chapter',
            unique_together={('series', 'volume', 'number')},
        ),
    ]
예제 #4
0
class Migration(migrations.Migration):

    initial = True

    dependencies = []

    # TODO: remove squashed migrations after application
    replaces = [
        ('groups', '0001_initial'),
        ('groups', '0002_irc_reddit'),
        ('groups', '0003_cdn_storage'),
    ]

    operations = [
        migrations.CreateModel(
            name='Group',
            fields=[
                ('id',
                 models.SmallIntegerField(auto_created=True,
                                          primary_key=True,
                                          serialize=False)),
                ('name',
                 models.CharField(help_text="The group's name.",
                                  max_length=100)),
                ('website',
                 models.URLField(blank=True,
                                 help_text="The group's website.")),
                ('email',
                 models.EmailField(blank=True,
                                   max_length=254,
                                   help_text="The group's E-mail address.")),
                ('discord',
                 fields.DiscordURLField(
                     blank=True, help_text="The group's Discord link.")),
                ('twitter',
                 fields.TwitterField(
                     blank=True,
                     max_length=15,
                     help_text="The group's Twitter username.")),
                ('description',
                 models.TextField(blank=True,
                                  help_text='A description for the group.')),
                ('logo',
                 models.ImageField(
                     blank=True,
                     help_text=("Upload the group's logo. "
                                "Its size must not exceed 2 MBs."),
                     upload_to=_logo_uploader,
                     storage=storage.CDNStorage((150, 150)),
                     validators=(validators.FileSizeValidator(2), ))),
                ('irc',
                 models.CharField(blank=True,
                                  max_length=63,
                                  help_text="The group's IRC.")),
                (('reddit',
                  fields.RedditField(
                      blank=True,
                      help_text=("The group's Reddit username or "
                                 "subreddit. (Include /u/ or /r/)"),
                      max_length=24)))
            ],
        ),
        migrations.CreateModel(
            name='Member',
            fields=[('id',
                     models.AutoField(auto_created=True,
                                      primary_key=True,
                                      serialize=False,
                                      verbose_name='ID')),
                    ('name',
                     models.CharField(help_text="The member's name.",
                                      max_length=100)),
                    ('twitter',
                     fields.TwitterField(
                         blank=True,
                         max_length=15,
                         help_text="The member's Twitter username.")),
                    ('discord',
                     fields.DiscordNameField(
                         blank=True,
                         max_length=37,
                         help_text=
                         "The member's Discord username and discriminator.")),
                    ('irc',
                     models.CharField(blank=True,
                                      max_length=63,
                                      help_text="The member's IRC username.")),
                    (('reddit',
                      fields.RedditField(
                          blank=True,
                          max_length=21,
                          help_text="The member's Reddit username.")))],
        ),
        migrations.CreateModel(
            name='Role',
            fields=[
                ('id',
                 models.AutoField(auto_created=True,
                                  primary_key=True,
                                  serialize=False,
                                  verbose_name='ID')),
                ('role',
                 models.CharField(
                     choices=(('LD', 'Leader'), ('TL', 'Translator'),
                              ('PR', 'Proofreader'), ('CL', 'Cleaner'),
                              ('RD', 'Redrawer'), ('TS', 'Typesetter'),
                              ('RP', 'Raw Provider'), ('QC',
                                                       'Quality Checker')),
                     max_length=2)),
                ('group',
                 models.ForeignKey(on_delete=models.deletion.CASCADE,
                                   related_name='roles',
                                   to='groups.Group')),
                ('member',
                 models.ForeignKey(on_delete=models.deletion.CASCADE,
                                   related_name='roles',
                                   to='groups.Member')),
            ],
            options={
                'verbose_name': 'Role',
            },
        ),
        migrations.AddField(model_name='member',
                            name='groups',
                            field=models.ManyToManyField(
                                blank=False,
                                related_name='members',
                                through='groups.Role',
                                to='groups.Group')),
        migrations.AlterUniqueTogether(
            name='role',
            unique_together={('member', 'role', 'group')},
        ),
    ]
예제 #5
0
class Page(models.Model):
    """A model representing a page."""
    #: The chapter this page belongs to.
    chapter = models.ForeignKey(Chapter,
                                related_name='pages',
                                on_delete=models.CASCADE)
    #: The image of the page.
    image = models.ImageField(storage=storage.CDNStorage(), max_length=255)
    #: The number of the page.
    number = _PageNumberField()

    class Meta:
        ordering = ('chapter', 'number')

    @cached_property
    def _file_name(self) -> str:
        return self.image.name.rsplit('/')[-1]

    def get_absolute_url(self) -> str:
        """
        Get the absolute URL of the object.

        :return: The URL of :func:`reader.views.chapter_page`.
        """
        return reverse('reader:page',
                       args=(self.chapter.series.slug, self.chapter.volume,
                             self.chapter.number, self.number))

    @cached_property
    def preload(self) -> models.QuerySet:
        """
        Get the pages that will be preloaded.

        .. admonition:: TODO
           :class: warning

           Make the number of preloaded pages configurable.

        :return: The three next pages of the chapter.
        """
        return self.__class__.objects.filter(
            chapter_id=self.chapter_id,
            number__range=(self.number + 1, self.number + 3)).only('image')

    def __str__(self) -> str:
        """
        Return a string representing the object.

        :return: The title of the series, the volume, number, title
                 of the chapter, and the file name of the page.
        """
        return '{0.series.title} - {0.volume}/{0.number} #{1:03d}' \
            .format(self.chapter, self.number)

    def __eq__(self, other: Any) -> bool:
        """
        Check whether this object is equal to another.

        If the other object is a number, the objects are equal if
        the ``number`` of this object is equal to the other object.

        Otherwise, the objects are equal if they have the same base model
        and their ``chapter`` and ``number`` are respectively equal.

        :param other: Any other object.

        :return: ``True`` if the objects are equal.
        """
        if isinstance(other, (float, int)):
            return self.number == other

        if not isinstance(other, self.__class__):
            return False

        return self.chapter == other.chapter and self.number == other.number

    def __gt__(self, other: Any) -> bool:
        """
        Check whether this object is greater than another.

        If the other object is a number, this object is greater
        if its ``number`` is greater than the other object.

        Otherwise, it's greater if the objects have the same base model
        and the ``number`` of this object is greater than the other's.

        :param other: Any other object.

        :return: ``True`` if this object is greater.

        :raises TypeError: If the other object is neither a tuple,
                           nor a ``Page`` model.
        """
        if isinstance(other, (float, int)):
            return self.number > other

        if isinstance(other, self.__class__):
            return self.number > other.number

        raise TypeError(
            "'>' not supported between instances of '{}' and '{}'".format(
                self.__class__, other.__class__))

    def __lt__(self, other: Any) -> bool:
        """
        Check whether this object is less than another.

        If the other object is a number, this object is lesser
        if its ``number`` is less than the other object.

        Otherwise, it's lesser if the objects have the same base model
        and the ``number`` of this object is less than the other's.

        :param other: Any other object.

        :return: ``True`` if this object is lesser.

        :raises TypeError: If the other object is neither a tuple,
                           nor a ``Page`` model.
        """
        if isinstance(other, (float, int)):
            return self.number < other

        if isinstance(other, self.__class__):
            return self.number < other.number

        raise TypeError(
            "'<' not supported between instances of '{}' and '{}'".format(
                self.__class__, other.__class__))

    def __hash__(self) -> int:
        """
        Return the hash of the object.

        :return: An integer hash value.
        """
        name = path.splitext(self._file_name)[0]
        if len(name) != 32:  # pragma: no cover
            return abs(hash(str(self)))
        return int(name, 16)
예제 #6
0
class Series(models.Model):
    #: The title of the series.
    title = models.CharField(max_length=250,
                             db_index=True,
                             help_text='The title of the series.')
    #: The unique slug of the series.
    slug = models.SlugField(
        blank=True,
        unique=True,
        verbose_name='Custom slug',
        help_text='The unique slug of the series. Will be used in the URL.')
    #: The description of the series.
    description = models.TextField(blank=True,
                                   help_text='The description of the series.')
    #: The cover image of the series.
    cover = models.ImageField(help_text=('Upload a cover image for the series.'
                                         ' Its size must not exceed 2 MBs.'),
                              upload_to=_cover_uploader,
                              validators=(validators.FileSizeValidator(2), ),
                              storage=storage.CDNStorage((300, 300)))
    #: The authors of the series.
    authors = models.ManyToManyField(Author, blank=True)
    #: The artists of the series.
    artists = models.ManyToManyField(Artist, blank=True)
    #: The categories of the series.
    categories = models.ManyToManyField(Category, blank=True)
    #: The status of the series.
    completed = models.BooleanField(default=False,
                                    help_text='Is the series completed?')
    #: The date the series was created.
    created = models.DateTimeField(auto_now_add=True, db_index=True)
    #: The modification date of the series.
    modified = models.DateTimeField(auto_now=True)
    #: The chapter name format of the series.
    format = models.CharField(
        max_length=100,
        default='Vol. {volume}, Ch. {number}: {title}',
        help_text='The format used to render the chapter names.',
        verbose_name='chapter name format')
    #: The aliases of the series.
    aliases = GenericRelation(to=Alias, blank=True, related_query_name='alias')
    #: The person who manages this series.
    manager = models.ForeignKey(
        User,
        editable=True,
        blank=False,
        null=True,
        help_text='The person who manages this series.',
        on_delete=models.SET_NULL,
        limit_choices_to=(models.Q(is_superuser=True)
                          | models.Q(groups__name='Scanlator')))

    def get_absolute_url(self) -> str:
        """
        Get the absolute URL of the object.

        :return: The URL of :func:`reader.views.series`.
        """
        return reverse('reader:series', args=(self.slug, ))

    def get_directory(self) -> PurePath:
        """
        Get the storage directory of the object.

        :return: A path relative to
                 :const:`~MangAdventure.settings.MEDIA_ROOT`.
        """
        return PurePath('series', self.slug)

    class Meta:
        verbose_name_plural = 'series'
        get_latest_by = 'modified'

    def save(self, *args, **kwargs):
        """Save the current instance."""
        self.slug = slugify(self.slug or self.title)
        super().save(*args, **kwargs)

    def __str__(self) -> str:
        """
        Return a string representing the object.

        :return: The title of the series.
        """
        return self.title
예제 #7
0
 def _thumb(self) -> models.ImageField:
     img = self.image
     img.storage = storage.CDNStorage((150, 150))
     return img