mirror of
https://github.com/ajurna/cbwebreader.git
synced 2025-12-06 06:17:17 +00:00
Rewrite of Comic lists so that tehy have thumbnails. (#26)
This commit is contained in:
@@ -20,6 +20,8 @@ COMIC_BOOK_VOLUME = '/path/to/comic/folder'
|
||||
|
||||
STATIC_ROOT = '/static'
|
||||
|
||||
MEDIA_ROOT = './media'
|
||||
|
||||
# This expects the office winrar unrar command line tool for windows or linux.
|
||||
# Will work without setting if it is in the path
|
||||
# UNRAR_TOOL = 'unrar.exe'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -86,6 +86,7 @@ target/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
media
|
||||
|
||||
.env
|
||||
db.sqlite3
|
||||
|
||||
427
LICENSE
Normal file
427
LICENSE
Normal file
@@ -0,0 +1,427 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More_considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
|
||||
including for purposes of Section 3(b); and
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -39,6 +39,7 @@ INSTALLED_APPS = (
|
||||
"comic",
|
||||
"comic_auth",
|
||||
'django_extensions',
|
||||
'imagekit',
|
||||
)
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -108,6 +109,11 @@ STATICFILES_DIRS = [
|
||||
|
||||
STATIC_ROOT = os.getenv('STATIC_ROOT', None)
|
||||
|
||||
|
||||
MEDIA_ROOT = os.getenv('MEDIA_ROOT', None)
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
LOGIN_REDIRECT_URL = "/comic/"
|
||||
|
||||
LOGIN_URL = "/login/"
|
||||
|
||||
@@ -16,5 +16,8 @@ server {
|
||||
location /static/ {
|
||||
alias /static/;
|
||||
}
|
||||
location /media/ {
|
||||
alias /media/;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,10 +13,10 @@ Including another URLconf
|
||||
1. Add an import: from blog import urls as blog_urls
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
|
||||
"""
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
|
||||
import comic.views
|
||||
import comic_auth.views
|
||||
@@ -28,7 +28,7 @@ urlpatterns = [
|
||||
url(r"^setup/", comic.views.initial_setup),
|
||||
url(r"^comic/", include("comic.urls")),
|
||||
url(r"^admin/", admin.site.urls),
|
||||
# url(r'^silk/', include('silk.urls', namespace='silk'))
|
||||
|
||||
]
|
||||
if settings.SILK_ENABLED:
|
||||
urlpatterns += [url(r'^silk/', include('silk.urls', namespace='silk'))]
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
from os.path import isdir
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from loguru import logger
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
from comic.models import ComicBook, Directory, ComicStatus
|
||||
from comic.models import ComicBook, Directory
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,6 +13,7 @@ class Command(BaseCommand):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.OUTPUT = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
@@ -91,4 +90,10 @@ class Command(BaseCommand):
|
||||
book.version = 1
|
||||
book.save()
|
||||
except ComicBook.DoesNotExist:
|
||||
ComicBook.process_comic_book(file, directory)
|
||||
book = ComicBook.process_comic_book(file, directory)
|
||||
try:
|
||||
book.generate_thumbnail()
|
||||
except UnidentifiedImageError:
|
||||
book.generate_thumbnail(1)
|
||||
except:
|
||||
pass
|
||||
|
||||
19
comic/migrations/0022_comicbook_thumbnail.py
Normal file
19
comic/migrations/0022_comicbook_thumbnail.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2 on 2021-04-21 11:13
|
||||
|
||||
from django.db import migrations
|
||||
import imagekit.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0021_delete_setting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='comicbook',
|
||||
name='thumbnail',
|
||||
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
|
||||
),
|
||||
]
|
||||
19
comic/migrations/0023_directory_thumbnail.py
Normal file
19
comic/migrations/0023_directory_thumbnail.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2 on 2021-04-21 17:44
|
||||
|
||||
from django.db import migrations
|
||||
import imagekit.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0022_comicbook_thumbnail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail',
|
||||
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
|
||||
),
|
||||
]
|
||||
29
comic/migrations/0024_auto_20210422_0855.py
Normal file
29
comic/migrations/0024_auto_20210422_0855.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2 on 2021-04-22 07:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comic', '0023_directory_thumbnail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='comicbook',
|
||||
name='thumbnail_index',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail_index',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='directory',
|
||||
name='thumbnail_issue',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='directory_thumbnail_issue', to='comic.comicbook'),
|
||||
),
|
||||
]
|
||||
132
comic/models.py
132
comic/models.py
@@ -8,15 +8,18 @@ from os import listdir
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union, Tuple
|
||||
|
||||
import PyPDF4
|
||||
import PyPDF4.utils
|
||||
import rarfile
|
||||
from PIL import Image
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db import models
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
import PyPDF4
|
||||
import PyPDF4.utils
|
||||
|
||||
import rarfile
|
||||
from imagekit.models import ProcessedImageField
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
from comic.errors import NotCompatibleArchive
|
||||
|
||||
@@ -28,6 +31,15 @@ class Directory(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE)
|
||||
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
|
||||
thumbnail = ProcessedImageField(upload_to='thumbs',
|
||||
processors=[ResizeToFill(200, 300)],
|
||||
format='JPEG',
|
||||
options={'quality': 60},
|
||||
null=True)
|
||||
thumbnail_issue = models.ForeignKey("ComicBook", null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='directory_thumbnail_issue')
|
||||
thumbnail_index = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -35,6 +47,39 @@ class Directory(models.Model):
|
||||
def __str__(self):
|
||||
return "Directory: {0}; {1}".format(self.name, self.parent)
|
||||
|
||||
def mark_read(self, user):
|
||||
books = ComicBook.objects.filter(directory=self)
|
||||
for book in books:
|
||||
book.mark_read(user)
|
||||
|
||||
def mark_unread(self, user):
|
||||
books = ComicBook.objects.filter(directory=self)
|
||||
for book in books:
|
||||
book.mark_unread(user)
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
if self.thumbnail:
|
||||
return self.thumbnail.url
|
||||
else:
|
||||
self.generate_thumbnail()
|
||||
return self.thumbnail.url
|
||||
|
||||
def generate_thumbnail(self):
|
||||
book = ComicBook.objects.filter(directory=self).order_by('file_name').first()
|
||||
if not book:
|
||||
return
|
||||
img, content_type = book.get_image(0)
|
||||
pil_data = Image.open(img)
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
img,
|
||||
None,
|
||||
f'{self.name}.jpg',
|
||||
content_type,
|
||||
pil_data.tell(),
|
||||
None
|
||||
)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self.get_path()
|
||||
@@ -63,6 +108,10 @@ class Directory(models.Model):
|
||||
self.parent.get_path_objects(p)
|
||||
return p
|
||||
|
||||
@property
|
||||
def url_safe_selector(self):
|
||||
return urlsafe_base64_encode(self.selector.bytes)
|
||||
|
||||
|
||||
class ComicBook(models.Model):
|
||||
file_name = models.TextField()
|
||||
@@ -70,12 +119,33 @@ class ComicBook(models.Model):
|
||||
directory = models.ForeignKey(Directory, blank=True, null=True, on_delete=models.CASCADE)
|
||||
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
|
||||
version = models.IntegerField(default=1)
|
||||
thumbnail = ProcessedImageField(upload_to='thumbs',
|
||||
processors=[ResizeToFill(200, 300)],
|
||||
format='JPEG',
|
||||
options={'quality': 60},
|
||||
null=True)
|
||||
thumbnail_index = models.PositiveIntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
|
||||
def mark_read(self, user: User):
|
||||
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
|
||||
status.mark_read()
|
||||
|
||||
def mark_unread(self, user: User):
|
||||
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
|
||||
status.mark_unread()
|
||||
|
||||
def mark_previous(self, user):
|
||||
books = ComicBook.objects.filter(directory=self.directory).order_by('file_name')
|
||||
for book in books:
|
||||
if book == self:
|
||||
break
|
||||
book.mark_read(user)
|
||||
|
||||
@property
|
||||
def selector_string(self):
|
||||
def url_safe_selector(self):
|
||||
return urlsafe_base64_encode(self.selector.bytes)
|
||||
|
||||
def get_pdf(self):
|
||||
@@ -99,6 +169,26 @@ class ComicBook(models.Model):
|
||||
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
|
||||
return out
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
if self.thumbnail:
|
||||
return self.thumbnail.url
|
||||
else:
|
||||
self.generate_thumbnail()
|
||||
return self.thumbnail.url
|
||||
|
||||
def generate_thumbnail(self, page_index: int = 0):
|
||||
img, content_type = self.get_image(page_index)
|
||||
pil_data = Image.open(img)
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
img,
|
||||
None,
|
||||
f'{self.file_name}.jpg',
|
||||
content_type,
|
||||
pil_data.tell(),
|
||||
None
|
||||
)
|
||||
self.save()
|
||||
|
||||
def is_last_page(self, page):
|
||||
if (self.page_count - 1) == page:
|
||||
return True
|
||||
@@ -209,22 +299,15 @@ class ComicBook(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
return [cp for cp in ComicPage.objects.filter(Comic=self).order_by("index")]
|
||||
|
||||
def page_name(self, index):
|
||||
return ComicPage.objects.get(Comic=self, index=index).page_file_name
|
||||
|
||||
@staticmethod
|
||||
def process_comic_book(comic_file_name: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
||||
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
||||
"""
|
||||
|
||||
:type comic_file_name: str
|
||||
:type comic_file_path: str
|
||||
:type directory: Directory
|
||||
"""
|
||||
try:
|
||||
book = ComicBook.objects.get(file_name=comic_file_name, version=0)
|
||||
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
|
||||
book.directory = directory
|
||||
book.version = 1
|
||||
book.save()
|
||||
@@ -232,12 +315,12 @@ class ComicBook(models.Model):
|
||||
except ComicBook.DoesNotExist:
|
||||
pass
|
||||
|
||||
book = ComicBook(file_name=comic_file_name, directory=directory if directory else None)
|
||||
book = ComicBook(file_name=comic_file_path.name, directory=directory if directory else None)
|
||||
book.save()
|
||||
try:
|
||||
archive, archive_type = book.get_archive()
|
||||
except NotCompatibleArchive:
|
||||
return comic_file_name
|
||||
return comic_file_path
|
||||
|
||||
if archive_type == 'archive':
|
||||
book.verify_pages()
|
||||
@@ -340,9 +423,18 @@ class ComicStatus(models.Model):
|
||||
unread = models.BooleanField(default=True)
|
||||
finished = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def read(self):
|
||||
return self.last_read_page
|
||||
def mark_read(self):
|
||||
page_count = ComicPage.objects.filter(Comic=self.comic).count()
|
||||
self.unread = False
|
||||
self.finished = True
|
||||
self.last_read_page = page_count - 1
|
||||
self.save()
|
||||
|
||||
def mark_unread(self):
|
||||
self.unread = True
|
||||
self.finished = False
|
||||
self.last_read_page = 0
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<script type="text/javascript" src="{% static "reveal.js/reveal.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "reveal.js/plugin/menu/menu.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/hammer.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/isotope.pkgd.min.js" %}"></script>
|
||||
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,121 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<form id="comic_form" method="post" action="/comic/edit/">
|
||||
{% csrf_token %}
|
||||
<table class="table table-bordered table-striped table-hover" id="comic_list">
|
||||
<caption><h2>Comics</h2> mark selected issues as:
|
||||
<select name="func" id="func_selector">
|
||||
<option value="choose">Choose...</option>
|
||||
<option value="read">Read</option>
|
||||
<option value="unread">Un-Read</option>
|
||||
</select>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select-all"><input type="checkbox" id="select-all-cb"></th>
|
||||
<th style="text-align: center;"><span class="fa fa-file"></span></th>
|
||||
<th width="100%">File/Folder</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="clickable-row" data-href="/comic/">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>loading data</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<div class="row">
|
||||
<div class="input-group">
|
||||
<input type="text" id="quicksearch" class="form-control" placeholder="Search" aria-label="Search list of comics" aria-describedby="button-addon4">
|
||||
<div id="filters" class="input-group-append" id="button-addon4">
|
||||
<button class="btn btn-outline-secondary filters" type="button" data-filter="*">All</button>
|
||||
<button class="btn btn-outline-secondary filters" type="button" data-filter=".read">Read</button>
|
||||
<button class="btn btn-outline-secondary filters" type="button" data-filter=".unread">Unread</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
|
||||
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container comic-container">
|
||||
<div class="row grid">
|
||||
{% for file in files %}
|
||||
<div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}">
|
||||
<div class="card" style="width: 200px;">
|
||||
{% if file.item_type == 'Directory' %}
|
||||
<a href="{% url "comic_list" file.selector %}">
|
||||
{% elif file.item_type == 'ComicBook' %}
|
||||
<a href="{% url "read_comic" file.selector %}">
|
||||
{% endif %}
|
||||
|
||||
{% if file.obj.thumbnail %}
|
||||
<img src="{{file.obj.thumbnail.url}}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
|
||||
{% else %}
|
||||
{% if file.item_type == 'Directory' %}
|
||||
<img src="{% url 'directory_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
|
||||
{% elif file.item_type == 'ComicBook' %}
|
||||
<img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{% if file.item_type == 'Directory' %}
|
||||
<a href="{% url "comic_list" file.selector %}" class="search-name">
|
||||
{% elif file.item_type == 'ComicBook' %}
|
||||
<a href="{% url "read_comic" file.selector %}" class="search-name">
|
||||
{% endif %}
|
||||
{{ file.name }}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: {{ file.percent }}%;" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100">{{ file.percent }}%</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group" role="group" aria-label="Comic Actions">
|
||||
<button type="button" class="btn btn-primary" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book"></i></button>
|
||||
<button type="button" class="btn btn-primary" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open"></i></button>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
|
||||
{% if file.item_type != 'Directory' %}
|
||||
<button type="button" class="btn btn-primary dropdown-item" title="Mark Previous Read"><i class="fas fa-book" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_previous')"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button>
|
||||
{% endif %}
|
||||
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table = $('#comic_list').DataTable({
|
||||
"processing": true,
|
||||
"stateSave": true,
|
||||
"ajax": {
|
||||
"type": "POST",
|
||||
"url": "{{ json_url }}",
|
||||
"data": function ( d ) {
|
||||
d.csrfmiddlewaretoken = Cookies.get('csrftoken');
|
||||
}
|
||||
},
|
||||
"rowCallback": function( row, data, index ) {
|
||||
var r = $(row);
|
||||
var cols = $('td:nth-child(n+2)', row);
|
||||
|
||||
if (data['selector'] === '0') {
|
||||
|
||||
} else {
|
||||
cols.attr('data-href', data['url']);
|
||||
cols.attr('style', 'cursor: pointer;');
|
||||
cols.click(function () {
|
||||
window.document.location = $(this).data("href");
|
||||
});
|
||||
}
|
||||
var tds = $('td:eq(0)', row);
|
||||
if (data['type'] === 'directory') {
|
||||
tds.html('');
|
||||
} else {
|
||||
tds.html('<input type="checkbox" name="selected" value="'+data['selector']+'" data-type="'+data['type']+'"/>');
|
||||
var cb = $('input', tds);
|
||||
cb.change(function() {
|
||||
$(this).closest('tr').toggleClass('info')
|
||||
});
|
||||
}
|
||||
},
|
||||
"drawCallback": function( settings ) {
|
||||
var tds = $('table tr td:first-child');
|
||||
tds.click(function(event){
|
||||
if (!$(event.target).is('input')) {
|
||||
var $cb = $('input', this);
|
||||
$cb.click();
|
||||
}
|
||||
});
|
||||
},
|
||||
"columns": [
|
||||
{ "data" : "blank", "orderable": false },
|
||||
{ "data" : "icon", "orderable": false },
|
||||
{ "data" : "name" },
|
||||
{"data": "label"}
|
||||
],
|
||||
|
||||
"order": [[2, 'asc']]
|
||||
var qsRegex;
|
||||
var buttonFilter;
|
||||
var $grid = $('.comic-container').isotope({
|
||||
itemSelector: '.grid-item',
|
||||
layoutMode: 'fitRows',
|
||||
filter: function() {
|
||||
var $this = $(this);
|
||||
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
|
||||
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
|
||||
return searchResult && buttonResult;
|
||||
{#return searchResult#}
|
||||
}
|
||||
});
|
||||
$(".clickable-row").click(function() {
|
||||
window.document.location = $(this).data("href");
|
||||
$('#filters').on( 'click', 'button', function() {
|
||||
buttonFilter = $( this ).attr('data-filter');
|
||||
sessionStorage.setItem(window.location.href+"button", buttonFilter);
|
||||
$grid.isotope();
|
||||
});
|
||||
$('#func_selector').on('change', function() {
|
||||
$.post('/comic/edit/', $('#comic_form').serialize())
|
||||
.done(function(){
|
||||
$('#func_selector').val('choose');
|
||||
$('#select-all input').prop('checked', false);
|
||||
table.ajax.reload();
|
||||
}).fail(function(){
|
||||
alert('Error Submitting Change');
|
||||
})
|
||||
|
||||
});
|
||||
$('#select-all').click(function(event){
|
||||
var cb = $('input', this);
|
||||
if (!$(event.target).is('input')) {
|
||||
cb.click();
|
||||
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
|
||||
qsRegex = new RegExp($quicksearch.val(), 'gi');
|
||||
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
|
||||
$grid.isotope();
|
||||
}) );
|
||||
|
||||
// debounce so filtering doesn't happen every millisecond
|
||||
function debounce( fn, threshold ) {
|
||||
var timeout;
|
||||
threshold = threshold || 100;
|
||||
return function debounced() {
|
||||
clearTimeout( timeout );
|
||||
var args = arguments;
|
||||
var _this = this;
|
||||
function delayed() {
|
||||
fn.apply( _this, args );
|
||||
}
|
||||
$('table tr td:first-child input').each(function(chkbx) {
|
||||
row = $(this);
|
||||
if (row.prop('checked') !== cb.prop('checked')){
|
||||
row.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
} );
|
||||
timeout = setTimeout( delayed, threshold );
|
||||
};
|
||||
}
|
||||
setInterval(function (){
|
||||
$grid.isotope();
|
||||
}, 1000)
|
||||
|
||||
let field = document.getElementById("quicksearch");
|
||||
|
||||
// See if we have an autosave value
|
||||
// (this will only happen if the page is accidentally refreshed)
|
||||
if (sessionStorage.getItem(window.location.href+'text') || sessionStorage.getItem(window.location.href+'button')) {
|
||||
// Restore the contents of the text field
|
||||
field.value = sessionStorage.getItem(window.location.href+'text');
|
||||
qsRegex = new RegExp($quicksearch.val(), 'gi');
|
||||
buttonFilter = sessionStorage.getItem(window.location.href+'button');
|
||||
$grid.isotope();
|
||||
}
|
||||
|
||||
// Listen for changes in the text field
|
||||
field.addEventListener("change", function() {
|
||||
// And save the results into the session storage object
|
||||
|
||||
});
|
||||
|
||||
function comic_action(selector, item_type, action) {
|
||||
$.ajax({
|
||||
url: '/comic/action/' + action + '/' + item_type + '/' + selector + '/',
|
||||
success: function (){window.location.reload()}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -103,43 +103,23 @@ class ComicBookTests(TestCase):
|
||||
folders = generate_directory(user)
|
||||
dir1 = folders[0]
|
||||
self.assertEqual(dir1.name, "test_folder")
|
||||
self.assertEqual(dir1.type, "directory")
|
||||
self.assertEqual("fa-folder-open", dir1.icon)
|
||||
d = Directory.objects.get(name="test_folder", parent__isnull=True)
|
||||
location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes))
|
||||
self.assertEqual(dir1.location, location)
|
||||
self.assertEqual(dir1.label, '<center><span class="label label-default">Empty</span></center>')
|
||||
self.assertEqual(dir1.item_type, "Directory")
|
||||
|
||||
dir2 = folders[1]
|
||||
self.assertEqual(dir2.name, "test1.rar")
|
||||
self.assertEqual(dir2.type, "book")
|
||||
self.assertEqual("fa-book", dir2.icon)
|
||||
c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir2.location, location)
|
||||
self.assertEqual(dir2.label, '<center><span class="label label-default">Unread</span></center>')
|
||||
self.assertEqual(dir2.item_type, "ComicBook")
|
||||
|
||||
dir3 = folders[2]
|
||||
self.assertEqual(dir3.name, "test2.rar")
|
||||
self.assertEqual(dir3.type, "book")
|
||||
self.assertEqual("fa-book", dir3.icon)
|
||||
c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir3.location, location)
|
||||
self.assertEqual(dir3.label, '<center><span class="label label-primary">3/4</span></center>')
|
||||
self.assertEqual(dir2.item_type, "ComicBook")
|
||||
|
||||
dir4 = folders[3]
|
||||
self.assertEqual(dir4.name, "test3.rar")
|
||||
self.assertEqual(dir4.type, "book")
|
||||
self.assertEqual("fa-book", dir3.icon)
|
||||
c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True)
|
||||
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
|
||||
self.assertEqual(dir4.location, location)
|
||||
self.assertEqual(dir4.label, '<center><span class="label label-default">Unread</span></center>')
|
||||
self.assertEqual(dir4.item_type, "ComicBook")
|
||||
|
||||
def test_pages(self):
|
||||
book = ComicBook.objects.get(file_name="test1.rar")
|
||||
pages = book.pages
|
||||
pages = [cp for cp in ComicPage.objects.filter(Comic=book).order_by("index")]
|
||||
self.assertEqual(pages[0].page_file_name, "img1.jpg")
|
||||
self.assertEqual(pages[0].index, 0)
|
||||
self.assertEqual(pages[1].page_file_name, "img2.png")
|
||||
@@ -149,9 +129,6 @@ class ComicBookTests(TestCase):
|
||||
self.assertEqual(pages[3].page_file_name, "img4.bmp")
|
||||
self.assertEqual(pages[3].index, 3)
|
||||
|
||||
def test_page_name(self):
|
||||
book = ComicBook.objects.get(file_name="test1.rar")
|
||||
self.assertEqual(book.page_name(0), "img1.jpg")
|
||||
|
||||
def test_comic_list(self):
|
||||
c = Client()
|
||||
@@ -166,18 +143,6 @@ class ComicBookTests(TestCase):
|
||||
response = c.get(f"/comic/{urlsafe_base64_encode(directory.selector.bytes)}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_comic_list_json(self):
|
||||
c = Client()
|
||||
response = c.post("/comic/list_json/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
c.login(username="test", password="test")
|
||||
response = c.post("/comic/list_json/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
directory = Directory.objects.first()
|
||||
response = c.post(f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_recent_comics(self):
|
||||
c = Client()
|
||||
response = c.get("/comic/recent/")
|
||||
@@ -240,7 +205,7 @@ class ComicBookTests(TestCase):
|
||||
response = c.get("/comic/edit/")
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.selector_string}
|
||||
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.url_safe_selector}
|
||||
response = c.post("/comic/edit/", req_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@@ -10,14 +10,15 @@ urlpatterns = [
|
||||
path("settings/users/add/", views.user_add_page, name="add_users"),
|
||||
path("account/", views.account_page, name="account"),
|
||||
path("read/<comic_selector>/", views.read_comic, name="read_comic"),
|
||||
path("read/<comic_selector>/thumb", views.comic_thumbnail, name="comic_thumbnail"),
|
||||
path("set_page/<comic_selector>/<int:page>/", views.set_read_page, name="set_read_page"),
|
||||
path("read/<comic_selector>/<int:page>/img", views.get_image, name="get_image"),
|
||||
path("read/<comic_selector>/pdf", views.get_pdf, name="get_pdf"),
|
||||
path("list_json/", views.comic_list_json, name="comic_list_json1"),
|
||||
path("list_json/<directory_selector>/", views.comic_list_json, name="comic_list_json2"),
|
||||
path("recent/", views.recent_comics, name="recent_comics"),
|
||||
path("recent/json/", views.recent_comics_json, name="recent_comics_json"),
|
||||
path("edit/", views.comic_edit, name="comic_edit"),
|
||||
path("feed/<int:user_selector>/", feeds.RecentComics()),
|
||||
path("<directory_selector>/", views.comic_list, name="comic_list"),
|
||||
path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"),
|
||||
path("action/<operation>/<item_type>/<selector>/", views.perform_action, name="perform_action")
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from os import listdir
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Q, F
|
||||
@@ -78,44 +79,30 @@ def generate_breadcrumbs_from_menu(paths):
|
||||
return output
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirFile:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.icon = ""
|
||||
self.location = ""
|
||||
self.label = ""
|
||||
self.type = ""
|
||||
self.selector = ""
|
||||
obj: Union[Directory, ComicBook]
|
||||
name: str = ''
|
||||
item_type: str = ''
|
||||
percent: int = 0
|
||||
selector: str = ''
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def populate_directory(self, directory, user):
|
||||
self.name = directory.name
|
||||
self.icon = "fa-folder-open"
|
||||
self.selector = urlsafe_base64_encode(directory.selector.bytes)
|
||||
self.location = "/comic/{0}/".format(self.selector)
|
||||
self.label = generate_dir_status(directory.total, directory.total_read)
|
||||
self.type = "directory"
|
||||
|
||||
def populate_comic(self, comic, user):
|
||||
if type(comic) == str:
|
||||
self.icon = "fa-exclamation-circle"
|
||||
self.name = comic
|
||||
self.selector = "0"
|
||||
self.location = "/"
|
||||
self.label = '<center><span class="label label-danger">Error</span></center>'
|
||||
self.type = "book"
|
||||
else:
|
||||
self.icon = "fa-book"
|
||||
self.name = comic.file_name
|
||||
self.selector = urlsafe_base64_encode(comic.selector.bytes)
|
||||
self.location = "/comic/read/{0}/".format(self.selector)
|
||||
self.label = generate_label(comic)
|
||||
self.type = "book"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DirFile: {self.name}: {self.type}>"
|
||||
def __post_init__(self):
|
||||
self.item_type = type(self.obj).__name__
|
||||
if hasattr(self.obj, 'total') and hasattr(self.obj, 'total_read'):
|
||||
# because pages count from zero.
|
||||
total_adjustment = 1
|
||||
if isinstance(self.obj, Directory):
|
||||
total_adjustment = 0
|
||||
try:
|
||||
self.percent = int((self.obj.total_read / (self.obj.total - total_adjustment)) * 100)
|
||||
except ZeroDivisionError:
|
||||
self.percent = 0
|
||||
self.selector = self.obj.url_safe_selector
|
||||
if isinstance(self.obj, Directory):
|
||||
self.name = self.obj.name
|
||||
elif isinstance(self.obj, ComicBook):
|
||||
self.name = self.obj.file_name
|
||||
|
||||
|
||||
def generate_directory(user, directory=False):
|
||||
@@ -127,11 +114,9 @@ def generate_directory(user, directory=False):
|
||||
files = []
|
||||
if directory:
|
||||
dir_path = Path(base_dir, directory.path)
|
||||
# ordered_dir_list = sorted(dir_path.glob('*'))
|
||||
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, directory.path, x).is_dir()]
|
||||
else:
|
||||
dir_path = base_dir
|
||||
# ordered_dir_list = base_dir.glob('*')
|
||||
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, x).is_dir()]
|
||||
file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()]
|
||||
if directory:
|
||||
@@ -154,23 +139,19 @@ def generate_directory(user, directory=False):
|
||||
ComicStatus.objects.bulk_create(new_status)
|
||||
|
||||
file_list_obj = file_list_obj.annotate(
|
||||
total_pages=Count('comicpage', distinct=True),
|
||||
last_read_page=F('comicstatus__last_read_page'),
|
||||
total=Count('comicpage', distinct=True),
|
||||
total_read=F('comicstatus__last_read_page'),
|
||||
finished=F('comicstatus__finished'),
|
||||
unread=F('comicstatus__unread'),
|
||||
user=F('comicstatus__user')
|
||||
).filter(Q(user__isnull=True) | Q(user=user.id))
|
||||
|
||||
for directory_obj in dir_list_obj:
|
||||
df = DirFile()
|
||||
df.populate_directory(directory_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(directory_obj))
|
||||
dir_list.remove(Path(dir_path, directory_obj.name))
|
||||
|
||||
for file_obj in file_list_obj:
|
||||
df = DirFile()
|
||||
df.populate_comic(file_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(file_obj))
|
||||
file_list.remove(Path(dir_path, file_obj.file_name))
|
||||
|
||||
for directory_name in dir_list:
|
||||
@@ -181,18 +162,14 @@ def generate_directory(user, directory=False):
|
||||
directory_obj.save()
|
||||
directory_obj.total = 0
|
||||
directory_obj.total_read = 0
|
||||
df = DirFile()
|
||||
df.populate_directory(directory_obj, user)
|
||||
files.append(df)
|
||||
files.append(DirFile(directory_obj))
|
||||
|
||||
for file_name in file_list:
|
||||
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:
|
||||
book = ComicBook.process_comic_book(file_name.name, directory)
|
||||
df = DirFile()
|
||||
df.populate_comic(book, user)
|
||||
files.append(df)
|
||||
book = ComicBook.process_comic_book(file_name, directory)
|
||||
files.append(DirFile(book))
|
||||
files.sort(key=lambda x: x.name)
|
||||
files.sort(key=lambda x: x.type, reverse=True)
|
||||
files.sort(key=lambda x: x.item_type, reverse=True)
|
||||
return files
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from PIL import Image
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db.models import Max, Count, F
|
||||
from django.db.transaction import atomic
|
||||
from django.http import HttpResponse, FileResponse
|
||||
@@ -31,60 +33,55 @@ from .util import (
|
||||
def comic_list(request, directory_selector=False):
|
||||
if User.objects.all().count() == 0:
|
||||
return redirect("/comic/settings/")
|
||||
# try:
|
||||
# base_dir = Setting.objects.get(name="BASE_DIR").value
|
||||
# except Setting.DoesNotExist:
|
||||
# return redirect("/comic/settings/")
|
||||
# if not path.isdir(base_dir):
|
||||
# return redirect("/comic/settings/")
|
||||
|
||||
directory = None
|
||||
if directory_selector:
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
directory = Directory.objects.get(selector=selector)
|
||||
else:
|
||||
directory = False
|
||||
|
||||
if directory:
|
||||
title = generate_title_from_path(directory.path)
|
||||
breadcrumbs = generate_breadcrumbs_from_path(directory)
|
||||
json_url = "/comic/list_json/{0}/".format(directory_selector)
|
||||
else:
|
||||
title = generate_title_from_path("Home")
|
||||
breadcrumbs = generate_breadcrumbs_from_path()
|
||||
json_url = "/comic/list_json/"
|
||||
|
||||
files = generate_directory(request.user, directory)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"comic/comic_list.html",
|
||||
{"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url},
|
||||
{
|
||||
"breadcrumbs": breadcrumbs,
|
||||
"menu": Menu(request.user, "Browse"),
|
||||
"title": title,
|
||||
"files": files,
|
||||
"selector": directory_selector if directory_selector else 'None'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def comic_list_json(request, directory_selector=False):
|
||||
icon_str = '<span class="fa {0}"></span>'
|
||||
if directory_selector:
|
||||
directory_selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
directory = Directory.objects.get(selector=directory_selector)
|
||||
else:
|
||||
directory = False
|
||||
files = generate_directory(request.user, directory)
|
||||
response_data = dict()
|
||||
response_data["data"] = []
|
||||
for file in files:
|
||||
response_data["data"].append(
|
||||
{
|
||||
"blank": "",
|
||||
"selector": file.selector,
|
||||
"type": file.type,
|
||||
"icon": icon_str.format(file.icon),
|
||||
"name": file.name,
|
||||
"label": file.label,
|
||||
"url": file.location,
|
||||
}
|
||||
)
|
||||
return HttpResponse(json.dumps(response_data), content_type="application/json")
|
||||
def perform_action(request, operation, item_type, selector):
|
||||
if operation not in ['mark_read', 'mark_unread', 'mark_previous']:
|
||||
return HttpResponse(400)
|
||||
elif operation == 'mark_previous' and item_type == 'Directory':
|
||||
return HttpResponse(422)
|
||||
try:
|
||||
selector_uuid = uuid.UUID(bytes=urlsafe_base64_decode(selector))
|
||||
except ValueError:
|
||||
if item_type == 'Directory':
|
||||
for book in ComicBook.objects.filter(directory__isnull=True):
|
||||
getattr(book, operation)(request.user)
|
||||
return HttpResponse(204)
|
||||
if item_type == 'ComicBook':
|
||||
book = get_object_or_404(ComicBook, selector=selector_uuid)
|
||||
getattr(book, operation)(request.user)
|
||||
return HttpResponse(204)
|
||||
elif item_type == 'Directory':
|
||||
directory = get_object_or_404(Directory, selector=selector_uuid)
|
||||
getattr(directory, operation)(request.user)
|
||||
return HttpResponse(204)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -328,6 +325,22 @@ def get_image(_, comic_selector, page):
|
||||
return FileResponse(img, content_type=content)
|
||||
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@login_required
|
||||
def comic_thumbnail(_, comic_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
|
||||
book = ComicBook.objects.get(selector=selector)
|
||||
return redirect(book.get_thumbnail_url())
|
||||
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@login_required
|
||||
def directory_thumbnail(_, directory_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
|
||||
folder = Directory.objects.get(selector=selector)
|
||||
return redirect(folder.get_thumbnail_url())
|
||||
|
||||
|
||||
@login_required
|
||||
def get_pdf(_, comic_selector):
|
||||
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
volumes:
|
||||
- ${COMIC_BOOK_VOLUME}:/data
|
||||
- static_files:/static
|
||||
- media_files:/media
|
||||
command: /bin/bash entrypoint.sh
|
||||
|
||||
cbwebreader-cron:
|
||||
@@ -42,6 +43,7 @@ services:
|
||||
image: nginx
|
||||
volumes:
|
||||
- static_files:/static
|
||||
- media_files:/media
|
||||
- ./cbreader/settings/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- 1337:80
|
||||
|
||||
BIN
placehoder.xcf
Normal file
BIN
placehoder.xcf
Normal file
Binary file not shown.
92
poetry.lock
generated
92
poetry.lock
generated
@@ -96,6 +96,17 @@ sqlparse = ">=0.2.2"
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-appconf"
|
||||
version = "1.0.4"
|
||||
description = "A helper class for handling configuration defaults of packaged apps gracefully."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
django = "*"
|
||||
|
||||
[[package]]
|
||||
name = "django-bootstrap4"
|
||||
version = "2.3.1"
|
||||
@@ -122,6 +133,23 @@ python-versions = ">=3.6"
|
||||
[package.dependencies]
|
||||
Django = ">=2.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-imagekit"
|
||||
version = "4.0.2"
|
||||
description = "Automated image processing for Django models."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
django-appconf = ">=0.5"
|
||||
pilkit = ">=0.2.0"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
async = ["django-celery (>=3.0)"]
|
||||
async_rq = ["django-rq (>=0.6.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-recaptcha2"
|
||||
version = "1.4.1"
|
||||
@@ -251,6 +279,22 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "pilkit"
|
||||
version = "2.0"
|
||||
description = "A collection of utilities and processors for the Python Imaging Libary."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "8.2.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.8.6"
|
||||
@@ -414,7 +458,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "4b1fa38b242472a4006be4f77c969c122881924594ff66f0f74e7a7eded4f362"
|
||||
content-hash = "05469ef9d59ad0de19cda78ddb8511654d50ac61d14eca9ca2ebac3605a3973c"
|
||||
|
||||
[metadata.files]
|
||||
asgiref = [
|
||||
@@ -504,6 +548,10 @@ django = [
|
||||
{file = "Django-3.2-py3-none-any.whl", hash = "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927"},
|
||||
{file = "Django-3.2.tar.gz", hash = "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"},
|
||||
]
|
||||
django-appconf = [
|
||||
{file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"},
|
||||
{file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"},
|
||||
]
|
||||
django-bootstrap4 = [
|
||||
{file = "django-bootstrap4-2.3.1.tar.gz", hash = "sha256:2c199020ac38866cdf8d1c5561ce7468116b9685b455a29843c0225ef8568879"},
|
||||
{file = "django_bootstrap4-2.3.1-py3-none-any.whl", hash = "sha256:b68f073b647b20ec7894a252a0ca4e06b7b8dafdbad995cb0cdc783d0bb4629d"},
|
||||
@@ -512,6 +560,10 @@ django-extensions = [
|
||||
{file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"},
|
||||
{file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"},
|
||||
]
|
||||
django-imagekit = [
|
||||
{file = "django-imagekit-4.0.2.tar.gz", hash = "sha256:6ec0afb77cdf52cd453c9fc2c10ef350d111edfd6ce53c5977aa8a0e22cee00c"},
|
||||
{file = "django_imagekit-4.0.2-py2.py3-none-any.whl", hash = "sha256:304c3379f6a5cac387e47ace11195a603ad3cb01e3e951b45489824d25b00359"},
|
||||
]
|
||||
django-recaptcha2 = [
|
||||
{file = "django-recaptcha2-1.4.1.tar.gz", hash = "sha256:c0b43851b05c6bf6ebb5ecc890c13ccedacd9bb33d64b4291c74dd6fcbc89366"},
|
||||
{file = "django_recaptcha2-1.4.1-py3-none-any.whl", hash = "sha256:9ea90db0cec502741be1066c09ec1b8e02a73162a319a042e78e67c4605087af"},
|
||||
@@ -626,6 +678,44 @@ mysqlclient = [
|
||||
{file = "mysqlclient-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6"},
|
||||
{file = "mysqlclient-2.0.3.tar.gz", hash = "sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432"},
|
||||
]
|
||||
pilkit = [
|
||||
{file = "pilkit-2.0.tar.gz", hash = "sha256:ddb30c2f0198a147e56b151476c3bb9fe045fbfd5b0a0fa2a3148dba62d1559f"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"},
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"},
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"},
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"},
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"},
|
||||
{file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"},
|
||||
{file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"},
|
||||
{file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"},
|
||||
{file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"},
|
||||
{file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"},
|
||||
{file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"},
|
||||
{file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"},
|
||||
{file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"},
|
||||
{file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"},
|
||||
{file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"},
|
||||
{file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"},
|
||||
{file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"},
|
||||
]
|
||||
psycopg2 = [
|
||||
{file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"},
|
||||
{file = "psycopg2-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5"},
|
||||
|
||||
@@ -24,6 +24,8 @@ psycopg2 = "^2.8.6"
|
||||
rarfile = "^4.0"
|
||||
coverage = "^5.5"
|
||||
django-extensions = "^3.1.3"
|
||||
Pillow = "^8.2.0"
|
||||
django-imagekit = "^4.0.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = "^0.812"
|
||||
|
||||
@@ -73,9 +73,18 @@ coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (p
|
||||
dj-database-url==0.5.0 \
|
||||
--hash=sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163 \
|
||||
--hash=sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9
|
||||
django-appconf==1.0.4 \
|
||||
--hash=sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380 \
|
||||
--hash=sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06
|
||||
django-bootstrap4==2.3.1; python_version >= "3.6" and python_version < "4.0" \
|
||||
--hash=sha256:2c199020ac38866cdf8d1c5561ce7468116b9685b455a29843c0225ef8568879 \
|
||||
--hash=sha256:b68f073b647b20ec7894a252a0ca4e06b7b8dafdbad995cb0cdc783d0bb4629d
|
||||
django-extensions==3.1.3; python_version >= "3.6" \
|
||||
--hash=sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0 \
|
||||
--hash=sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3
|
||||
django-imagekit==4.0.2 \
|
||||
--hash=sha256:6ec0afb77cdf52cd453c9fc2c10ef350d111edfd6ce53c5977aa8a0e22cee00c \
|
||||
--hash=sha256:304c3379f6a5cac387e47ace11195a603ad3cb01e3e951b45489824d25b00359
|
||||
django-recaptcha2==1.4.1 \
|
||||
--hash=sha256:c0b43851b05c6bf6ebb5ecc890c13ccedacd9bb33d64b4291c74dd6fcbc89366 \
|
||||
--hash=sha256:9ea90db0cec502741be1066c09ec1b8e02a73162a319a042e78e67c4605087af
|
||||
@@ -156,6 +165,42 @@ mysqlclient==2.0.3; python_version >= "3.5" \
|
||||
--hash=sha256:71c4b330cf2313bbda0307fc858cc9055e64493ba9bf28454d25cf8b3ee8d7f5 \
|
||||
--hash=sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6 \
|
||||
--hash=sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432
|
||||
pilkit==2.0 \
|
||||
--hash=sha256:ddb30c2f0198a147e56b151476c3bb9fe045fbfd5b0a0fa2a3148dba62d1559f
|
||||
pillow==8.2.0; python_version >= "3.6" \
|
||||
--hash=sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9 \
|
||||
--hash=sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b \
|
||||
--hash=sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b \
|
||||
--hash=sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9 \
|
||||
--hash=sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727 \
|
||||
--hash=sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f \
|
||||
--hash=sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d \
|
||||
--hash=sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a \
|
||||
--hash=sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9 \
|
||||
--hash=sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388 \
|
||||
--hash=sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5 \
|
||||
--hash=sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2 \
|
||||
--hash=sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4 \
|
||||
--hash=sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812 \
|
||||
--hash=sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178 \
|
||||
--hash=sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb \
|
||||
--hash=sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232 \
|
||||
--hash=sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797 \
|
||||
--hash=sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5 \
|
||||
--hash=sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484 \
|
||||
--hash=sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602 \
|
||||
--hash=sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2 \
|
||||
--hash=sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef \
|
||||
--hash=sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713 \
|
||||
--hash=sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c \
|
||||
--hash=sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9 \
|
||||
--hash=sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9 \
|
||||
--hash=sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c \
|
||||
--hash=sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b \
|
||||
--hash=sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4 \
|
||||
--hash=sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120 \
|
||||
--hash=sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e \
|
||||
--hash=sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1
|
||||
psycopg2==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
|
||||
--hash=sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725 \
|
||||
--hash=sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5 \
|
||||
|
||||
BIN
static/img/placeholder.png
Normal file
BIN
static/img/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
3563
static/js/isotope.pkgd.js
Normal file
3563
static/js/isotope.pkgd.js
Normal file
File diff suppressed because it is too large
Load Diff
12
static/js/isotope.pkgd.min.js
vendored
Normal file
12
static/js/isotope.pkgd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user