Compare commits
53 Commits
current_re
...
main
Author | SHA1 | Date | |
---|---|---|---|
13effa5372 | |||
49e141ebbd | |||
b6c001b8eb | |||
e521349ad5 | |||
fd3ee119fb | |||
b499fea9cf | |||
572cb0fe16 | |||
5e48463403 | |||
f393f7389f | |||
b435b159e3 | |||
7fc2534744 | |||
fe0aa6e826 | |||
7a0d84cec3 | |||
8f27a86514 | |||
08530a8c85 | |||
25bd8b36e7 | |||
6010b76309 | |||
c18f7735f5 | |||
a5bbaacf8f | |||
d6b3286bc5 | |||
7db20b290e | |||
b986b11850 | |||
544d3fa8a0 | |||
12bb82a976 | |||
c77661e985 | |||
4a2e6f76ca | |||
452bf73b84 | |||
e779a764b4 | |||
36090a402c | |||
944ecb59ea | |||
a67888117f | |||
221aa229d4 | |||
8fe24a8820 | |||
0c21688c00 | |||
fc750a1e14 | |||
76fc4719c2 | |||
f35260bb4a | |||
a99abc0b09 | |||
62473c0f48 | |||
cc3b5f55b8 | |||
200b689f55 | |||
1a72a74184 | |||
9d4e67b089 | |||
52b513bc2a | |||
ff7189af66 | |||
d306b61b2d | |||
ff30e05391 | |||
c35f80582c | |||
baecbc71b1 | |||
ad11579ada | |||
eb016d23bb | |||
e294b56a85 | |||
bec27fdaff |
9
.gitignore
vendored
@ -6,3 +6,12 @@
|
||||
build/
|
||||
dist/
|
||||
pip-wheel-metadata/
|
||||
*.db
|
||||
instance/*
|
||||
migrations/*
|
||||
library/data/*.toc
|
||||
library/data/*.csv
|
||||
library/data/*.seg
|
||||
library/data/MAIN_WRITELOCK
|
||||
library/files/*.pdf
|
||||
settings_development.toml
|
||||
|
21
README.md
@ -1,6 +1,7 @@
|
||||
# varia-library-website
|
||||
# csv-library-website
|
||||
|
||||
> Work In Progress
|
||||
> Work in progress: this is a simple flask application that turns a csv file into a website.
|
||||
> originally used to show the physical books like in the [Varia Library Website](https://library.varia.zone/).. but it can be used for any collection of books, digital or physical as described in the csv file
|
||||
|
||||
## Hacking
|
||||
|
||||
@ -17,3 +18,19 @@ $ cd library && python page.py
|
||||
```
|
||||
|
||||
Or run `make`.
|
||||
## major changes
|
||||
* currently this software is broken.
|
||||
|
||||
## readme driven development
|
||||
* add regular login instead of a secret key ✅
|
||||
* have a settings file for the application ✅
|
||||
* remove varia library specific code ✅
|
||||
* downloadable pdfs ✅
|
||||
* mail for forgotten passwords tested/
|
||||
* flask-oidc for keycloak login
|
||||
* refactor csvparser into library and publication classes
|
||||
* uploadable pdfs
|
||||
* upon boot check for images of the book otherwise extract front page of pdfs
|
||||
* implement [openlibrary](https://openlibrary.org/) ISBN to book in libarary software
|
||||
* [openreads](https://github.com/mateusz-bak/openreads) ability to import this csv file
|
||||
* maybe also GoodReads, BookWyrm
|
||||
|
92
library/app.py
Normal file
@ -0,0 +1,92 @@
|
||||
import csv
|
||||
import os
|
||||
import tomllib
|
||||
|
||||
import flask_apscheduler
|
||||
from flask import Flask
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_login import LoginManager
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from whoosh.fields import *
|
||||
from whoosh.index import create_in
|
||||
from whoosh.qparser import QueryParser
|
||||
|
||||
from application.csvparser import CsvParser
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
bcrypt = Bcrypt()
|
||||
login_manager = LoginManager()
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(__file__)
|
||||
DATA_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "data"))
|
||||
|
||||
|
||||
def create_app():
|
||||
settings = settings_from_file()
|
||||
|
||||
APP = Flask(__name__, static_folder="static")
|
||||
APP.config["SECRET_KEY"] = os.urandom(24)
|
||||
APP.config["UPLOAD_FOLDER"] = "tmpupload"
|
||||
APP.config["IMAGE_FOLDER"] = "static/images"
|
||||
|
||||
APP.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///libraryusers.db"
|
||||
APP.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
|
||||
|
||||
APP.config["LIBRARY_FILENAME"] = settings["libaryfilename"]
|
||||
APP.config["TITLE"] = settings["title"]
|
||||
|
||||
csrf = CSRFProtect(APP)
|
||||
csrf.init_app(APP)
|
||||
login_manager.init_app(APP)
|
||||
db.init_app(APP)
|
||||
migrate.init_app(APP, db, render_as_batch=True)
|
||||
bcrypt.init_app(APP)
|
||||
|
||||
scheduler = flask_apscheduler.APScheduler()
|
||||
scheduler.api_enabled = False
|
||||
scheduler.init_app(APP)
|
||||
scheduler.start()
|
||||
index_books(APP.config["LIBRARY_FILENAME"], APP.config["IMAGE_FOLDER"])
|
||||
|
||||
@scheduler.task("interval", id="update", minutes=10)
|
||||
def update():
|
||||
index_books(APP.config["LIBRARY_FILENAME"], APP.config["IMAGE_FOLDER"])
|
||||
|
||||
@APP.context_processor
|
||||
def inject_title():
|
||||
return dict(title=APP.config["TITLE"])
|
||||
|
||||
return APP
|
||||
|
||||
|
||||
def get_app():
|
||||
APP = Flask(__name__, static_folder="static")
|
||||
return APP
|
||||
|
||||
|
||||
def index_books(filename: str, image_folder: str):
|
||||
csvparser = CsvParser(filename, image_folder)
|
||||
filename = os.path.join(DATA_DIR, csvparser.csv_file)
|
||||
schema = Schema(
|
||||
title=TEXT(stored=True), path=ID(stored=True), content=TEXT
|
||||
)
|
||||
ix = create_in(DATA_DIR, schema)
|
||||
writer = ix.writer()
|
||||
with open(filename, "r", encoding="utf_8_sig") as libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
for row in csv_as_dict:
|
||||
rowcontent = csvparser.concatenate_csv_row(row)
|
||||
writer.add_document(title=row["Id"], path="/a", content=rowcontent)
|
||||
writer.commit()
|
||||
|
||||
|
||||
def settings_from_file():
|
||||
settings = {}
|
||||
if os.path.isfile("settings_development.toml"):
|
||||
with open("settings_development.toml", "rb") as settings_file:
|
||||
return tomllib.load(settings_file)
|
||||
with open("settings.toml", "rb") as settings_file:
|
||||
return tomllib.load(settings_file)
|
218
library/application/csvparser.py
Normal file
@ -0,0 +1,218 @@
|
||||
import csv
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(__file__)
|
||||
DATA_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "../data"))
|
||||
FIELDNAMES = [
|
||||
"Id",
|
||||
"Publication",
|
||||
"Author",
|
||||
"Year",
|
||||
"Fields",
|
||||
"Type",
|
||||
"Publishers",
|
||||
"License",
|
||||
"LicenseShort",
|
||||
"Highlights",
|
||||
"Comments",
|
||||
"Files",
|
||||
]
|
||||
|
||||
|
||||
class CsvParser:
|
||||
def __init__(self, csv_file, image_dir):
|
||||
self.csv_file = csv_file
|
||||
self.image_dir = image_dir
|
||||
|
||||
def _hasimage(self, id):
|
||||
"""does this Id from the csv have an image uploaded"""
|
||||
image_jpg = os.path.join(self.image_dir, "image-{0}.jpg".format(id))
|
||||
if os.path.exists(image_jpg):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _getpublicationfromcsvrow(self, row):
|
||||
"""get entire publication info from a csv row"""
|
||||
year = row["Year"]
|
||||
if not year:
|
||||
year = "Unknown"
|
||||
|
||||
license = row["License"]
|
||||
if not license:
|
||||
license = "No license mentioned"
|
||||
|
||||
pubinfo = {
|
||||
"Title": row["Publication"],
|
||||
"Author": row["Author"],
|
||||
"Year": year,
|
||||
"Fields": row["Fields"],
|
||||
"Type": row["Type"],
|
||||
"Publishers": row["Publishers"],
|
||||
"License": license,
|
||||
"Highlights": row["Highlights"],
|
||||
"Comments": row["Comments"],
|
||||
"Files": row["Files"],
|
||||
"Image": self._hasimage(row["Id"]),
|
||||
}
|
||||
return pubinfo
|
||||
|
||||
def parsecsv(self):
|
||||
"""Test function to inspect csv file as dict"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
return csv_as_dict
|
||||
|
||||
def getpublications(self):
|
||||
"""get an overview of all publications for the main page"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
publications = {}
|
||||
for row in csv_as_dict:
|
||||
year = row["Year"]
|
||||
if not year:
|
||||
year = "Unknown"
|
||||
|
||||
pubinfo = {
|
||||
"Title": row["Publication"],
|
||||
"Author": row["Author"],
|
||||
"Type": row["Type"].lower().title(),
|
||||
"Year": year,
|
||||
"License": row["License"].lower().title(),
|
||||
"Image": self._hasimage(row["Id"]),
|
||||
}
|
||||
publications[row["Id"]] = pubinfo
|
||||
return publications
|
||||
|
||||
def gettypes(self):
|
||||
"""for the dynamic menu get the unique types of publicatons"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listoftypes = []
|
||||
for row in csv_as_dict:
|
||||
lowertype = row["Type"].lower().title()
|
||||
if lowertype not in listoftypes:
|
||||
listoftypes.append(lowertype)
|
||||
return listoftypes
|
||||
|
||||
def getyears(self):
|
||||
"""for the dynamic menu get the unique years for publicatons"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listofyears = []
|
||||
for row in csv_as_dict:
|
||||
uniqueyear = row["Year"]
|
||||
if not uniqueyear:
|
||||
uniqueyear = "Unknown"
|
||||
if uniqueyear not in listofyears:
|
||||
listofyears.append(uniqueyear)
|
||||
listofyears.sort()
|
||||
return listofyears
|
||||
|
||||
def getlicenses(self):
|
||||
"""for the dynamic menu get the unique liscenses for publicatons"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listoflicenses = []
|
||||
for row in csv_as_dict:
|
||||
license = row["License"].lower().title()
|
||||
if not license:
|
||||
license = "No License Mentioned"
|
||||
if license not in listoflicenses:
|
||||
listoflicenses.append(license)
|
||||
return listoflicenses
|
||||
|
||||
def getfieldsofinterest(self):
|
||||
"""for the R&R page get the fields of interest from the publicatons"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
fieldsofinterest = {}
|
||||
for row in csv_as_dict:
|
||||
fields = row["Fields"].split(",")
|
||||
fieldsofinterest[row["Id"]] = fields
|
||||
return fieldsofinterest
|
||||
|
||||
def getfullpublication(self, pubid):
|
||||
"""For the single book view, most complete overview"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
pubinfo = {}
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
for row in csv_as_dict:
|
||||
if pubid == row["Id"]:
|
||||
pubinfo = self._getpublicationfromcsvrow(row)
|
||||
|
||||
# print(pubinfo)
|
||||
return pubinfo
|
||||
|
||||
def generatenewpublicationid(self):
|
||||
"""When uploading a book generate a new unique ID"""
|
||||
libcsv = open(os.path.join(DATA_DIR, self.csv_file), "r")
|
||||
allidsincsv = []
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
for row in csv_as_dict:
|
||||
allidsincsv.append(int(row["Id"]))
|
||||
return str(max(allidsincsv) + 1)
|
||||
|
||||
def writepublication(self, publicationform):
|
||||
"""When uploading a publication writes entry to the csv"""
|
||||
id = generatenewpublicationid()
|
||||
with open(
|
||||
os.path.join(DATA_DIR, "varlib.csv"), "a", newline=""
|
||||
) as csvfile:
|
||||
csv_as_writer = csv.DictWriter(csvfile, FIELDNAMES=FIELDNAMES)
|
||||
csv_as_writer.writerow(
|
||||
{
|
||||
"Id": id,
|
||||
"Publication": publicationform.uploadpublication.data,
|
||||
"Author": publicationform.author.data,
|
||||
"Year": publicationform.year.data,
|
||||
"Fields": publicationform.fields.data,
|
||||
"Type": publicationform.type.data,
|
||||
"Publishers": publicationform.publishers.data,
|
||||
"License": publicationform.license.data,
|
||||
"LicenseShort": publicationform.licenseshort.data,
|
||||
"Highlights": publicationform.highlights.data,
|
||||
"Comments": publicationform.comments.data,
|
||||
}
|
||||
)
|
||||
print("succesfully written book to csv")
|
||||
return id
|
||||
|
||||
def editborrowedby(self, pubid, borrower):
|
||||
"""Edits the borrowed by field for a publication entry in csv"""
|
||||
tempfile = NamedTemporaryFile("w+t", newline="", delete=False)
|
||||
filename = os.path.join(DATA_DIR, "varlib.csv")
|
||||
with open(filename, "r", newline="") as libcsv, tempfile:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
csv_as_writer = csv.DictWriter(tempfile, FIELDNAMES=FIELDNAMES)
|
||||
# use the reader to read where, then writer to write the new row.
|
||||
csv_as_writer.writeheader()
|
||||
for row in csv_as_dict:
|
||||
if pubid == row["Id"]:
|
||||
print("publication changes borrower")
|
||||
print(row["Publication"])
|
||||
row["Currently borrowed by"] = borrower
|
||||
csv_as_writer.writerow(row)
|
||||
|
||||
shutil.move(tempfile.name, filename)
|
||||
|
||||
def concatenate_csv_row(self, row):
|
||||
rowcontent = []
|
||||
rowcontent.append(row["Publication"])
|
||||
rowcontent.append(row["Author"])
|
||||
rowcontent.append(row["Fields"])
|
||||
rowcontent.append(row["Type"])
|
||||
rowcontent.append(row["Publishers"])
|
||||
rowcontent.append(row["Highlights"])
|
||||
rowcontent.append(row["Comments"])
|
||||
return " ".join(rowcontent)
|
@ -1,10 +1,5 @@
|
||||
"""Form object declaration."""
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
SubmitField,
|
||||
)
|
||||
from wtforms import validators
|
||||
from wtforms import StringField, SubmitField, validators
|
||||
from wtforms.validators import Length
|
||||
|
||||
|
||||
@ -20,13 +15,4 @@ class BorrowForm(FlaskForm):
|
||||
),
|
||||
],
|
||||
)
|
||||
secret = StringField(
|
||||
"Librarians secret:",
|
||||
[
|
||||
validators.InputRequired(),
|
||||
Length(
|
||||
min=2, message="Fill in the secret to unlock to library."
|
||||
),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Borrow")
|
13
library/application/forms/forgotpasswordform.py
Normal file
@ -0,0 +1,13 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, validators
|
||||
from wtforms.validators import Email, Length
|
||||
|
||||
|
||||
class ForgotPasswordForm(FlaskForm):
|
||||
"""Forgotten password csv-library form"""
|
||||
|
||||
email = StringField(
|
||||
"Email address:",
|
||||
validators=[validators.InputRequired(), Email(), Length(6, 64)],
|
||||
)
|
||||
submit = SubmitField("Send email")
|
14
library/application/forms/image_uploadform.py
Normal file
@ -0,0 +1,14 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileAllowed, FileField
|
||||
from wtforms import (
|
||||
SubmitField,
|
||||
validators,
|
||||
)
|
||||
|
||||
class ImageUploadForm(FlaskForm):
|
||||
"""Image upload form."""
|
||||
image = FileField(
|
||||
"Image of the book:",
|
||||
validators=[FileAllowed(["jpg", "png", "gif", "webp"], "Images only!")],
|
||||
)
|
||||
submit = SubmitField("Submit")
|
17
library/application/forms/loginform.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Login form to validate user."""
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField, SubmitField, validators
|
||||
from wtforms.validators import Email, Length
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
"""Login csv-library form"""
|
||||
|
||||
email = StringField(
|
||||
"Email address:",
|
||||
validators=[validators.InputRequired(), Email(), Length(6, 64)],
|
||||
)
|
||||
password = PasswordField(
|
||||
"Password:", validators=[validators.InputRequired()]
|
||||
)
|
||||
submit = SubmitField("Sign In")
|
14
library/application/forms/pdf_uploadform.py
Normal file
@ -0,0 +1,14 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileAllowed, FileField
|
||||
from wtforms import (
|
||||
SubmitField,
|
||||
validators,
|
||||
)
|
||||
|
||||
class PdfUploadForm(FlaskForm):
|
||||
"""Pdf upload form."""
|
||||
pdf = FileField(
|
||||
"Pdf of the book:",
|
||||
validators=[FileAllowed(["pdf"], "Only pdf uploads supported")],
|
||||
)
|
||||
submit = SubmitField("Submit")
|
@ -1,17 +1,13 @@
|
||||
"""Form object declaration."""
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import validators
|
||||
from flask_wtf.file import FileAllowed, FileField
|
||||
from wtforms import (
|
||||
StringField,
|
||||
IntegerField,
|
||||
RadioField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
validators,
|
||||
)
|
||||
from wtforms.validators import (
|
||||
Length,
|
||||
NumberRange,
|
||||
)
|
||||
from wtforms.validators import Length, NumberRange
|
||||
|
||||
|
||||
class PublicationForm(FlaskForm):
|
||||
@ -39,9 +35,10 @@ class PublicationForm(FlaskForm):
|
||||
],
|
||||
)
|
||||
year = IntegerField(
|
||||
"Year:", [validators.InputRequired(), NumberRange(min=0, max=2050)]
|
||||
"Year:",
|
||||
[validators.InputRequired(), NumberRange(min=0, max=2050)],
|
||||
default=2023,
|
||||
)
|
||||
custodian = StringField("Custodian:")
|
||||
fields = StringField("Fields:")
|
||||
type = StringField(
|
||||
"Type of publication:",
|
||||
@ -58,34 +55,14 @@ class PublicationForm(FlaskForm):
|
||||
)
|
||||
publishers = StringField("Publishers:")
|
||||
license = StringField("License:")
|
||||
licenseshort = RadioField(
|
||||
"Select the closest license type:",
|
||||
choices=[
|
||||
("Anti-copyright", "Anti-copyright"),
|
||||
("No License Mentioned", "No License Mentioned"),
|
||||
("Free Art License", "Free Art License"),
|
||||
("Copyright", "Copyright"),
|
||||
("Copyleft", "Copyleft"),
|
||||
("Creative Commons", "Creative Commons"),
|
||||
("Public Domain", "Public Domain"),
|
||||
(
|
||||
"GNU Free Documentation License",
|
||||
"GNU Free Documentation License",
|
||||
),
|
||||
],
|
||||
)
|
||||
highlights = StringField("Highlights from the publication:")
|
||||
comments = StringField("Comments on the publication:")
|
||||
borrowed = StringField("Currently borrowed by:")
|
||||
image = FileField(
|
||||
"Image of the book:",
|
||||
validators=[FileAllowed(["jpg", "png", "gif"], "Images only!")],
|
||||
)
|
||||
secret = StringField(
|
||||
"Librarians secret:",
|
||||
[
|
||||
validators.InputRequired(),
|
||||
Length(min=2, message="Fill in the secret to unlock to library."),
|
||||
],
|
||||
pdf = FileField(
|
||||
"Pdf of the book:",
|
||||
validators=[FileAllowed(["pdf"], "Only pdf uploads supported")],
|
||||
)
|
||||
submit = SubmitField("Submit")
|
37
library/application/forms/registerform.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Register form to make a new user."""
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField, SubmitField, validators
|
||||
from wtforms.validators import Email, EqualTo, Length, ValidationError
|
||||
|
||||
|
||||
class RegisterForm(FlaskForm):
|
||||
"""Register for csv-library form"""
|
||||
|
||||
username = StringField(
|
||||
"Username:",
|
||||
validators=[validators.InputRequired(), Length(3, 150)],
|
||||
)
|
||||
|
||||
email = StringField(
|
||||
"Email address:",
|
||||
validators=[
|
||||
validators.InputRequired(),
|
||||
Email(),
|
||||
Length(6, 64),
|
||||
],
|
||||
)
|
||||
|
||||
password = PasswordField(
|
||||
"New password:",
|
||||
validators=[validators.InputRequired(), Length(12, 72)],
|
||||
)
|
||||
|
||||
confirmpassword = PasswordField(
|
||||
"Confirm your password:",
|
||||
validators=[
|
||||
validators.InputRequired(),
|
||||
Length(12, 72),
|
||||
EqualTo("password", message="Passwords must match !"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Register to the library")
|
22
library/application/forms/resetpasswordform.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Reset Password Form form to reset a users PasswordField."""
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, SubmitField, validators
|
||||
from wtforms.validators import EqualTo, Length
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
"""ResetPassword for csv-library form"""
|
||||
|
||||
password = PasswordField(
|
||||
"New password:",
|
||||
validators=[validators.InputRequired(), Length(12, 72)],
|
||||
)
|
||||
confirmpassword = PasswordField(
|
||||
"Confirm your password:",
|
||||
validators=[
|
||||
validators.InputRequired(),
|
||||
Length(12, 72),
|
||||
EqualTo("password", message="Passwords must match !"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Reset your password")
|
19
library/application/models/usermodel.py
Normal file
@ -0,0 +1,19 @@
|
||||
from flask_login import UserMixin
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""User model class for a user in the library"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||
email = db.Column(db.String(150), unique=True, nullable=False)
|
||||
password = db.Column(db.String(300), nullable=False, unique=False)
|
||||
resethash = db.Column(db.String(300), nullable=True, unique=True)
|
||||
resettime = db.Column(db.DateTime)
|
||||
|
||||
def __repr__(self):
|
||||
return "<User %r>" % self.email
|
22
library/application/search.py
Normal file
@ -0,0 +1,22 @@
|
||||
import os
|
||||
|
||||
from whoosh.fields import *
|
||||
from whoosh.index import open_dir
|
||||
from whoosh.qparser import QueryParser
|
||||
|
||||
from application.csvparser import CsvParser
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(__file__)
|
||||
DATA_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "../data"))
|
||||
|
||||
|
||||
def search(searchinput):
|
||||
"""search and get search result titles and return them as book ids"""
|
||||
ix = open_dir(DATA_DIR)
|
||||
with ix.searcher() as searcher:
|
||||
query = QueryParser("content", ix.schema).parse(searchinput)
|
||||
search_results = searcher.search(query)
|
||||
searched_book_ids = []
|
||||
for book in search_results:
|
||||
searched_book_ids.append(book["title"])
|
||||
return searched_book_ids
|
59
library/application/user/forgotpassword.py
Normal file
@ -0,0 +1,59 @@
|
||||
from datetime import datetime
|
||||
from uuid import uuid1
|
||||
|
||||
from flask import render_template
|
||||
from flask_mail import Message
|
||||
from sqlalchemy.exc import (
|
||||
DatabaseError,
|
||||
DataError,
|
||||
InterfaceError,
|
||||
InvalidRequestError,
|
||||
)
|
||||
|
||||
from app import db
|
||||
from application.forms.forgotpasswordform import ForgotPasswordForm
|
||||
from application.models.usermodel import User
|
||||
|
||||
|
||||
def ForgotPassword():
|
||||
forgotpasswordform = ForgotPasswordForm()
|
||||
if forgotpasswordform.validate_on_submit():
|
||||
user = User.query.filter_by(
|
||||
email=forgotpasswordform.email.data
|
||||
).first()
|
||||
if user is not None:
|
||||
resethash = AddResetPasswordHash(user, forgotpasswordform)
|
||||
ResetPassWordMessage(user, resethash, mail)
|
||||
forgotpasswordform.email.errors.append(
|
||||
f"""If {forgotpasswordform.email.data} exists, an email is send with
|
||||
a password reset link. (If your inbox doesn't
|
||||
contain any new mail, please check your spam folder.)"""
|
||||
)
|
||||
return render_template(
|
||||
"user/forgotpassword.html", forgotpasswordform=forgotpasswordform
|
||||
)
|
||||
|
||||
|
||||
def AddResetPasswordHash(user, forgotpasswordform):
|
||||
resethash = uuid1().hex
|
||||
try:
|
||||
user.resettime = datetime.now()
|
||||
user.resethash = resethash
|
||||
db.session.commit()
|
||||
except (InvalidRequestError, DataError, InterfaceError, DatabaseError):
|
||||
forgotpasswordform.email.errors.append("Something went wrong!")
|
||||
db.session.rollback()
|
||||
return resethash
|
||||
|
||||
|
||||
def ResetPassWordMessage(user, resethash, mail):
|
||||
msg = Message(
|
||||
"Forgotten Password ",
|
||||
sender=("mailer", "test@this.com"),
|
||||
recipients=[user.email],
|
||||
)
|
||||
msg.html = f"""{user.username} has requested a password reset for
|
||||
libary website.<br><hr>
|
||||
<a href='http://localhost:5000/resetpassword/{resethash}'>Click here to
|
||||
reset your password.</a>"""
|
||||
mail.send(msg)
|
30
library/application/user/loginuser.py
Normal file
@ -0,0 +1,30 @@
|
||||
from flask import abort, flash, redirect, render_template, request, url_for
|
||||
from flask_bcrypt import check_password_hash
|
||||
from flask_login import login_user
|
||||
|
||||
from application.forms.loginform import LoginForm
|
||||
from application.models.usermodel import User
|
||||
|
||||
|
||||
def LoginUser():
|
||||
loginform = LoginForm()
|
||||
if loginform.validate_on_submit():
|
||||
try:
|
||||
user = User.query.filter_by(email=loginform.email.data).first()
|
||||
if user is None:
|
||||
loginform.password.errors.append("Invalid email or password!")
|
||||
return render_template("login.html", loginform=loginform)
|
||||
if check_password_hash(user.password, loginform.password.data):
|
||||
login_user(user)
|
||||
flash("Logged in successfully.", "success")
|
||||
next = request.args.get("next")
|
||||
if next is not None and not is_safe_url(next): # noqa: F821
|
||||
return abort(400)
|
||||
return redirect(next or url_for("index"))
|
||||
else:
|
||||
flash("Invalid email or password!", "danger")
|
||||
loginform.password.errors.append("Invalid email or password!")
|
||||
return render_template("user/login.html", loginform=loginform)
|
||||
except Exception as e:
|
||||
flash(e, "danger")
|
||||
return render_template("user/login.html", loginform=loginform)
|
66
library/application/user/registeruser.py
Normal file
@ -0,0 +1,66 @@
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask_bcrypt import generate_password_hash
|
||||
from flask_login import login_user
|
||||
from sqlalchemy.exc import (
|
||||
DatabaseError,
|
||||
DataError,
|
||||
IntegrityError,
|
||||
InterfaceError,
|
||||
InvalidRequestError,
|
||||
)
|
||||
from werkzeug.routing import BuildError
|
||||
|
||||
from app import db
|
||||
from application.forms.registerform import RegisterForm
|
||||
from application.models.usermodel import User
|
||||
|
||||
|
||||
def RegisterUser():
|
||||
registerform = RegisterForm()
|
||||
if registerform.validate_on_submit():
|
||||
try:
|
||||
username = registerform.username.data
|
||||
email = registerform.email.data
|
||||
password = registerform.confirmpassword.data
|
||||
|
||||
newuser = User(
|
||||
username=username,
|
||||
email=email,
|
||||
password=generate_password_hash(password),
|
||||
)
|
||||
|
||||
db.session.add(newuser)
|
||||
db.session.commit()
|
||||
flash("Account Succesfully created", "success")
|
||||
login_user(newuser)
|
||||
return redirect(url_for("index"))
|
||||
|
||||
except InvalidRequestError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append("Something went wrong!")
|
||||
flash("Something went wrong!", "danger")
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append("User already exists!")
|
||||
flash("User already exists!", "warning")
|
||||
except DataError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append("Invalid Entry")
|
||||
flash("Invalid Entry", "warning")
|
||||
except InterfaceError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append(
|
||||
"Error connecting to the database"
|
||||
)
|
||||
flash("Error connecting to the database", "danger")
|
||||
except DatabaseError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append(
|
||||
"Error connecting to the database"
|
||||
)
|
||||
flash("Error connecting to the database", "danger")
|
||||
except BuildError:
|
||||
db.session.rollback()
|
||||
registerform.email.errors.append("Unknown error occured!")
|
||||
flash("An error occured !", "danger")
|
||||
return render_template("user/register.html", registerform=registerform)
|
78
library/application/user/resetpassword.py
Normal file
@ -0,0 +1,78 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask_bcrypt import generate_password_hash
|
||||
from flask_login import login_user
|
||||
from sqlalchemy.exc import (
|
||||
DatabaseError,
|
||||
DataError,
|
||||
IntegrityError,
|
||||
InterfaceError,
|
||||
InvalidRequestError,
|
||||
)
|
||||
from werkzeug.routing import BuildError
|
||||
|
||||
from app import db
|
||||
from application.forms.resetpasswordform import ResetPasswordForm
|
||||
from application.models.usermodel import User
|
||||
|
||||
|
||||
def ResetPassword(path):
|
||||
linkvalid = False
|
||||
user = User.query.filter_by(resethash=path).first()
|
||||
if user is None:
|
||||
return redirect(url_for("index"))
|
||||
timepassed = datetime.now() - user.resettime
|
||||
if timepassed.days < 1:
|
||||
linkvalid = True
|
||||
|
||||
resetpasswordform = ResetPasswordForm()
|
||||
if resetpasswordform.validate_on_submit():
|
||||
return ResetUserPasswordInDB(user, resetpasswordform)
|
||||
return render_template(
|
||||
"resetpassword.html",
|
||||
resetpasswordform=resetpasswordform,
|
||||
path=path,
|
||||
linkvalid=linkvalid,
|
||||
)
|
||||
|
||||
|
||||
def ResetUserPasswordInDB(user, resetpasswordform):
|
||||
try:
|
||||
newpassword = resetpasswordform.confirmpassword.data
|
||||
user.password = generate_password_hash(newpassword)
|
||||
user.resethash = None
|
||||
user.resettime = None
|
||||
db.session.commit()
|
||||
flash("Password Succesfully updated", "success")
|
||||
login_user(user)
|
||||
return redirect(url_for("index"))
|
||||
|
||||
except InvalidRequestError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append("Something went wrong!")
|
||||
flash("Something went wrong!", "danger")
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append("User already exists!")
|
||||
flash("User already exists!", "warning")
|
||||
except DataError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append("Invalid Entry")
|
||||
flash("Invalid Entry", "warning")
|
||||
except InterfaceError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append(
|
||||
"Error connecting to the database"
|
||||
)
|
||||
flash("Error connecting to the database", "danger")
|
||||
except DatabaseError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append(
|
||||
"Error connecting to the database"
|
||||
)
|
||||
flash("Error connecting to the database", "danger")
|
||||
except BuildError:
|
||||
db.session.rollback()
|
||||
resetpasswordform.email.errors.append("Unknown error occured!")
|
||||
flash("An error occured !", "danger")
|
@ -1,224 +0,0 @@
|
||||
"""This parses the varlib.csv but only in a way
|
||||
that is actually useful for the site"""
|
||||
from tempfile import NamedTemporaryFile
|
||||
import shutil
|
||||
import csv
|
||||
import os
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
data_dir = os.path.abspath(os.path.join(script_dir, "../data"))
|
||||
image_dir = os.path.abspath(os.path.join(script_dir, "../static/images"))
|
||||
fieldnames = [
|
||||
"Id",
|
||||
"Publication",
|
||||
"Author",
|
||||
"Year",
|
||||
"Custodian",
|
||||
"Fields",
|
||||
"Type",
|
||||
"Publishers",
|
||||
"License",
|
||||
"LicenseShort",
|
||||
"Highlights",
|
||||
"Comments",
|
||||
"Currently borrowed by",
|
||||
]
|
||||
|
||||
|
||||
def parsecsv():
|
||||
"""Test function to inspect csv file as dict"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
return csv_as_dict
|
||||
|
||||
|
||||
def getpublications():
|
||||
"""get an overview of all publications for the main page"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
publications = {}
|
||||
for row in csv_as_dict:
|
||||
year = row["Year"]
|
||||
if not year:
|
||||
year = "Unknown"
|
||||
|
||||
pubinfo = {
|
||||
"Title": row["Publication"],
|
||||
"Author": row["Author"],
|
||||
"Type": row["Type"].lower().title(),
|
||||
"Year": year,
|
||||
"License": row["LicenseShort"].lower().title(),
|
||||
"Image": hasimage(row["Id"]),
|
||||
}
|
||||
publications[row["Id"]] = pubinfo
|
||||
return publications
|
||||
|
||||
|
||||
def hasimage(id):
|
||||
"""does this Id from the csv have an image uploaded"""
|
||||
image_jpg = os.path.join(image_dir, "image-{0}.jpg".format(id))
|
||||
if os.path.exists(image_jpg):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def gettypes():
|
||||
"""for the dynamic menu get the unique types of publicatons"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listoftypes = []
|
||||
for row in csv_as_dict:
|
||||
lowertype = row["Type"].lower().title()
|
||||
if lowertype not in listoftypes:
|
||||
listoftypes.append(lowertype)
|
||||
return listoftypes
|
||||
|
||||
|
||||
def getyears():
|
||||
"""for the dynamic menu get the unique years for publicatons"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listofyears = []
|
||||
for row in csv_as_dict:
|
||||
uniqueyear = row["Year"]
|
||||
if not uniqueyear:
|
||||
uniqueyear = "Unknown"
|
||||
if uniqueyear not in listofyears:
|
||||
listofyears.append(uniqueyear)
|
||||
listofyears.sort()
|
||||
return listofyears
|
||||
|
||||
|
||||
def getlicenses():
|
||||
"""for the dynamic menu get the unique liscenses for publicatons"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
listoflicenses = []
|
||||
for row in csv_as_dict:
|
||||
license = row["LicenseShort"].lower().title()
|
||||
if not license:
|
||||
license = "No License Mentioned"
|
||||
if license not in listoflicenses:
|
||||
listoflicenses.append(license)
|
||||
return listoflicenses
|
||||
|
||||
|
||||
def getfieldsofinterest():
|
||||
"""for the R&R page get the fields of interest from the publicatons"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
fieldsofinterest = {}
|
||||
for row in csv_as_dict:
|
||||
fields = row["Fields"].split(",")
|
||||
fieldsofinterest[row["Id"]] = fields
|
||||
return fieldsofinterest
|
||||
|
||||
|
||||
def getpublicationfromcsvrow(row):
|
||||
"""get entire publication info from a csv row"""
|
||||
year = row["Year"]
|
||||
if not year:
|
||||
year = "Unknown"
|
||||
|
||||
license = row["License"]
|
||||
if not license:
|
||||
license = "No license mentioned"
|
||||
|
||||
borrowed = row["Currently borrowed by"]
|
||||
if not borrowed:
|
||||
borrowed = "No one"
|
||||
|
||||
pubinfo = {
|
||||
"Title": row["Publication"],
|
||||
"Author": row["Author"],
|
||||
"Year": year,
|
||||
"Custodian": row["Custodian"],
|
||||
"Fields": row["Fields"],
|
||||
"Type": row["Type"],
|
||||
"Publishers": row["Publishers"],
|
||||
"License": license,
|
||||
"Highlights": row["Highlights"],
|
||||
"Comments": row["Comments"],
|
||||
"Borrowed": borrowed,
|
||||
"Image": hasimage(row["Id"]),
|
||||
}
|
||||
return pubinfo
|
||||
|
||||
|
||||
def getfullpublication(pubid):
|
||||
"""For the single book view, most complete overview"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
pubinfo = {}
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
for row in csv_as_dict:
|
||||
if pubid == row["Id"]:
|
||||
pubinfo = getpublicationfromcsvrow(row)
|
||||
|
||||
# print(pubinfo)
|
||||
return pubinfo
|
||||
|
||||
|
||||
def generatenewpublicationid():
|
||||
"""When uploading a book generate a new unique ID"""
|
||||
libcsv = open(os.path.join(data_dir, "varlib.csv"), "r")
|
||||
allidsincsv = []
|
||||
with libcsv:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
for row in csv_as_dict:
|
||||
allidsincsv.append(int(row["Id"]))
|
||||
return str(max(allidsincsv) + 1)
|
||||
|
||||
|
||||
def writepublication(uploadform):
|
||||
"""When uploading a publication writes entry to the csv"""
|
||||
id = generatenewpublicationid()
|
||||
with open(
|
||||
os.path.join(data_dir, "varlib.csv"), "a", newline=""
|
||||
) as csvfile:
|
||||
csv_as_writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
csv_as_writer.writerow(
|
||||
{
|
||||
"Id": id,
|
||||
"Publication": uploadform.uploadpublication.data,
|
||||
"Author": uploadform.author.data,
|
||||
"Year": uploadform.year.data,
|
||||
"Custodian": uploadform.custodian.data,
|
||||
"Fields": uploadform.fields.data,
|
||||
"Type": uploadform.type.data,
|
||||
"Publishers": uploadform.publishers.data,
|
||||
"License": uploadform.license.data,
|
||||
"LicenseShort": uploadform.licenseshort.data,
|
||||
"Highlights": uploadform.highlights.data,
|
||||
"Comments": uploadform.comments.data,
|
||||
"Currently borrowed by": uploadform.borrowed.data,
|
||||
}
|
||||
)
|
||||
print("succesfully written book to csv")
|
||||
return id
|
||||
|
||||
|
||||
def editborrowedby(pubid, borrower):
|
||||
"""Edits the borrowed by field for a publication entry in csv"""
|
||||
tempfile = NamedTemporaryFile("w+t", newline="", delete=False)
|
||||
filename = os.path.join(data_dir, "varlib.csv")
|
||||
with open(filename, "r", newline="") as libcsv, tempfile:
|
||||
csv_as_dict = csv.DictReader(libcsv)
|
||||
csv_as_writer = csv.DictWriter(tempfile, fieldnames=fieldnames)
|
||||
# use the reader to read where, then writer to write the new row.
|
||||
csv_as_writer.writeheader()
|
||||
for row in csv_as_dict:
|
||||
if pubid == row["Id"]:
|
||||
print("publication changes borrower")
|
||||
print(row["Publication"])
|
||||
row["Currently borrowed by"] = borrower
|
||||
csv_as_writer.writerow(row)
|
||||
|
||||
shutil.move(tempfile.name, filename)
|
@ -1,66 +0,0 @@
|
||||
Id,Publication,Author,Year,Custodian,Fields,Type,Publishers,License,LicenseShort,Highlights,Comments,Currently borrowed by
|
||||
1,The Economics of Anarchism,Anarcho,2012,Varia,"Economics, Anarchism",Zine,theanarchistlibrary.org,Anti-copyright,Anti-copyright,"The labourer retains, even after he has recieved his wages, a natural right in the thing he has produced",,No one
|
||||
2,Identity Politics - An Anthology,The Anarchist Library,,Varia,Identity politics,Zine,Paper Jam Collective,No license mentioned,No license mentioned,,Varia,No one
|
||||
3,The mythology of work,CrimeThinc.com,,Varia,"Work, Anticapitalism",Zine,CrimeThinc.com,No license mentioned,No license mentioned,,"A selection from 'Work', a 376-page analysis of contemporary capitalism",
|
||||
4,Forget Shorter Showers - Why Personal Change Does Not Equal Political Change,Derrick Jensen,2009,Varia,Environmental justice,Zine,,No license mentioned,No license mentioned,Green consumerism isn't enough.,,
|
||||
5,Choreo-Graphic-Hypothesis,"<meta-author=""Joana Chicau"";>",2018,Varia,"Live Coding, Choreography",Paperback,Self published: Joana Chicau,Free Art License 1.3,Free Art License,"Theatrical actions are not necessary to the performance, Avoid if at all possible",,
|
||||
6,Point of no Return,,2013,Varia,Anarchism in the West,Zine,,No license mentioned,No license mentioned,,Stories about becoming an anarchist in the West in the 80s,
|
||||
7,Waar slapen de kroegtijgers van Oud Charlois,Jacco Weener,,Varia,"Oud Charlois, Drawing",Paperback,Self-published,No license mentioned,No license mentioned,,,
|
||||
8,Dubbeldruk Play with me,"Editorial team: Karin de Jong, Giulia de Giovanelli, Floor van Luijk",2019,Varia,"Copyright, Publishing, Printing",Parazine,Printroom,Free art license,Free art license,,"Confusing licenses mentioned in the about text on the back of the publication, FAL, Copy-left and copy right all mentioned",
|
||||
9,To Boldly Go: a starters guide to hand made and diy films,Film Werkplaats Worm Rotterdam,2008,Varia,"Experimental film, Analog film, DIY, Hand-made",Paperback,Knust (Nijmegen),'No license mentioned,No license mentioned,,,
|
||||
10,Anarchism: Basic concepts and ideas,Peter Storm,2018,Varia,"Anarchism, Dutch Theory",Zine,Ravotr Editions in collaboration with Paper Jam,Feel free to use and copy this text as you see fit. Mentioning source and author would be greatly appreciated.,No license mentioned,,Revised text of a transcribed lecture Storm gave in Rotterdam.,
|
||||
11,Queering Anarchism,"Deric Shannon, Abbey Willis",,Varia,"Anarchism, Queer Theory",Zine,Paper Jam Collective,No license mentioned,No license mentioned,,,
|
||||
12,Abolish restaurants,Prole.info,2010,Varia,"Labour, Food industry",Paperback,Pm Press,Copyright 2010,Copyright,Drawing on a range of anti-capitalist ideas as well as a heaping plate of personal experience,,CCL
|
||||
13,"Elk Woord Een Vonk: Verboden Teksten, verwerpelijke vervolging: de zaak Joke Kaviaar",Steungroep 13 September,2014,Varia (or Luke?),"Joke Kaviaar, Immigration, Activism, Forbidden Texts, Incarceration",Softcover,Self-published,Copyleft,Copyleft,,https://13-september.nl,
|
||||
14,A NO BORDERS manifesto,Ill Will Editions,2015,Varia,Migrant justice,Zine,Self-published,No license mentioned,No license mentioned,,,
|
||||
16,Futur Musique,De fanfare voor vooruit strevende volksmuziek,2018,Varia,"Musical Instruments, DIY",Zine,Self-published,No license mentioned,No license mentioned,Copied instructions on building one's own musical instruments,,
|
||||
17,Franchir le cap,Quentin Juhel,2019,Varia,Convivial computation,Zine,Self-publsihed,Creative commons CC-BY-SA,Creative commons,,,
|
||||
18,Consensus: Decision Making,Seeds For Change UK,2010,Varia,"Decision making, Consensus",Zine,Self-published,No license mentioned,No license mentioned,Short guide ,,
|
||||
19,"Waar ook op gestemd wordt, wij laten ons niet regeren",Anarchistische Groep Nijmegen,2014,Varia,Democracy,Zine,Self-published,No license mentioned,No license mentioned,,,
|
||||
20,The NGO sector: the trojan horse of capitalism,Crn Blok,,Varia,"Anti-capitalism, NGO",Zine,Self-published,Copyleft,Copyleft,,,
|
||||
21,Messing Around With Packet Radio,"Roel Roscam Abbing, Dennis De Bel",,Varia,"Packet radio, DIY, radio, wireless",Zine,Self-published,Public domain,Public domain,,,
|
||||
22,Organising Socially: affinity groups & decision making,Seeds for change,,Varia,"Consensus, Decision making, Affinity groups",Zine,Paper Jam Collective,No license mentioned,No license mentioned,,,
|
||||
23,The Moral of the Xerox,"Florian Cramer, Clara Lobregat Balaguer",2017,Varia,"Piracy, Cultural Appropriation",Zine,Self-published,No license mentioned,No license mentioned,"Printed in diocese of Cologne, Germany on the joyous occasion of the Pluriversale VII: Stealing from the west for the critical parishioners of Akademie der Kunste der Welt",,
|
||||
24,Non-Western Anarchisms,Jason Adams,,Varia,Non-Western Anarchisms,Zine,Zaba Books,No license mentioned,No license mentioned,"The purpose of this paper is to help anarchist/anti-authoritarian movements active today to reconceptualise the history and theory of first-wave anarchism on the global level, and to reconsider its relevance to the continuing anarchist project.",,
|
||||
33,The immaterial labor union #7: immersive advertisement,Lídia Pereira and Δεριζαματζορ Προμπλεμ ιναυστραλια,,Varia,"labour, Advertisement, immersion, social media",Zine,Self-published,Zine is published under Gnu free documentation license 1.3 unless otherwise specified ,GNU Free Documentation License,,,
|
||||
34,The immateriality labor union #10: immateriality,Lídia Pereira and Δεριζαματζορ Προμπλεμ ιναυστραλια,2017,Varia,"Labour, Immateriality",Zine,Self-published,GNU Free Documentation License,GNU Free Documentation License,,Varia,No one
|
||||
35,The immaterial labor union. Special Issue #1: Homebrew Server Club,Homebrew Server Club,2017,Varia,"Self-Hosting, Servers, DIY",Zine,Self-published,CC-BY-SA,Creative commons,,,
|
||||
36,Pervasive labour union. Special issue #2: The Entreprecariat,Silvio Lorusso,2017,Varia,"Entreprecariat, Labour, Precarity",Zine,Self-published,No license mentioned,No license mentioned,,Between April and May 2017 the Zine's name changed from Immaterial Labor Union to Pervasive Labour Union,
|
||||
37,'Pervasive labour union #13: Fed Up,Lídia Pereira,2019,Varia,"Labour, DIY, federation",Zine,Self-published,"GNU Free Documentation License 1.3, CC-0, Copyright (C) 2019, Julia Janssen, Peer Production License",GNU Free Documentation License,,,
|
||||
38,Each Page a Function,Raphaël Bastide,2019,Varia,"Automation, Drawing, Web to Print",Paperback,LeMegot editions,No license mentioned,No license mentioned,,,
|
||||
39,In Beweging: Magazine van de Anarchistische Groep Nijmegen,Anarchistische Groep Nijmegen ,2018,Varia,"Anarchism, 1st of May, Nijmegen",Zine,Self-published,No license mentioned,No license mentioned,,Anarchistische Bieb de Zwarte Uil. library in Nijmegen open on saturday from 12:00 till 17:30,
|
||||
40,Deep pockets #2 Shadowbook: Writing through the digital 2014-2018,Miriam Rasch,2018,Varia,"Language, digital, communication",Paperback,Institute of Network Cultures,CC BY-NC-SA 4.0,Creative Commons,,,
|
||||
41,The Techno Galactic Software Observatory Reader,Constant,2018,Varia,"Software Studies, Software Curious People, Observation",Paperback,Self- published,No license mentioned,No license mentioned,,Reader for Techno Galactic Software Observatory work session,
|
||||
42,Upsetting Settings,Piet Zwart Institute,2019,Varia,"Catalogue, Experimental publishing",Softcover,XPUB,Copyleft,Copyleft,,Dissertations from the Piet Zwart Institute 2017-2019,
|
||||
43,I Have Witnessed First Time Experiences ,Connie Butler,2016,Varia,Fine Art,Softcover,Piet Zwart institute MFA,Copyright 2016,Copyright,,,
|
||||
44,The Age of Aquariums: old horoscopes for the new year,Brenda Bosma,2013,Varia,Horoscope,Paperback,Subbacultcha!,No license mentioned,No license mentioned,,,
|
||||
45,The Internet Measurement Handbook,,2018,Varia,"Internet, blackout, cyber-security",Paperback,Netblocks,CC,Creative Commons,,,
|
||||
46,Issue #4 2019,Design Department Sandberg,2019,Varia ,Design theory,Paperback,Drukkerij RaddraaierSSP,No license mentioned,No license mentioned,,Dissertations from the Design Museum,
|
||||
47,O Bike and Friends,Dennis De Bel,,Varia,"Bike sharing, DIY",Zine,Self-published,No license mentioned,No license mentioned,,,
|
||||
48,Algoliterary Encounters,Algolit,2017,Varia,"Natural text processing, machine learning, algorithms",Catalogue,Algolit,Free Art License,Free Art License,,,
|
||||
49,Der Fall von Afrin,Unknown,,Varia,Turkish politics,Zine,Unknown,No license mentioned,No license mentioned,,,
|
||||
50,Andy de Fiets: Letter to Robin Kinross ,"Paul Haworth, Sam de Groot",2010,Varia,Typography,Paperback,TRUE TRUE TRUE Amsterdam,"Copyright 2009,2010",Copyright,,,
|
||||
51,Ora Elastică,ODD,2018,Varia,"Art, Indonesia, Romania, non-western art theory",Softcover,frACTalia,No license mentioned,No license mentioned,,,
|
||||
52,4 x Through The Labyrinth,"O. Nicolai & J. Wenzel, translated by Sadie Plant",2012,Joana Chicau,Labyrinths,Softcover,Rollo Press,Copyright,Copyright,,,
|
||||
53,Towards an Immersive Intelligence,Joseph Nechvatal,2009,Joana Chicau?,"Virtual reality, Computer Art",Paperback,Edgewise,Copyright,Copyright,,,
|
||||
54,Xavan et Jaluka - The Death of the Authors 1946,Peter Westenberg,2018,Varia,Public domain,Zine,Constant Verlag,Copyleft,Copyleft,,,
|
||||
55,Spreekt U Sint-Gillis? Parlez-vous Saint-Gillois?,,,Varia,"Bruxelles, Language, Sint-Gillis",Softcover,Constant Verlag,Copyleft,Copyleft,,,
|
||||
56,Mondothèque: a radiated book,"André Castro , Sînziana Păltineanu , Dennis Pohl , Dick Reckard , Natacha Roussel , Femke Snelting , Alexia de Visscher",2016,Varia,"Archive, Otlet, Library science, Mondaneum, Google, Mons",Softcover,Constant,Copyleft,Copyleft,,,
|
||||
57,"The Death of the Authors: 'James Joyce, Rabindranath Tagore & Their Return to Life in Four Seasons",A Constant Remix,2013,Varia,"James Joyce, Ulysses",Softcover,Constant,Public domain,Public domain,,'generated from Public domain sources,
|
||||
58,Verbindingen/Jonctions 10 : Tracks in electr(on)ic fields,Constant,2009,Varia,"Art, activism, technological culture",Softcover,Constant,Copyleft,Copyleft,,,
|
||||
59,Conversations,Constant,2014,Varia,"Software studies, Libre graphics",Softcover,Constant Verlag,Copyleft,Copyleft,,,
|
||||
60,Networks of one's own #1: Etherbox,"Michael Murtaugh, An Mertens, Roel Roscam Abbing, Femke Snelting",2018,Varia,"Networks, Digital Infrastructures, DIY, DIWO, Executable publication, Experimental Publishing",Paperback,Constant Verlag,Copyleft,Copyleft,,,
|
||||
61,Mots de la cage aux ours - woorden uit de berenkuil,Constant,2012,Varia,"words, language, Bruxelles",Softcover,Constant,Copyleft,Copyleft,,,
|
||||
62,Snake rituals and switching circuits,Florian Cramer,2009,Danny,"mass communication, personal communication, new media",paperback,Piet Zwart Institute,Creative Commons Attribution-Share Alike 3.0,Creative Commons,The function of a medium is ultimately decided by its users and not by its creators,,
|
||||
63,Magium issue 1: On Eating in isolation,Alice Strete,2020,Varia,"food, sharing, personal stories, consumption",zine,Self Published,Free Art License,Free Art License,,,No one
|
||||
64,Networks of One's Own 2: three takes on taking care,"Varia, Constant and Colm O’Neill",2019,Varia,"Software, internet, taking care, homebrew",paperback,Varia,Copyleft,Copyleft,Networks Of One’s Own is a periodic para-nodal publication that is itself collectively within a network.,,No one
|
||||
65,My Hard-Drive Died Along With My Heart ,Thomas Walsklaar,2016,Varia,"Hard-drives, Data, Loss, Trust, Technology, collection, materiality, obsolescence, preservation, progress, writing ",paperback,Self Published, No License Mentioned,No License Mentioned,,"We always seem to be looking for a new technical solution for knowledge and information storage. We hope there is one magical, final solution, one that will solve every issu But easy solutions create their own problems. The perceived view of the stable nature of digital information differs from reality. There are many points of failure, like old physical formats, lost or non functional machines, companies that go bankrupt, file formats with no support in the future, or changing user licenses. It seems that the more technical the technology gets, the more problems it creates.",No one
|
||||
67,"Low power to the people: Pirates, protest. and politics in FM radio activism",Christina Dunbar-Hester,2014,Danny,"Radio, Activism, FM, Wireless, Legal, Policies, Piracy, Politics",hardcover,MIT press,Copyright,Copyright,The internet didn't drop down from the sky--it was created by the military and we need to take it back,,No one
|
||||
68,Pretty fly for a wi-fi,Roel Roscam Abbing,2014,Varia,"Wireless, Wifi, internet, DIY, antennas ",paperback,Self Published, GNU Free Documentation License 1.3,GNU Free Documentation License,,appreciate the DIY etched beauty of Object #4,Danny
|
||||
69,TX Pirate radio dispatches from eighties London ,Stephen Hebditch,2017,Danny,"Radio, Activism, FM, AM, Wireless, Piracy, Politics, Zine culture",paperback,Tx Publications,Copyright,Copyright,,,Danny
|
||||
70,The impossibility of interface,Matthew Fuller,2006,Varia,"Interface, Software, Software culture, Media design, ",paperback,HRO,No License Mentioned,No License Mentioned,,"Such contextualisation, in turn, suggests ways of developing digital media with an awareness of its social ramifications.",
|
||||
71,Let's use free speech to praise pirate radio ,Andrew Bushard,2014,Danny,"Radio, Poetry, Piracy",paperback, Free Press Media Press Inc,No License Mentioned,No License Mentioned,At the risk of sounding melodramatic I want to consider pirate radio as innately noble,"25 Poems, 26 Pages Pirate radio represents magical rebellion. Pirate radio signifies creative protest. Pirate radio can change the world.",
|
||||
72,Cursusboek voor het N-examen,VERON vereniging experimenteel onderzoek nederland,2012,Varia,"Radio, Broadcasting, Exams, equipment, Radio amateurs",Paperback,Stichting servicebureau VERON,Copyright,Copyright,,,No one
|
||||
73,Freax the brief history of the computer demoscene,Tamás Polgár,2008,Danny,"Demoscene, Coding, Software art, Amiga, Home Computers, Music",paperback,CSW Verlag digitalkultur,Copyright,Copyright,,Very thorough first hand account of the demoscene until the end of the amiga era. The book hints towards freax volume 2 but this book was never made.,Luke
|
||||
74,Tasks of the Contingent Librarian,Simon Browne,2020,Varia,Contingent Librarianship,Box of cards,de Appel,Copyleft,Copyleft,,,
|
||||
75,Handmade Electronic Music,Nicolas Collins,2009,Danny,"Music, DIY, Hacking, Hardware, How-To",Paperback,Routledge,Copyright,Copyright,,,No one
|
|
23
library/deploydb.py
Normal file
@ -0,0 +1,23 @@
|
||||
from flask_migrate import init, migrate, stamp, upgrade
|
||||
|
||||
from app import create_app, db
|
||||
|
||||
|
||||
def deploy():
|
||||
"""Run deployment of database."""
|
||||
|
||||
# This model is required for flask_migrate to make the table
|
||||
from application.models.usermodel import User # noqa: F401
|
||||
|
||||
app = create_app()
|
||||
app.app_context().push()
|
||||
db.create_all()
|
||||
|
||||
# migrate database to latest revision
|
||||
init()
|
||||
stamp()
|
||||
migrate()
|
||||
upgrade()
|
||||
|
||||
|
||||
deploy()
|
0
library/files/files_here
Normal file
1
library/migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
50
library/migrations/alembic.ini
Normal file
@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
113
library/migrations/env.py
Normal file
@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from flask import current_app
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger("alembic.env")
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions["migrate"].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions["migrate"].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return (
|
||||
get_engine()
|
||||
.url.render_as_string(hide_password=False)
|
||||
.replace("%", "%%")
|
||||
)
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace("%", "%%")
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option("sqlalchemy.url", get_engine_url())
|
||||
target_db = current_app.extensions["migrate"].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, "metadatas"):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, "autogenerate", False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info("No changes in schema detected.")
|
||||
|
||||
conf_args = current_app.extensions["migrate"].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=get_metadata(), **conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
library/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
27
library/migrations/versions/3105f85a9d8e_.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 3105f85a9d8e
|
||||
Revises:
|
||||
Create Date: 2024-03-30 17:50:00.878071
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "3105f85a9d8e"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
195
library/page.py
@ -1,48 +1,65 @@
|
||||
"""This is the main flask library page"""
|
||||
|
||||
|
||||
import os
|
||||
import flask
|
||||
from requests import get
|
||||
from icalendar import Calendar
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
import bcrypt
|
||||
from flask import (
|
||||
render_template,
|
||||
Blueprint,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from flask_wtf.csrf import CSRFError, CSRFProtect
|
||||
from icalendar import Calendar
|
||||
from PIL import Image
|
||||
from rnrfeed.rnrfeeder import getevents, getlatestevent
|
||||
from uploadform import PublicationForm
|
||||
from borrowform import BorrowForm
|
||||
from csvparser.csvparser import (
|
||||
getlicenses,
|
||||
getpublications,
|
||||
gettypes,
|
||||
getyears,
|
||||
getfullpublication,
|
||||
writepublication,
|
||||
editborrowedby,
|
||||
from requests import get
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app import create_app, login_manager
|
||||
from application.search import search
|
||||
from application.csvparser import CsvParser
|
||||
from application.forms.borrowform import BorrowForm
|
||||
from application.forms.image_uploadform import ImageUploadForm
|
||||
from application.forms.pdf_uploadform import PdfUploadForm
|
||||
from application.forms.publicationform import PublicationForm
|
||||
from application.models.usermodel import User
|
||||
from application.user.forgotpassword import ForgotPassword
|
||||
from application.user.loginuser import LoginUser
|
||||
from application.user.registeruser import RegisterUser
|
||||
from application.user.resetpassword import ResetPassword
|
||||
|
||||
|
||||
APP = create_app()
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(APP)
|
||||
files = Blueprint("files", __name__, static_folder="files")
|
||||
APP.register_blueprint(files)
|
||||
csvparser = CsvParser(
|
||||
APP.config["LIBRARY_FILENAME"], APP.config["IMAGE_FOLDER"]
|
||||
)
|
||||
|
||||
|
||||
csrf = CSRFProtect()
|
||||
APP = flask.Flask(__name__, static_folder="static")
|
||||
APP.config["SECRET_KEY"] = "ty4425hk54a21eee5719b9s9df7sdfklx"
|
||||
APP.config["UPLOAD_FOLDER"] = "tmpupload"
|
||||
csrf.init_app(APP)
|
||||
@APP.before_request
|
||||
def session_handler():
|
||||
session.permanent = True
|
||||
APP.permanent_session_lifetime = timedelta(minutes=30)
|
||||
|
||||
|
||||
@APP.route("/")
|
||||
def index():
|
||||
"""Main route, shows all the books and you can filter them, a bit"""
|
||||
pubtypes = gettypes()
|
||||
pubyears = getyears()
|
||||
publicenses = getlicenses()
|
||||
publicatons = getpublications()
|
||||
"""Main route, shows all the books and you can filter them
|
||||
based on year, type"""
|
||||
pubtypes = csvparser.gettypes()
|
||||
pubyears = csvparser.getyears()
|
||||
publicenses = csvparser.getlicenses()
|
||||
publicatons = csvparser.getpublications()
|
||||
template = render_template(
|
||||
"index.html",
|
||||
publications=publicatons,
|
||||
@ -54,87 +71,57 @@ def index():
|
||||
|
||||
|
||||
@APP.route("/upload", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def upload():
|
||||
"""Upload route, a page to upload a book to the csv"""
|
||||
uploadform = PublicationForm()
|
||||
publicationform = PublicationForm()
|
||||
if request.method == "POST":
|
||||
if uploadform.validate_on_submit() and checksecret(
|
||||
uploadform.secret.data
|
||||
):
|
||||
id = writepublication(uploadform)
|
||||
saveimage(uploadform.image.data, id)
|
||||
if publicationform.validate_on_submit():
|
||||
id = csvparser.writepublication(publicationform)
|
||||
saveimage(publicationform.image.data, id)
|
||||
return redirect(str(id), code=303)
|
||||
else:
|
||||
return render_template("upload.html", uploadform=uploadform)
|
||||
print("test")
|
||||
return render_template("upload.html", uploadform=uploadform)
|
||||
return render_template("upload.html", publicationform=publicationform)
|
||||
return render_template("upload.html", publicationform=publicationform)
|
||||
|
||||
|
||||
@APP.route("/<publicationID>", methods=["GET", "POST"])
|
||||
def show_book(publicationID):
|
||||
"""route for a single publication, shows full info and allows borrowing"""
|
||||
fullpublication = getfullpublication(publicationID)
|
||||
fullpublication = csvparser.getfullpublication(publicationID)
|
||||
borrowform = BorrowForm()
|
||||
image_uploadform = ImageUploadForm()
|
||||
pdf_uploadform = PdfUploadForm()
|
||||
if request.method == "POST":
|
||||
if borrowform.validate_on_submit() and checksecret(
|
||||
borrowform.secret.data
|
||||
):
|
||||
if borrowform.validate_on_submit():
|
||||
editborrowedby(publicationID, borrowform.borrowed.data)
|
||||
fullpublication["Borrowed"] = borrowform.borrowed.data
|
||||
return render_template(
|
||||
"publication.html",
|
||||
fullpublication=fullpublication,
|
||||
publicationID=publicationID,
|
||||
borrowform=borrowform,
|
||||
)
|
||||
if image_uploadform.validate_on_submit():
|
||||
saveimage(image_uploadform.image.data, fullpublication.id)
|
||||
|
||||
# return a full publication with or without form errors
|
||||
return render_template(
|
||||
"publication.html",
|
||||
fullpublication=fullpublication,
|
||||
publicationID=publicationID,
|
||||
borrowform=borrowform,
|
||||
image_uploadform=image_uploadform,
|
||||
pdf_uploadform=pdf_uploadform,
|
||||
)
|
||||
|
||||
|
||||
@APP.route("/pastevents")
|
||||
def pastevents():
|
||||
"""show past R&R events and book recommendations"""
|
||||
events = getevents()
|
||||
return render_template("pastevents.html", events=events)
|
||||
|
||||
|
||||
@APP.route("/upcoming")
|
||||
def latestevent():
|
||||
"""show upcoming or latest R&R events and book recommendations"""
|
||||
event = getlatestevent()
|
||||
return render_template("upcomingevent.html", event=event)
|
||||
|
||||
|
||||
@APP.context_processor
|
||||
def upcoming_or_latest():
|
||||
"""determines wether the newest R&R event is upcoming or not"""
|
||||
upcoming = True
|
||||
ics = get("https://varia.zone/events.ics").text
|
||||
gcal = Calendar.from_ical(ics)
|
||||
eventtimes = [
|
||||
c.get("dtstart").dt
|
||||
for c in gcal.walk()
|
||||
if c.name == "VEVENT" and "Read & Repair" in c.get("summary")
|
||||
]
|
||||
now = datetime.datetime.now()
|
||||
eventtimes.sort()
|
||||
eventtimes.reverse()
|
||||
if now > eventtimes[0]:
|
||||
upcoming = False
|
||||
|
||||
return dict(upcoming=upcoming)
|
||||
@APP.route("/search/<search_query>", methods=["GET"])
|
||||
def searchbooks(search_query):
|
||||
print(f"Searched for {search_query}")
|
||||
search_results = search(search_query)
|
||||
return json.dumps(search_results)
|
||||
|
||||
|
||||
def saveimage(image, id):
|
||||
"""helper function that can save images"""
|
||||
image.save(os.path.join(APP.config["UPLOAD_FOLDER"], image.filename))
|
||||
orig_image = Image.open(
|
||||
os.path.join(APP.config["UPLOAD_FOLDER"], image.filename)
|
||||
os.path.join(APP.confmailig["UPLOAD_FOLDER"], image.filename)
|
||||
)
|
||||
new_width = 640
|
||||
new_height = int(new_width * orig_image.height / orig_image.width)
|
||||
@ -144,14 +131,46 @@ def saveimage(image, id):
|
||||
os.remove(os.path.join(APP.config["UPLOAD_FOLDER"], image.filename))
|
||||
|
||||
|
||||
def checksecret(secret):
|
||||
"""small simple check to a secret, library group members can upload"""
|
||||
with open("secret") as f:
|
||||
secrethash = f.readline().rstrip()
|
||||
if bcrypt.checkpw(secret.encode("utf-8"), secrethash.encode("utf-8")):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@APP.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@APP.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
return LoginUser()
|
||||
|
||||
|
||||
@APP.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
return RegisterUser()
|
||||
|
||||
|
||||
@APP.route("/forgotpassword", methods=["GET", "POST"])
|
||||
def forgotpassword():
|
||||
return ForgotPassword()
|
||||
|
||||
|
||||
@APP.route("/resetpassword/<path>", methods=["GET", "POST"])
|
||||
def resetpassword(path):
|
||||
return ResetPassword(path)
|
||||
|
||||
|
||||
@APP.errorhandler(CSRFError)
|
||||
def handle_csrf_error(e):
|
||||
return render_template("csrf_error.html", reason=e.description), 400
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
@APP.errorhandler(CSRFError)
|
||||
def handle_csrf_error(e):
|
||||
return render_template("csrf_error.html", reason=e.description), 400
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1,51 +0,0 @@
|
||||
from feedparser import parse
|
||||
from csvparser.csvparser import getfieldsofinterest, getfullpublication
|
||||
|
||||
feed = parse("http://varia.zone/en/feeds/all-en.rss.xml")
|
||||
|
||||
|
||||
def getentries():
|
||||
entries = {}
|
||||
for entry in feed.entries:
|
||||
if "readrepair" in entry.category:
|
||||
entries[entry.title] = []
|
||||
entrylist = entries[entry.title]
|
||||
entrylist.append(entry.description)
|
||||
entrylist.append(rabbithole(entry))
|
||||
return entries
|
||||
|
||||
|
||||
def getlatestevent():
|
||||
for entry in feed.entries:
|
||||
if "readrepair" in entry.category:
|
||||
entry[entry.title] = []
|
||||
entrylist = entry[entry.title]
|
||||
entrylist.append(entry.description)
|
||||
return entry
|
||||
|
||||
|
||||
def gettitles():
|
||||
titles = []
|
||||
for entry in feed.entries:
|
||||
if "readrepair" in entry.category:
|
||||
titles.append(entry.title)
|
||||
return titles
|
||||
|
||||
|
||||
def rabbithole(entry):
|
||||
pubtitles = {}
|
||||
fieldsofinterest = getfieldsofinterest()
|
||||
categories = [t.get('term').lower() for t in entry.tags]
|
||||
for id, fields in fieldsofinterest.items():
|
||||
if [f for f in fields if(f.strip().lower() in categories)]:
|
||||
# print("book found")
|
||||
publicationinfo = getfullpublication(id)
|
||||
fulltitle = "{0} - {1}".format(
|
||||
publicationinfo["Author"], publicationinfo["Title"])
|
||||
pubtitles[id] = fulltitle
|
||||
return pubtitles
|
||||
|
||||
|
||||
def getevents():
|
||||
events = getentries()
|
||||
return events
|
@ -1 +0,0 @@
|
||||
$2b$12$kZC/e1smAiBCntQxLUpsZ.H0Y5VkWG/YLt18wIdGmONtijkXYaVsO
|
3
library/settings.toml
Normal file
@ -0,0 +1,3 @@
|
||||
title = "Your library title"
|
||||
libaryfilename = "newlib.csv"
|
||||
readmethod = "download"
|
@ -1,23 +0,0 @@
|
||||
#pastevents{
|
||||
position: fixed;
|
||||
top: -1em;
|
||||
right: 3em;
|
||||
height: 14em;
|
||||
}
|
||||
|
||||
#upcomingevents{
|
||||
position: fixed;
|
||||
top: -2.5em;
|
||||
right: 6em;
|
||||
height: 14em;
|
||||
}
|
||||
#pastevents:hover{
|
||||
z-index: 1;
|
||||
}
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
|
||||
#upcomingevents,#pastevents {
|
||||
position:absolute;
|
||||
width: 15%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
@ -1,26 +1,21 @@
|
||||
/* Dropdown Button */
|
||||
/* for sorting on Year, Type, License
|
||||
*/
|
||||
.menu {
|
||||
display:flex;
|
||||
gap: 0.3em;
|
||||
position: relative;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.filter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activebtn {
|
||||
background-color: #62b264;
|
||||
}
|
||||
|
||||
#leftmostbtn{
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Dropdown Content (Hidden by Default) */
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
@ -41,7 +36,7 @@
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
.dropbtn {
|
||||
.dropbtn .dropdown {
|
||||
margin-top: 1em;
|
||||
}
|
||||
/* Change color of dropdown links on hover */
|
||||
@ -54,6 +49,30 @@
|
||||
/* Change the background color of the dropdown button when the dropdown content is shown */
|
||||
.dropdown:hover .dropbtn {background-color: #3e8e41;}
|
||||
|
||||
#booksearch{
|
||||
background: #f1f1f1;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border: 3px solid black;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
box-shadow: 0.3em 0.35em rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 3px solid black;
|
||||
padding: 0.8em;
|
||||
color: black;
|
||||
min-width: auto;
|
||||
background-color: #f1f1f1;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
box-shadow: 0.3em 0.35em rgba(0,0,0,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
|
||||
.dropdown-content button {
|
||||
font-size: 0.7em;
|
||||
|
9
library/static/css/feather.css
Normal file
@ -0,0 +1,9 @@
|
||||
.feather {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
@ -1,56 +1,34 @@
|
||||
@font-face {
|
||||
font-family: "libreBaskerville";
|
||||
src: url(../fonts/LibreBaskerville-Regular.otf);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
font-family: Garamond, serif;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
font-family: "libreBaskerville";
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body:after {
|
||||
font-size: .8em;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#cloud {
|
||||
overflow: hidden;
|
||||
width: 1px; height: 1px;
|
||||
transform: translate(-100%, -100%);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: alphaClouds;
|
||||
src: url(../fonts/AlphaClouds.ttf);
|
||||
}
|
||||
|
||||
#varia {
|
||||
line-height: 1.03em;
|
||||
#library {
|
||||
position: relative;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 2px 2px #8B5B7F;
|
||||
font-size: 52px;
|
||||
text-align: center;
|
||||
font-family: alphaClouds;
|
||||
text-shadow: 2px 2px #004225;
|
||||
font-size: 1.5em;
|
||||
mix-blend-mode: difference;
|
||||
margin: 1em 0;
|
||||
left: 1em;
|
||||
}
|
||||
|
||||
@supports (-webkit-text-stroke: 1px lightpink) {
|
||||
#varia {
|
||||
-webkit-text-stroke: 1px lightpink;
|
||||
@supports (-webkit-text-stroke: 1px darkgreen) {
|
||||
#library {
|
||||
-webkit-text-stroke: 1px darkgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin 0 auto;
|
||||
}
|
||||
|
||||
#bookshelf {
|
||||
max-width: 90%;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
margin-left: 1em;
|
||||
display: block;
|
||||
columns: 30rem;
|
||||
gap: 1rem;
|
||||
@ -71,57 +49,23 @@ body:after {
|
||||
}
|
||||
|
||||
#publication {
|
||||
margin-bottom: 3em;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
#latestevent {
|
||||
text-align: center;
|
||||
color: #DD4F77;
|
||||
}
|
||||
|
||||
#upcomingevent {
|
||||
text-align: center;
|
||||
color: #404d81;
|
||||
}
|
||||
|
||||
.event {
|
||||
margin: 0 1em 1em;
|
||||
max-width: calc(90% - 3em);
|
||||
min-width: calc(90% - 3em);
|
||||
margin-top: 3em;
|
||||
padding: 6px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
border: 3px solid black;
|
||||
background-color: #f1f1f1;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
button {
|
||||
z-index: 10;
|
||||
border: 3px solid black;
|
||||
padding: 6px;
|
||||
color: black;
|
||||
min-width: auto;
|
||||
background-color: #f1f1f1;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 1em 1em;
|
||||
z-index: 10;
|
||||
border: 3px solid black;
|
||||
border: 1px solid black;
|
||||
background-color: #f1f1f1;
|
||||
border-spacing: 0;
|
||||
}
|
||||
tbody:hover {background-color: #ddd;}
|
||||
|
||||
tr {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
margin:0;
|
||||
padding: 0.5em;
|
||||
min-width: auto;
|
||||
max-width: 60%;
|
||||
@ -140,18 +84,60 @@ td {
|
||||
.error{
|
||||
color: #ff1111;
|
||||
}
|
||||
|
||||
a:link { text-decoration: none; }
|
||||
a:visited { text-decoration: none; }
|
||||
a:hover { text-decoration: none; }
|
||||
a:active { text-decoration: none; }
|
||||
|
||||
|
||||
div#login {
|
||||
width: 25%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-decoration: none;
|
||||
}
|
||||
input[type=text], input[type=password], input[type=file] {
|
||||
width: 18em;
|
||||
max-width: 18em;
|
||||
border: 1px solid #E0B0FF;
|
||||
}
|
||||
|
||||
input {
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div#auth_buttons{
|
||||
position: absolute;
|
||||
top: 0.7em;
|
||||
right: 0.7em;
|
||||
display:flex;
|
||||
gap: 1em;
|
||||
flex-direction: row;
|
||||
|
||||
}
|
||||
div#auth_buttons a{
|
||||
font-size: 0.8em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
.auth {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
border: 1px solid #404d81;
|
||||
background-color: #fefefe;
|
||||
}
|
||||
.auth:hover{
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
|
||||
#varia {
|
||||
#library {
|
||||
text-align: left;
|
||||
max-width: 70%;
|
||||
}
|
||||
.event {
|
||||
max-width: 90%;
|
||||
margin: 3em 1em 1em 1em;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
#uploadform {
|
||||
#publicationform {
|
||||
max-width: 60%;
|
||||
margin-top: 3em;
|
||||
margin-left: 1em;
|
||||
@ -13,12 +13,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uploadform-field {
|
||||
.publicationform-field {
|
||||
margin: 0;
|
||||
padding: 1em 0em 1em 0em;
|
||||
}
|
||||
|
||||
input[type=text], select {
|
||||
.publicationform-field input[type=text], select {
|
||||
width: 100%;
|
||||
padding: 1em 3em;
|
||||
padding-left: 0.5em;
|
||||
@ -44,9 +44,7 @@ input[type=submit] {
|
||||
background-color: #DD4F77;
|
||||
text-align: right;
|
||||
color: white;
|
||||
padding: 1em 3em;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=submit]:hover {
|
||||
@ -59,7 +57,7 @@ fieldset{
|
||||
padding-left: 0em;
|
||||
}
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 480px) {
|
||||
#uploadform {
|
||||
#publicationform {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
BIN
library/static/fonts/LibreBaskerville-Regular.otf
Normal file
BIN
library/static/fonts/LibreBaskerville-Regular.ttf
Normal file
18
library/static/icons/users-feather-sprite.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"><defs>
|
||||
<symbol id="log-in">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||
<polyline points="10 17 15 12 10 7"></polyline>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
</symbol>
|
||||
<symbol id="log-out">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</symbol>
|
||||
<symbol id="user-plus">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="8.5" cy="7" r="4"></circle>
|
||||
<line x1="20" y1="8" x2="20" y2="14"></line>
|
||||
<line x1="23" y1="11" x2="17" y2="11"></line>
|
||||
</symbol>
|
||||
</defs></svg>
|
After Width: | Height: | Size: 673 B |
3
library/static/images/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
*
|
||||
*/
|
||||
!.gitignore
|
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 68 KiB |
@ -1,4 +1,3 @@
|
||||
// Filter section ===================== old school code divider ================
|
||||
|
||||
filterSelection("all", "None");
|
||||
function filterSelection(c, id) {
|
||||
@ -29,11 +28,14 @@ function resetDropDownButtons(){
|
||||
document.getElementById("License").innerText = "License";
|
||||
document.getElementById("PubType").innerText = "Type";
|
||||
document.getElementById("Year").innerText = "Year";
|
||||
document.getElementById('booksearch').value= "";
|
||||
document.getElementById('booksearch').placeholder = "🔍 Search..";
|
||||
allactivebuttons = document.getElementsByClassName("activebtn");
|
||||
for(var i = 0;allactivebuttons.length; i++) {
|
||||
removeClass(allactivebuttons[i], "activebtn");
|
||||
}
|
||||
}
|
||||
|
||||
function addClass(element, name) {
|
||||
var i, arr1, arr2;
|
||||
arr1 = element.className.split(" ");
|
||||
|
@ -1,32 +0,0 @@
|
||||
// Cloud section ===================== old school code divider =================
|
||||
function rn(from, to) {
|
||||
return ~~(Math.random() * (to - from + 1)) + from;
|
||||
}
|
||||
|
||||
function rs() {
|
||||
return arguments[rn(1, arguments.length) - 1];
|
||||
}
|
||||
|
||||
function boxShadows(max) {
|
||||
let ret = [];
|
||||
for (let i = 0; i < max; ++i) {
|
||||
ret.push(`
|
||||
${ rn(1, 110) }vw ${ rn(1, 110) }vh ${ rn(20, 30) }vmin ${ rn(10, 60) }vmin
|
||||
${ rs('#F52D75', '#CCBD4F', '#32497F', '#EB4377') }
|
||||
`)
|
||||
}
|
||||
return ret.join(',');
|
||||
}
|
||||
|
||||
const cloud = document.querySelector('#cloud');
|
||||
|
||||
function update() {
|
||||
if (window.screen.availWidth > 400 && window.screen.availHeight > 400 ) {
|
||||
cloud.style.boxShadow = boxShadows(30);
|
||||
}
|
||||
else {
|
||||
document.body.style.backgroundImage = "linear-gradient(to bottom right, white, #F52D75)";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', update);
|
42
library/static/js/search.js
Normal file
@ -0,0 +1,42 @@
|
||||
let searchInput = document.getElementById('booksearch');
|
||||
var allpublications = document.getElementsByClassName("filter");
|
||||
|
||||
const ENTER_KEY_CODE = 13;
|
||||
|
||||
searchInput.addEventListener('keyup', function(e) {
|
||||
if (e.keyCode === ENTER_KEY_CODE) {
|
||||
if (searchInput.value.length > 2) {
|
||||
searchBooks(searchInput.value);
|
||||
} else {
|
||||
clearSearchBooks();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function searchBooks(searchQuery) {
|
||||
let searchUrl = `search/${searchQuery}`
|
||||
fetch(searchUrl)
|
||||
.then(response => response.json())
|
||||
.then(searchdata => {
|
||||
console.log(`book ids: ${searchdata} found for ${searchQuery}`);
|
||||
if (searchdata === undefined || searchdata.length == 0) return;
|
||||
for (i = 0; i < allpublications.length; i++) {
|
||||
removeClass(allpublications[i], "show");
|
||||
}
|
||||
searchdata.forEach(bookid => {
|
||||
showBookId(bookid)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function showBookId(bookid) {
|
||||
let book = document.getElementById(bookid)
|
||||
addClass(book, "show");
|
||||
}
|
||||
|
||||
|
||||
function clearSearchBooks() {
|
||||
for (i = 0; i < allpublications.length; i++) {
|
||||
addClass(allpublications[i], "show");
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="197.81665"
|
||||
height="539.99939"
|
||||
viewBox="0 0 197.06678 537.95139"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="bookmark-upcoming.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
|
||||
id="metadata13"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs11"><rect
|
||||
x="207.04167"
|
||||
y="97.061699"
|
||||
width="111.52129"
|
||||
height="86.161156"
|
||||
id="rect17" /></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0725863"
|
||||
inkscape:cx="-8.3621061"
|
||||
inkscape:cy="267.6066"
|
||||
inkscape:window-x="1366"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-170.442,-5e-4)">
|
||||
<path
|
||||
d="M 346.164,0 H 191.939 c -11.857,0 -21.497,9.716 -21.497,21.497 v 505.894 c 0,11.857 6.12,14.076 13.617,4.896 0,0 56.304,-68.697 70.609,-87.822 14.306,-19.125 15.683,-21.268 32.972,0 17.365,21.268 66.938,87.363 66.938,87.363 7.191,9.41 12.929,7.496 12.929,-4.283 V 21.497 C 367.66,9.716 357.945,0 346.164,0 Z"
|
||||
fill="#404d81"
|
||||
id="path2" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text15"
|
||||
style="font-size:40px;line-height:1.25;font-family:unscii;-inkscape-font-specification:unscii;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect17);"
|
||||
transform="translate(-182.94372,-1.3642201)" /><text
|
||||
xml:space="preserve"
|
||||
style="font-size:42.6667px;line-height:1.25;font-family:unscii;-inkscape-font-specification:unscii;letter-spacing:0px;word-spacing:0px"
|
||||
x="98.764069"
|
||||
y="245.41867"
|
||||
id="text23"
|
||||
inkscape:transform-center-x="-22.440464"
|
||||
inkscape:transform-center-y="6.5100632"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan21"
|
||||
x="98.764069"
|
||||
y="245.41867"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Latest</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="98.764069"
|
||||
y="300.75204"
|
||||
id="tspan25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Read & </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="98.764069"
|
||||
y="356.08542"
|
||||
id="tspan27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Repair</tspan></text></svg>
|
Before Width: | Height: | Size: 3.6 KiB |
@ -1,73 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="197.81665"
|
||||
height="539.99939"
|
||||
viewBox="0 0 197.06678 537.95139"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="bookmark-past.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
|
||||
id="metadata11"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs9"><rect
|
||||
x="177.97797"
|
||||
y="223.17792"
|
||||
width="182.00249"
|
||||
height="193.39874"
|
||||
id="rect21" /></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="956"
|
||||
inkscape:window-height="1041"
|
||||
id="namedview7"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0725863"
|
||||
inkscape:cx="97.88594"
|
||||
inkscape:cy="268.97551"
|
||||
inkscape:window-x="2326"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Capa_1"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-170.442,-5e-4)">
|
||||
<path
|
||||
d="M 346.164,0 H 191.939 c -11.857,0 -21.497,9.716 -21.497,21.497 v 505.894 c 0,11.857 6.12,14.076 13.617,4.896 0,0 56.304,-68.697 70.609,-87.822 14.306,-19.125 15.683,-21.268 32.972,0 17.365,21.268 66.938,87.363 66.938,87.363 7.191,9.41 12.929,7.496 12.929,-4.283 V 21.497 C 367.66,9.716 357.945,0 346.164,0 Z"
|
||||
fill="#dd4f77"
|
||||
id="path2" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text19"
|
||||
style="font-size:42.6667px;line-height:1.25;font-family:unscii;-inkscape-font-specification:unscii;text-align:center;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect21);"
|
||||
x="57.5"
|
||||
y="0"
|
||||
transform="translate(-170.442,-5e-4)"><tspan
|
||||
x="232.94822"
|
||||
y="262.5111"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Carlito;-inkscape-font-specification:Carlito;fill:#ffffff">Past </tspan></tspan><tspan
|
||||
x="212.52112"
|
||||
y="317.84447"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Carlito;-inkscape-font-specification:Carlito;fill:#ffffff">Events</tspan></tspan></text></svg>
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="197.81665"
|
||||
height="539.99939"
|
||||
viewBox="0 0 197.06678 537.95139"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="bookmark-upcoming.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
|
||||
id="metadata13"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs11"><rect
|
||||
x="207.04167"
|
||||
y="97.061699"
|
||||
width="111.52129"
|
||||
height="86.161156"
|
||||
id="rect17" /></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1041"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0725863"
|
||||
inkscape:cx="-8.3621061"
|
||||
inkscape:cy="267.6066"
|
||||
inkscape:window-x="1366"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-170.442,-5e-4)">
|
||||
<path
|
||||
d="M 346.164,0 H 191.939 c -11.857,0 -21.497,9.716 -21.497,21.497 v 505.894 c 0,11.857 6.12,14.076 13.617,4.896 0,0 56.304,-68.697 70.609,-87.822 14.306,-19.125 15.683,-21.268 32.972,0 17.365,21.268 66.938,87.363 66.938,87.363 7.191,9.41 12.929,7.496 12.929,-4.283 V 21.497 C 367.66,9.716 357.945,0 346.164,0 Z"
|
||||
fill="#404d81"
|
||||
id="path2" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text15"
|
||||
style="font-size:40px;line-height:1.25;font-family:unscii;-inkscape-font-specification:unscii;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect17);"
|
||||
transform="translate(-182.94372,-1.3642201)" /><text
|
||||
xml:space="preserve"
|
||||
style="font-size:42.6667px;line-height:1.25;font-family:unscii;-inkscape-font-specification:unscii;letter-spacing:0px;word-spacing:0px"
|
||||
x="98.764069"
|
||||
y="245.41867"
|
||||
id="text23"
|
||||
inkscape:transform-center-x="-22.440464"
|
||||
inkscape:transform-center-y="6.5100632"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan21"
|
||||
x="98.764069"
|
||||
y="245.41867"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Upcoming</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="98.764069"
|
||||
y="300.75204"
|
||||
id="tspan25"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Read & </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="98.764069"
|
||||
y="356.08542"
|
||||
id="tspan27"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:42.6667px;font-family:Carlito;-inkscape-font-specification:Carlito;text-align:center;text-anchor:middle;fill:#ffffff">Repair</tspan></text></svg>
|
Before Width: | Height: | Size: 3.6 KiB |
@ -3,11 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>varia library zone</title>
|
||||
<title>{{title}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/style.css')}}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dropdown.css')}}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bookmark.css')}}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/upload.css')}}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/feather.css')}}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='icons/favicon.ico') }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='icons/apple-touch-icon.png')}}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='icons/favicon-32x32.png')}}">
|
||||
@ -15,23 +15,8 @@
|
||||
<link rel="manifest" href="{{ url_for('static', filename='icons/site.webmanifest')}}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="cloud"></div>
|
||||
<a href="/"><h1 id="varia">VARIA LIBRARY COLLECTION</h1></a>
|
||||
<a href="pastevents"><img src="{{ url_for('static', filename='svg/bookmark-past.svg')}}" id="pastevents" /></a>
|
||||
{% if upcoming %}
|
||||
<a href="upcoming"><img src="{{ url_for('static', filename='svg/bookmark-upcoming.svg')}}" id="upcomingevents" /></a>
|
||||
{% else %}
|
||||
<a href="upcoming"><img src="{{ url_for('static', filename='svg/bookmark-latest.svg')}}" id="upcomingevents" /></a>
|
||||
{% endif %}
|
||||
<a href="/" id="library"><h1 >{{title}}</h1></a>
|
||||
{% block main %}
|
||||
{% endblock main %}
|
||||
<svg width="0">
|
||||
<filter id="filter">
|
||||
<feTurbulence type="fractalNoise"
|
||||
baseFrequency=".001" numOctaves="5" />
|
||||
<feDisplacementMap in="SourceGraphic" scale="240" />
|
||||
</filter>
|
||||
</svg>
|
||||
</body>
|
||||
<script src="{{ url_for('static', filename='js/script.js')}}"></script>
|
||||
</html>
|
||||
|
29
library/templates/bookshelf.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% block bookshelf %}
|
||||
<div id="bookshelf">
|
||||
{% for id, pubinfo in publications.items() %}
|
||||
<div id="{{ id }}" class='book filter {{ pubinfo["Type"] }} {{ pubinfo["Year"] }} {{ pubinfo["License"] }}'>
|
||||
<table>
|
||||
<tbody>
|
||||
{%if pubinfo["Image"]%}
|
||||
<tr>
|
||||
<td colspan="2" class="tdimage">
|
||||
<img src="{{ url_for('static', filename='images/image-{0}.jpg'.format(id))}}" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="author">Author/Editor:</td>
|
||||
<td>{{ pubinfo["Author"] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Title:</td>
|
||||
<td><a href='{{ id }}'>{{ pubinfo["Title"] }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor%}
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/dropdown.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/search.js')}}"></script>
|
||||
{% endblock bookshelf %}
|
@ -1,34 +1,7 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="nav" class="container">
|
||||
{% include 'menu.html' %}
|
||||
</div>
|
||||
<div id="bookshelf">
|
||||
{% for id, pubinfo in publications.items() %}
|
||||
<div class='book filter {{ pubinfo["Type"] }} {{ pubinfo["Year"] }} {{ pubinfo["License"] }}'>
|
||||
<a href='{{ id }}'>
|
||||
<table>
|
||||
<tbody>
|
||||
{%if pubinfo["Image"]%}
|
||||
<tr>
|
||||
<td colspan="2" class="tdimage">
|
||||
<img src="{{ url_for('static', filename='images/image-{0}.jpg'.format(id))}}" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Author/Editor</td>
|
||||
<td>{{ pubinfo["Author"] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Title</td>
|
||||
<td>{{ pubinfo["Title"] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor%}
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='js/dropdown.js')}}"></script>
|
||||
{% include 'user_authorization.html' %}
|
||||
{% include 'bookshelf.html' %}
|
||||
{% endblock %}
|
||||
|
@ -1,7 +1,14 @@
|
||||
{% block menu %}
|
||||
<button id="leftmostbtn" onclick="filterSelection('all')">All books</button>
|
||||
<button><a href="/upload">Upload</a></button>
|
||||
<div class="dropdown">
|
||||
<nav id="nav" class="menu">
|
||||
<div class="dropdown">
|
||||
<button onclick="filterSelection('all')" class="dropbtn">Reset search</button>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn"><a href="/upload">Upload</a></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<button id="PubType" class="dropbtn">Type</button>
|
||||
<div class="dropdown-content">
|
||||
{% for pubtype in pubtypes %}
|
||||
@ -25,4 +32,8 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<input class="dropbtn" id="booksearch" type="text" placeholder="🔍 Search..">
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock menu %}
|
||||
|
@ -1,22 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="nav" class="container">
|
||||
<button id="leftmostbtn"><a href="/">All books</a></button>
|
||||
<button><a href="/upload">Upload</a></button>
|
||||
<div class="dropdown" style="visibility: hidden">
|
||||
<button id="Year" class="dropbtn">Year</button>
|
||||
</div>
|
||||
</div>
|
||||
{% for eventtitle, text in events.items() %}
|
||||
<div class="event">
|
||||
<h2>{{ eventtitle }}</h2>
|
||||
{{ text[0]|safe }}
|
||||
<p>For those interested to learn more on the topics of this read and repair event the physical library at varia offers the following books:</p>
|
||||
<ul>
|
||||
{% for link, booktitle in text[1].items() %}
|
||||
<li><a href="/{{ link }}">{{ booktitle }}</a></li>
|
||||
{% endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor%}
|
||||
{% endblock %}
|
@ -1,8 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="nav" class="container">
|
||||
<button id="leftmostbtn"><a href="/">All books</a></button>
|
||||
{% include 'user_authorization.html' %}
|
||||
<div id="nav" class="menu">
|
||||
<div class="dropdown">
|
||||
<button><a href="/">All books</a></button>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="dropdown">
|
||||
<button><a href="/upload">Upload</a></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dropdown" style="visibility: hidden">
|
||||
<button id="Year" class="dropbtn">Year</button>
|
||||
</div>
|
||||
@ -29,10 +36,6 @@
|
||||
<td>Year</td>
|
||||
<td>{{ fullpublication["Year"] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Custodian</td>
|
||||
<td>{{ fullpublication["Custodian"] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fields</td>
|
||||
<td>{{ fullpublication["Fields"] }}</td>
|
||||
@ -58,31 +61,43 @@
|
||||
<td><p>{{ fullpublication["Comments"] }}</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Currently borrowed by:</td>
|
||||
<td><p>{{ fullpublication["Borrowed"] }}</p></td>
|
||||
<td>File</td>
|
||||
<td><a href='files/{{fullpublication["Files"]}}'><p>{{ fullpublication["Files"] }}</p></a></td>
|
||||
</tr>
|
||||
{% if current_user.is_authenticated %}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<form class="borrow" method="POST" action="/{{ publicationID }}">
|
||||
{{ borrowform.csrf_token }}
|
||||
<fieldset class="borrowform-field">
|
||||
{{ borrowform.borrowed.label }}
|
||||
{{ borrowform.borrowed }}
|
||||
{% for message in borrowform.borrowed.errors %}
|
||||
<td>Upload new</td>
|
||||
<td>
|
||||
<form method="POST" enctype="multipart/form-data" action="/{{ publicationID }}">
|
||||
{{ pdf_uploadform.csrf_token }}
|
||||
<fieldset class="fileupload-field">
|
||||
{{ pdf_uploadform.pdf.label }}
|
||||
{{ pdf_uploadform.pdf }}
|
||||
{% for message in pdf_uploadform.pdf.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="borrowform-field">
|
||||
{{ borrowform.secret.label }}
|
||||
{{ borrowform.secret }}
|
||||
{% for message in borrowform.secret.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{{ borrowform.submit }}
|
||||
<form>
|
||||
{{ pdf_uploadform.submit }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upload new</td>
|
||||
<td>
|
||||
<form method="POST" enctype="multipart/form-data" action="/{{ publicationID }}">
|
||||
{{ image_uploadform.csrf_token }}
|
||||
<fieldset class="fileupload-field">
|
||||
{{ image_uploadform.image.label }}
|
||||
{{ image_uploadform.image }}
|
||||
{% for message in image_uploadform.image.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{{ image_uploadform.submit }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -1,20 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="nav" class="container">
|
||||
<button id="leftmostbtn"><a href="/">All books</a></button>
|
||||
<button><a href="/upload">Upload</a></button>
|
||||
<div class="dropdown" style="visibility: hidden">
|
||||
<button id="Year" class="dropbtn">Year</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event">
|
||||
{% if upcoming %}
|
||||
<h2 id="upcomingevent">Upcoming event!</h2>
|
||||
{% else %}
|
||||
<p id="latestevent">Unfortunately this Read&Repair event has already happened, keep an eye on <a href="http://varia.zone/">varia.zone</a>
|
||||
or this site for upcoming Read&Repair and other Varia events!</p>
|
||||
{% endif %}
|
||||
<h2>{{ event.title }}</h2>
|
||||
{{ event.description|safe }}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,115 +1,102 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="nav" class="container">
|
||||
<button id="leftmostbtn"><a href="/">All books</a></button>
|
||||
<div id="nav" class="menu">
|
||||
<div class="dropdown">
|
||||
<button><a href="/">Back to books</a></button>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button><a href="/upload">Upload</a></button>
|
||||
</div>
|
||||
<div class="dropdown" style="visibility: hidden">
|
||||
<button id="Year" class="dropbtn">Year</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploadform">
|
||||
<div id="publicationform">
|
||||
|
||||
{% for message in uploadform.uploadpublication.errors %}
|
||||
{% for message in publicationform.uploadpublication.errors %}
|
||||
<div>{{ message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in uploadform.author.errors %}
|
||||
{% for message in publicationform.author.errors %}
|
||||
<div>{{ message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<h2 id="uploadformtitle">Upload a new book</h2>
|
||||
<h2 id="publicationformtitle">Upload a new book</h2>
|
||||
<form method="POST" enctype="multipart/form-data" action="{{ url_for('upload') }}">
|
||||
{{ uploadform.csrf_token }}
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.uploadpublication.label }}
|
||||
{{ uploadform.uploadpublication }}
|
||||
{% for message in uploadform.uploadpublication.errors %}
|
||||
{{ publicationform.csrf_token }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.uploadpublication.label }}
|
||||
{{ publicationform.uploadpublication }}
|
||||
{% for message in publicationform.uploadpublication.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.author.label }}
|
||||
{{ uploadform.author }}
|
||||
{% for message in uploadform.author.errors %}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.author.label }}
|
||||
{{ publicationform.author }}
|
||||
{% for message in publicationform.author.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.year.label }}
|
||||
{{ uploadform.year }}
|
||||
{% for message in uploadform.year.errors %}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.year.label }}
|
||||
{{ publicationform.year }}
|
||||
{% for message in publicationform.year.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.custodian.label }}
|
||||
{{ uploadform.custodian }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.fields.label }}
|
||||
{{ publicationform.fields }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.fields.label }}
|
||||
{{ uploadform.fields }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.type.label }}
|
||||
{{ uploadform.type }}
|
||||
{% for message in uploadform.type.errors %}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.type.label }}
|
||||
{{ publicationform.type }}
|
||||
{% for message in publicationform.type.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.publishers.label }}
|
||||
{{ uploadform.publishers }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.publishers.label }}
|
||||
{{ publicationform.publishers }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.license.label }}
|
||||
{{ uploadform.license }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.license.label }}
|
||||
{{ publicationform.license }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.licenseshort.label }}
|
||||
{{ uploadform.licenseshort }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.highlights.label }}
|
||||
{{ publicationform.highlights }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.highlights.label }}
|
||||
{{ uploadform.highlights }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.comments.label }}
|
||||
{{ uploadform.comments }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.borrowed.label }}
|
||||
{{ uploadform.borrowed }}
|
||||
<fieldset class="publicationform-field">
|
||||
{{ publicationform.comments.label }}
|
||||
{{ publicationform.comments }}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fileupload-field">
|
||||
{{ uploadform.image.label }}
|
||||
{{ uploadform.image }}
|
||||
{% for message in uploadform.image.errors %}
|
||||
{{ publicationform.image.label }}
|
||||
{{ publicationform.image }}
|
||||
{% for message in publicationform.image.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="uploadform-field">
|
||||
{{ uploadform.secret.label }}
|
||||
{{ uploadform.secret }}
|
||||
{% for message in uploadform.secret.errors %}
|
||||
<fieldset class="fileupload-field">
|
||||
{{ publicationform.pdf.label }}
|
||||
{{ publicationform.pdf }}
|
||||
{% for message in publicationform.pdf.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
{{ uploadform.submit }}
|
||||
|
||||
{{ publicationform.submit }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
29
library/templates/user/forgotpassword.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="mainworkflow">
|
||||
<div class="workflow">
|
||||
<h2>Forgot your password?</h2>
|
||||
<p>
|
||||
Enter the email address that was used to register with the library.
|
||||
</p>
|
||||
<form class="form" action="{{ url_for('forgotpassword') }}" method="post">
|
||||
{{ forgotpasswordform.csrf_token }}
|
||||
<fieldset class="required">
|
||||
{{ forgotpasswordform.email.label }}
|
||||
{{ forgotpasswordform.email }}
|
||||
{% for message in forgotpasswordform.email.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="button required error">
|
||||
{{ forgotpasswordform.submit }}
|
||||
<div class="overview">
|
||||
<a href="/">
|
||||
<input type="button" name="button" value="Back to main page"></input>
|
||||
</a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div
|
||||
{% endblock main %}
|
26
library/templates/user/login.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="login">
|
||||
<form class="form" action="{{ url_for('login') }}" method="post">
|
||||
{{ loginform.csrf_token }}
|
||||
<fieldset class="required">
|
||||
{{ loginform.email.label }}
|
||||
{{ loginform.email }}
|
||||
{% for message in loginform.email.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="required">
|
||||
{{ loginform.password.label }}
|
||||
{{ loginform.password }}
|
||||
{% for message in loginform.password.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="button required">
|
||||
{{ loginform.submit }}
|
||||
<a href="/forgotpassword">Forgot Password?</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock main %}
|
39
library/templates/user/register.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="login">
|
||||
<form class="form" action="{{ url_for('register') }}" method="post">
|
||||
{{ registerform.csrf_token }}
|
||||
<fieldset class="required">
|
||||
{{ registerform.username.label }}
|
||||
{{ registerform.username }}
|
||||
{% for message in registerform.username.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="required">
|
||||
{{ registerform.email.label }}
|
||||
{{ registerform.email }}
|
||||
{% for message in registerform.email.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="required">
|
||||
{{ registerform.password.label }}
|
||||
{{ registerform.password }}
|
||||
{% for message in registerform.password.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="required">
|
||||
{{ registerform.confirmpassword.label }}
|
||||
{{ registerform.confirmpassword }}
|
||||
{% for message in registerform.confirmpassword.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="button required">
|
||||
{{ registerform.submit }}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock main %}
|
29
library/templates/user/resetpassword.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div id="login">
|
||||
{% if linkvalid%}
|
||||
<form class="form" action="{{ url_for('resetpassword', path=path) }}" method="post">
|
||||
{{ resetpasswordform.csrf_token }}
|
||||
<fieldset class="required">
|
||||
{{ resetpasswordform.password.label }}
|
||||
{{ resetpasswordform.password }}
|
||||
{% for message in resetpasswordform.password.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="required">
|
||||
{{ resetpasswordform.confirmpassword.label }}
|
||||
{{ resetpasswordform.confirmpassword }}
|
||||
{% for message in resetpasswordform.confirmpassword.errors %}
|
||||
<div class="error">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset class="button required">
|
||||
{{ resetpasswordform.submit }}
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
<h3>Password reset link no longer valid.</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock main %}
|
31
library/templates/user_authorization.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% block auth %}
|
||||
<div id="auth_buttons">
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="auth">
|
||||
<a href="/login">
|
||||
<svg class="feather">
|
||||
<use href="{{ url_for('static', filename='icons/users-feather-sprite.svg')+ '#log-in'}}" />
|
||||
</svg>
|
||||
<span>Sign in</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="auth">
|
||||
<a href="/register">
|
||||
<svg class="feather">
|
||||
<use href="{{ url_for('static', filename='icons/users-feather-sprite.svg') + '#user-plus'}}" />
|
||||
</svg>
|
||||
<span>Register</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="auth">
|
||||
<a href="/logout">
|
||||
<svg class="feather">
|
||||
<use href="{{ url_for('static', filename='icons/users-feather-sprite.svg') + '#log-out'}}" />
|
||||
</svg>
|
||||
<span>Sign out</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock auth %}
|
@ -1,6 +1,6 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
target-version = ['py37', 'py38', 'py39']
|
||||
target-version = ['py310','py311']
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
@ -21,4 +21,3 @@ exclude = '''
|
||||
| profiling
|
||||
)/
|
||||
'''
|
||||
|
||||
|
325
requirements.txt
@ -1,7 +1,318 @@
|
||||
icalendar
|
||||
feedparser
|
||||
flask
|
||||
flask_wtf
|
||||
requests
|
||||
Pillow
|
||||
bcrypt
|
||||
anyio==4.2.0
|
||||
appdirs==1.4.4
|
||||
apsw==3.43.0.0
|
||||
argcomplete==3.1.4
|
||||
asttokens==2.4.1
|
||||
async-timeout==4.0.3
|
||||
attrs==23.2.0
|
||||
beautifulsoup4==4.12.3
|
||||
blinker==1.7.0
|
||||
Brotli==1.1.0
|
||||
certifi==2023.11.17
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
colorama==0.4.6
|
||||
coloredlogs==15.0.1
|
||||
configobj==5.0.8
|
||||
contourpy==1.0.7
|
||||
crit==3.17.1
|
||||
cryptography==42.0.5
|
||||
css-parser==1.0.10
|
||||
cssselect==1.2.0
|
||||
cupshelpers==1.0
|
||||
cycler==0.12.1
|
||||
dbus-python==1.3.2
|
||||
decorator==5.1.1
|
||||
defcon==0.10.3
|
||||
Deprecated==1.2.14
|
||||
deprecation==2.0.7
|
||||
distlib==0.3.8
|
||||
distro==1.9.0
|
||||
dnspython==2.6.1
|
||||
executing==2.0.1
|
||||
feedparser==6.0.10
|
||||
filelock==3.13.1
|
||||
fontPens==0.2.4
|
||||
fonttools==4.46.0
|
||||
freetype-py==2.4.0
|
||||
fs==2.4.16
|
||||
greenlet==3.0.1
|
||||
gyp-next==0.16.2
|
||||
h11==0.14.0
|
||||
h2==4.1.0
|
||||
hpack==4.0.0
|
||||
html2text==2024.2.26
|
||||
html5-parser==0.4.12
|
||||
html5lib==1.1
|
||||
httpcore==1.0.4
|
||||
httplib2==0.20.4
|
||||
httpx==0.27.0
|
||||
humanfriendly==10.0
|
||||
hyperframe==6.0.0
|
||||
hypothesis==6.99.9
|
||||
idna==3.6
|
||||
ifaddr==0.2.0
|
||||
img2pdf==0.5.1
|
||||
importlib-metadata==4.12.0
|
||||
ipython==8.20.0
|
||||
jedi==0.19.1
|
||||
jeepney==0.8.0
|
||||
kiwisolver==0.0.0
|
||||
lazr.restfulclient==0.14.6
|
||||
lazr.uri==1.0.6
|
||||
libevdev==0.11
|
||||
lxml==5.1.0
|
||||
lz4==4.0.2+dfsg
|
||||
Markdown==3.6
|
||||
markdown-it-py==3.0.0
|
||||
matplotlib==3.6.3
|
||||
matplotlib-inline==0.1.6
|
||||
mdurl==0.1.2
|
||||
mechanize==0.4.9
|
||||
more-itertools==10.2.0
|
||||
mpmath==1.3.0
|
||||
msgpack==1.0.3
|
||||
mutagen==1.46.0
|
||||
netifaces==0.11.0
|
||||
nicotine-plus==3.2.9
|
||||
notify2==0.3
|
||||
numpy==1.26.4
|
||||
oauthlib==3.2.2
|
||||
ocrmypdf==15.2.0+dfsg1
|
||||
olefile==0.46
|
||||
openshot-qt==3.1.1
|
||||
packaging==23.2
|
||||
parso==0.8.3
|
||||
pbr==6.0.0
|
||||
pdfminer.six==20221105
|
||||
pexpect==4.9.0
|
||||
pikepdf==8.7.1
|
||||
pillow==10.2.0
|
||||
pipx==1.4.3
|
||||
platformdirs==4.2.0
|
||||
pluggy==1.4.0
|
||||
prompt-toolkit==3.0.43
|
||||
protobuf==4.21.12
|
||||
psutil==5.9.8
|
||||
ptyprocess==0.7.0
|
||||
pure-eval==0.0.0
|
||||
py==1.11.0
|
||||
py7zr==0.11.3+dfsg
|
||||
pycairo==1.25.1
|
||||
pychm==0.8.6
|
||||
pycryptodomex==3.20.0
|
||||
pycups==2.0.1
|
||||
pycurl==7.45.3
|
||||
Pygments==2.17.2
|
||||
PyGObject==3.48.1
|
||||
PyJWT==2.7.0
|
||||
pynvim==0.5.0
|
||||
PyOpenGL==3.1.7
|
||||
pyparsing==3.1.1
|
||||
PyQt5==5.15.10
|
||||
PyQt5-sip==12.13.0
|
||||
PyQt6==6.6.1
|
||||
PyQt6-sip==13.6.0
|
||||
PyQt6-WebEngine==6.6.0
|
||||
pysmbc==1.0.25.1
|
||||
python-apt==2.7.7
|
||||
python-dateutil==2.9.0
|
||||
python-debian==0.1.49
|
||||
python-debianbts==4.0.2
|
||||
pytz==2024.1
|
||||
pyudev==0.24.0
|
||||
pyxdg==0.28
|
||||
PyYAML==6.0.1
|
||||
pyzmq==24.0.1
|
||||
regex==2022.10.31
|
||||
reportbug==13.0.1
|
||||
reportlab==4.1.0
|
||||
repoze.lru==0.7
|
||||
requests==2.31.0
|
||||
rich==13.7.1
|
||||
rlPyCairo==0.3.0
|
||||
Routes==2.5.1
|
||||
SciPy==1.11.4
|
||||
scour==0.38.2
|
||||
sgmllib3k==1.0.0
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.5
|
||||
stack-data==0.6.3
|
||||
stevedore==5.1.0
|
||||
sympy==1.12
|
||||
terminator==2.1.3
|
||||
texttable==1.6.7
|
||||
traitlets==5.5.0
|
||||
types-aiofiles==23.2
|
||||
types-aws-xray-sdk==2.12
|
||||
types-beautifulsoup4==4.12
|
||||
types-bleach==6.1
|
||||
types-boltons==23.0
|
||||
types-boto==2.49
|
||||
types-braintree==4.24
|
||||
types-cachetools==5.3
|
||||
types-caldav==1.3
|
||||
types-cffi==1.16
|
||||
types-chevron==0.14
|
||||
types-click-default-group==1.2
|
||||
types-click-spinner==0.1
|
||||
types-colorama==0.4
|
||||
types-commonmark==0.9
|
||||
types-console-menu==0.8
|
||||
types-croniter==2.0
|
||||
types-dateparser==1.1
|
||||
types-decorator==5.1
|
||||
types-Deprecated==1.2
|
||||
types-dockerfile-parse==2.0
|
||||
types-docopt==0.6
|
||||
types-docutils==0.20
|
||||
types-editdistance==0.6
|
||||
types-entrypoints==0.4
|
||||
types-ExifRead==3.0
|
||||
types-first==2.0
|
||||
types-flake8-2020==1.8
|
||||
types-flake8-bugbear==23.9.16
|
||||
types-flake8-builtins==2.2
|
||||
types-flake8-docstrings==1.7
|
||||
types-flake8-plugin-utils==1.3
|
||||
types-flake8-rst-docstrings==0.3
|
||||
types-flake8-simplify==0.21
|
||||
types-flake8-typing-imports==1.15
|
||||
types-Flask-Cors==4.0
|
||||
types-Flask-Migrate==4.0
|
||||
types-Flask-SocketIO==5.3
|
||||
types-fpdf2==2.7.4
|
||||
types-gdb==12.1
|
||||
types-google-cloud-ndb==2.2
|
||||
types-greenlet==3.0
|
||||
types-hdbcli==2.18
|
||||
types-html5lib==1.1
|
||||
types-httplib2==0.22
|
||||
types-humanfriendly==10.0
|
||||
types-ibm-db==3.2
|
||||
types-influxdb-client==1.38
|
||||
types-inifile==0.4
|
||||
types-JACK-Client==0.5
|
||||
types-jmespath==1.0
|
||||
types-jsonschema==4.19
|
||||
types-keyboard==0.13
|
||||
types-ldap3==2.9
|
||||
types-libsass==0.22
|
||||
types-Markdown==3.5
|
||||
types-mock==5.1
|
||||
types-mypy-extensions==1.0
|
||||
types-mysqlclient==2.2
|
||||
types-netaddr==0.9
|
||||
types-oauthlib==3.2
|
||||
types-openpyxl==3.1
|
||||
types-opentracing==2.4
|
||||
types-paho-mqtt==1.6
|
||||
types-paramiko==3.3
|
||||
types-parsimonious==0.10
|
||||
types-passlib==1.7
|
||||
types-passpy==1.0
|
||||
types-peewee==3.17
|
||||
types-pep8-naming==0.13
|
||||
types-pexpect==4.8
|
||||
types-pika-ts==1.3
|
||||
types-Pillow==10.1
|
||||
types-playsound==1.3
|
||||
types-pluggy==1.2.0
|
||||
types-polib==1.2
|
||||
types-portpicker==1.6
|
||||
types-protobuf==4.24
|
||||
types-psutil==5.9
|
||||
types-psycopg2==2.9
|
||||
types-pyasn1==0.5
|
||||
types-pyaudio==0.2
|
||||
types-PyAutoGUI==0.9
|
||||
types-pycocotools==2.0
|
||||
types-pycurl==7.45.2
|
||||
types-pyfarmhash==0.3
|
||||
types-pyflakes==3.1
|
||||
types-Pygments==2.16
|
||||
types-pyinstaller==6.1
|
||||
types-pyjks==20.0
|
||||
types-PyMySQL==1.1
|
||||
types-pynput==1.7
|
||||
types-pyOpenSSL==23.3
|
||||
types-pyRFC3339==1.1
|
||||
types-PyScreeze==0.1.29
|
||||
types-pyserial==3.5
|
||||
types-pysftp==0.2
|
||||
types-pytest-lazy-fixture==0.6
|
||||
types-python-crontab==3.0
|
||||
types-python-datemath==1.5
|
||||
types-python-dateutil==2.8
|
||||
types-python-gflags==3.1
|
||||
types-python-jose==3.3
|
||||
types-python-nmap==0.7
|
||||
types-python-slugify==8.0
|
||||
types-python-xlib==0.33
|
||||
types-pytz==2023.3.post1
|
||||
types-pywin32==306
|
||||
types-pyxdg==0.28
|
||||
types-PyYAML==6.0
|
||||
types-qrcode==7.4
|
||||
types-redis==4.6.0
|
||||
types-regex==2023.10.3
|
||||
types-requests==2.31
|
||||
types-requests-oauthlib==1.3
|
||||
types-retry==0.9
|
||||
types-s2clientprotocol==5
|
||||
types-seaborn==0.13
|
||||
types-Send2Trash==1.8
|
||||
types-setuptools==68.2
|
||||
types-simplejson==3.19
|
||||
types-singledispatch==4.1
|
||||
types-six==1.16
|
||||
types-slumber==0.7
|
||||
types-stdlib-list==0.8
|
||||
types-stripe==3.5
|
||||
types-tabulate==0.9
|
||||
types-tensorflow==2.12
|
||||
types-toml==0.10
|
||||
types-toposort==1.10
|
||||
types-tqdm==4.66
|
||||
types-translationstring==1.4
|
||||
types-tree-sitter==0.20.1
|
||||
types-tree-sitter-languages==1.8
|
||||
types-ttkthemes==3.2
|
||||
types-tzlocal==5.1
|
||||
types-ujson==5.8
|
||||
types-untangle==1.2
|
||||
types-usersettings==1.1
|
||||
types-uWSGI==2.0
|
||||
types-vobject==0.9
|
||||
types-waitress==2.1
|
||||
types-WebOb==1.8
|
||||
types-whatthepatch==1.0
|
||||
types-workalendar==17.0
|
||||
types-WTForms==3.1
|
||||
types-xmltodict==0.13
|
||||
types-zstd==1.5
|
||||
types-zxcvbn==4.4
|
||||
typing_extensions==4.10.0
|
||||
unicodedata2==15.1.0
|
||||
urllib3==1.26.18
|
||||
userpath==1.9.1
|
||||
virtualenv==20.25.1+ds
|
||||
virtualenv-clone==0.5.7
|
||||
virtualenvwrapper==4.8.4
|
||||
wadllib==1.3.6
|
||||
wcwidth==0.2.13
|
||||
webencodings==0.5.1
|
||||
WebOb==1.8.7
|
||||
websocket-client==1.7.0
|
||||
websockets==10.4
|
||||
wrapt==1.15.0
|
||||
wxPython==4.2.1
|
||||
xdg==5
|
||||
xxhash==0.0.0
|
||||
yt-dlp==2024.3.10
|
||||
zeroconf==0.131.0
|
||||
zim==0.75.2
|
||||
zipp==1.0.0
|
||||
|