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')}, ), ]
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
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')}, ), ]
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')}, ), ]
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)
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
def _thumb(self) -> models.ImageField: img = self.image img.storage = storage.CDNStorage((150, 150)) return img