├── projeto ├── books │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── errors │ ├── __init__.py │ └── handlers.py ├── main │ ├── __init__.py │ └── routes.py ├── users │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── utils │ ├── __init__.py │ └── utils.py ├── site.db ├── static │ ├── img │ │ ├── eye.png │ │ ├── book.png │ │ ├── dark.png │ │ ├── skull.jpg │ │ └── Avatar.png │ ├── genre │ │ ├── Anime.png │ │ ├── Horror.png │ │ ├── Fantasy.png │ │ ├── Dystopian.png │ │ ├── FolkTales.png │ │ ├── Philosophy.png │ │ ├── Psychology.png │ │ ├── Spirituality.png │ │ ├── ScienceFiction.png │ │ └── SecretSocieties.png │ ├── book │ │ ├── book_default.jpg │ │ ├── 05c0d161486b5f4d.jpg │ │ ├── 0b9f1cc51534b4ad.jpg │ │ ├── 1db251b4ddd9b66c.jpg │ │ ├── 1e373146afe35225.jpg │ │ ├── 1f07eb24d1bbd7c4.jpg │ │ ├── 33cb04fcf7c006a7.jpg │ │ ├── 53755fa382636350.jpg │ │ ├── 9f9125ef1d5abc15.jpg │ │ ├── a255bf48f88b8640.jpg │ │ ├── a4c80fd9d9927a86.jpg │ │ ├── aac2979e7ed57323.jpg │ │ ├── bf738734dec6a982.jpg │ │ ├── c0f6b3b5cbeea00e.jpg │ │ ├── c6141374a557aef8.jpg │ │ ├── d2feeec82eb03ddd.jpg │ │ └── f7550ff017e23f5e.jpg │ ├── profile │ │ ├── default.jpg │ │ ├── 084f341724d79904.jpg │ │ ├── 86f28170e71332d8.jpg │ │ └── 98b12b995bc72768.png │ └── css │ │ └── style.css ├── config.py ├── templates │ ├── errors │ │ ├── 403.html │ │ ├── 500.html │ │ └── 404.html │ ├── message.html │ ├── user_books.html │ ├── search.html │ ├── login.html │ ├── genre.html │ ├── author.html │ ├── analysis.html │ ├── messages.html │ ├── register.html │ ├── layout.html │ ├── home.html │ ├── book.html │ ├── create_book.html │ └── account.html ├── __init__.py └── models.py ├── .gitignore ├── migrations ├── README ├── script.py.mako ├── versions │ ├── 2b121e25f167_.py │ ├── a99c1165e476_private_messages.py │ ├── 46e80c86a0fb_.py │ ├── 5ef733a72780_.py │ ├── e3f791d50878_.py │ ├── f3cdee093c3a_.py │ ├── b16720f2bef5_.py │ └── 9db4f46dd61b_private_messages.py ├── alembic.ini └── env.py ├── run.py ├── requirements.txt └── README.md /projeto/books/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projeto/errors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projeto/main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projeto/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projeto/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .myvenv 2 | myvenv/ 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /projeto/site.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/site.db -------------------------------------------------------------------------------- /projeto/static/img/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/img/eye.png -------------------------------------------------------------------------------- /projeto/static/img/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/img/book.png -------------------------------------------------------------------------------- /projeto/static/img/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/img/dark.png -------------------------------------------------------------------------------- /projeto/static/img/skull.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/img/skull.jpg -------------------------------------------------------------------------------- /projeto/static/genre/Anime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Anime.png -------------------------------------------------------------------------------- /projeto/static/genre/Horror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Horror.png -------------------------------------------------------------------------------- /projeto/static/img/Avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/img/Avatar.png -------------------------------------------------------------------------------- /projeto/static/genre/Fantasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Fantasy.png -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from projeto import create_app 2 | 3 | app = create_app() 4 | 5 | if __name__ == '__main__': 6 | app.run(debug=True) -------------------------------------------------------------------------------- /projeto/static/book/book_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/book_default.jpg -------------------------------------------------------------------------------- /projeto/static/genre/Dystopian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Dystopian.png -------------------------------------------------------------------------------- /projeto/static/genre/FolkTales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/FolkTales.png -------------------------------------------------------------------------------- /projeto/static/genre/Philosophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Philosophy.png -------------------------------------------------------------------------------- /projeto/static/genre/Psychology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Psychology.png -------------------------------------------------------------------------------- /projeto/static/profile/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/profile/default.jpg -------------------------------------------------------------------------------- /projeto/static/genre/Spirituality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/Spirituality.png -------------------------------------------------------------------------------- /projeto/static/book/05c0d161486b5f4d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/05c0d161486b5f4d.jpg -------------------------------------------------------------------------------- /projeto/static/book/0b9f1cc51534b4ad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/0b9f1cc51534b4ad.jpg -------------------------------------------------------------------------------- /projeto/static/book/1db251b4ddd9b66c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/1db251b4ddd9b66c.jpg -------------------------------------------------------------------------------- /projeto/static/book/1e373146afe35225.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/1e373146afe35225.jpg -------------------------------------------------------------------------------- /projeto/static/book/1f07eb24d1bbd7c4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/1f07eb24d1bbd7c4.jpg -------------------------------------------------------------------------------- /projeto/static/book/33cb04fcf7c006a7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/33cb04fcf7c006a7.jpg -------------------------------------------------------------------------------- /projeto/static/book/53755fa382636350.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/53755fa382636350.jpg -------------------------------------------------------------------------------- /projeto/static/book/9f9125ef1d5abc15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/9f9125ef1d5abc15.jpg -------------------------------------------------------------------------------- /projeto/static/book/a255bf48f88b8640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/a255bf48f88b8640.jpg -------------------------------------------------------------------------------- /projeto/static/book/a4c80fd9d9927a86.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/a4c80fd9d9927a86.jpg -------------------------------------------------------------------------------- /projeto/static/book/aac2979e7ed57323.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/aac2979e7ed57323.jpg -------------------------------------------------------------------------------- /projeto/static/book/bf738734dec6a982.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/bf738734dec6a982.jpg -------------------------------------------------------------------------------- /projeto/static/book/c0f6b3b5cbeea00e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/c0f6b3b5cbeea00e.jpg -------------------------------------------------------------------------------- /projeto/static/book/c6141374a557aef8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/c6141374a557aef8.jpg -------------------------------------------------------------------------------- /projeto/static/book/d2feeec82eb03ddd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/d2feeec82eb03ddd.jpg -------------------------------------------------------------------------------- /projeto/static/book/f7550ff017e23f5e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/book/f7550ff017e23f5e.jpg -------------------------------------------------------------------------------- /projeto/static/genre/ScienceFiction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/ScienceFiction.png -------------------------------------------------------------------------------- /projeto/static/genre/SecretSocieties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/genre/SecretSocieties.png -------------------------------------------------------------------------------- /projeto/static/profile/084f341724d79904.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/profile/084f341724d79904.jpg -------------------------------------------------------------------------------- /projeto/static/profile/86f28170e71332d8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/profile/86f28170e71332d8.jpg -------------------------------------------------------------------------------- /projeto/static/profile/98b12b995bc72768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-akira/Flask-Library/master/projeto/static/profile/98b12b995bc72768.png -------------------------------------------------------------------------------- /projeto/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | SECRET_KEY = 'ab57ccec0f56942a5ca33215f9d2d88c' 3 | SQLALCHEMY_DATABASE_URI = 'sqlite:///site.db' # caminho relativo em relação ao nosso arquivo 4 | SQLALCHEMY_TRACK_MODIFICATIONS = False -------------------------------------------------------------------------------- /projeto/templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Error 403

6 |
7 | 8 |
9 |

"Page Not Found!"

10 |
11 |
12 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/errors/handlers.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | errors = Blueprint('errors', __name__) 4 | 5 | @errors.app_errorhandler(404) 6 | def error_404(error): 7 | return render_template('errors/404.html'), 404 8 | 9 | @errors.app_errorhandler(403) 10 | def error_403(error): 11 | return render_template('errors/403.html'), 403 12 | 13 | @errors.app_errorhandler(500) 14 | def error_500(error): 15 | return render_template('errors/500.html'), 500 -------------------------------------------------------------------------------- /projeto/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Error 500

6 |
7 | 8 |
9 |

We are experiencing some trouble on our end. Please try again in the near future

10 |
11 |
12 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Error 404

6 |
7 | 8 |
9 |

"Page Not Found (404)!"

10 |

That page does not exist. Please try a different location

11 |
12 |
13 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | from PIL import Image 4 | from flask import url_for, current_app 5 | 6 | def save_picture(form_picture, path, width, height): 7 | random_hex = secrets.token_hex(8) 8 | _, f_ext = os.path.splitext(form_picture.filename) 9 | picture_fn = random_hex + f_ext 10 | picture_path = os.path.join(current_app.root_path, path, picture_fn) 11 | output_size = (width,height) 12 | i = Image.open(form_picture) 13 | i.thumbnail(output_size) 14 | i.save(picture_path) 15 | return picture_fn -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.7.7 2 | bcrypt==3.1.7 3 | cachelib==0.6.0 4 | cffi==1.12.3 5 | Click==7.0 6 | Flask==1.1.1 7 | Flask-Bcrypt==0.7.1 8 | Flask-Login==0.4.1 9 | Flask-Migrate==3.1.0 10 | Flask-Session==0.4.0 11 | Flask-SQLAlchemy==2.4.0 12 | Flask-WTF==0.14.2 13 | importlib-metadata==4.11.3 14 | importlib-resources==5.7.1 15 | itsdangerous==1.1.0 16 | Jinja2==2.11.3 17 | Mako==1.2.0 18 | MarkupSafe==1.1.1 19 | Pillow==9.0.1 20 | pycparser==2.19 21 | six==1.12.0 22 | SQLAlchemy==1.3.8 23 | typing-extensions==4.2.0 24 | Werkzeug==0.15.5 25 | WTForms==2.2.1 26 | zipp==3.8.0 27 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/2b121e25f167_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2b121e25f167 4 | Revises: 5 | Create Date: 2022-05-12 04:08:16.936651 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2b121e25f167' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('book', sa.Column('genre', sa.String(length=100), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('book', 'genre') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/a99c1165e476_private_messages.py: -------------------------------------------------------------------------------- 1 | """private messages 2 | 3 | Revision ID: a99c1165e476 4 | Revises: 9db4f46dd61b 5 | Create Date: 2022-05-16 02:06:43.851991 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a99c1165e476' 14 | down_revision = '9db4f46dd61b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('user', sa.Column('last_message_read_time', sa.DateTime(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('user', 'last_message_read_time') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /projeto/main/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, Blueprint, redirect, url_for 2 | from projeto.models import Book 3 | from sqlalchemy import or_ 4 | 5 | main = Blueprint('main', __name__) 6 | 7 | @main.route("/") 8 | @main.route("/home") 9 | def home(): 10 | page = request.args.get('page', 1, type=int) 11 | books = Book.query.order_by(Book.date_posted.desc()).paginate(page=page, per_page=5) 12 | return render_template('home.html', books=books) 13 | 14 | @main.route('/search/', methods=['GET']) 15 | def search(): 16 | page = request.args.get('page', 1, type=int) 17 | query = request.args.get('q') 18 | if not query: 19 | return redirect(url_for('main.home')) 20 | books = Book.query.filter(or_( 21 | Book.title.contains(query.title()), 22 | Book.author.contains(query.title()), 23 | Book.genre.contains(query.title()), 24 | Book.summary.contains(query))).paginate(page=page) 25 | return render_template('search.html', books=books, query=query) -------------------------------------------------------------------------------- /projeto/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from projeto.config import Config 4 | from flask_bcrypt import Bcrypt 5 | from flask_login import LoginManager 6 | from flask_migrate import Migrate 7 | 8 | db = SQLAlchemy() 9 | bcrypt = Bcrypt() 10 | migrate = Migrate() 11 | login_manager = LoginManager() 12 | login_manager.login_view = 'users.login' 13 | login_manager.login_message_category = 'info' 14 | 15 | def create_app(config_class=Config): 16 | app = Flask(__name__) 17 | app.config.from_object(Config) 18 | 19 | db.init_app(app) 20 | bcrypt.init_app(app) 21 | migrate.init_app(app, db) 22 | login_manager.init_app(app) 23 | 24 | from projeto.users.routes import users 25 | from projeto.books.routes import books 26 | from projeto.main.routes import main 27 | from projeto.errors.handlers import errors 28 | app.register_blueprint(users) 29 | app.register_blueprint(main) 30 | app.register_blueprint(books) 31 | app.register_blueprint(errors) 32 | 33 | return app -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrations/versions/46e80c86a0fb_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 46e80c86a0fb 4 | Revises: 5ef733a72780 5 | Create Date: 2022-05-14 05:28:36.082684 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '46e80c86a0fb' 14 | down_revision = '5ef733a72780' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('analysis', sa.Column('user_id', sa.Integer(), nullable=True)) 22 | op.create_foreign_key(None, 'analysis', 'user', ['user_id'], ['id']) 23 | op.alter_column('book', 'image_book', 24 | existing_type=sa.VARCHAR(length=20), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('book', 'image_book', 32 | existing_type=sa.VARCHAR(length=20), 33 | nullable=False) 34 | op.drop_constraint(None, 'analysis', type_='foreignkey') 35 | op.drop_column('analysis', 'user_id') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/5ef733a72780_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 5ef733a72780 4 | Revises: f3cdee093c3a 5 | Create Date: 2022-05-14 05:28:00.153209 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '5ef733a72780' 14 | down_revision = 'f3cdee093c3a' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('analysis', sa.Column('user_id', sa.Integer(), nullable=False)) 22 | op.create_foreign_key(None, 'analysis', 'user', ['user_id'], ['id']) 23 | op.alter_column('book', 'image_book', 24 | existing_type=sa.VARCHAR(length=20), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('book', 'image_book', 32 | existing_type=sa.VARCHAR(length=20), 33 | nullable=False) 34 | op.drop_constraint(None, 'analysis', type_='foreignkey') 35 | op.drop_column('analysis', 'user_id') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/e3f791d50878_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: e3f791d50878 4 | Revises: b16720f2bef5 5 | Create Date: 2022-05-14 05:22:06.702170 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e3f791d50878' 14 | down_revision = 'b16720f2bef5' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('analysis', sa.Column('user_id', sa.Integer(), nullable=False)) 22 | op.create_foreign_key(None, 'analysis', 'user', ['user_id'], ['id']) 23 | op.alter_column('book', 'image_book', 24 | existing_type=sa.VARCHAR(length=20), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('book', 'image_book', 32 | existing_type=sa.VARCHAR(length=20), 33 | nullable=False) 34 | op.drop_constraint(None, 'analysis', type_='foreignkey') 35 | op.drop_column('analysis', 'user_id') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/f3cdee093c3a_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f3cdee093c3a 4 | Revises: e3f791d50878 5 | Create Date: 2022-05-14 05:24:57.498088 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f3cdee093c3a' 14 | down_revision = 'e3f791d50878' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('analysis', sa.Column('user_id', sa.Integer(), nullable=False)) 22 | op.create_foreign_key(None, 'analysis', 'user', ['user_id'], ['id']) 23 | op.alter_column('book', 'image_book', 24 | existing_type=sa.VARCHAR(length=20), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('book', 'image_book', 32 | existing_type=sa.VARCHAR(length=20), 33 | nullable=False) 34 | op.drop_constraint(None, 'analysis', type_='foreignkey') 35 | op.drop_column('analysis', 'user_id') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/b16720f2bef5_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b16720f2bef5 4 | Revises: 2b121e25f167 5 | Create Date: 2022-05-14 01:32:03.430591 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b16720f2bef5' 14 | down_revision = '2b121e25f167' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('analysis', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('rating', sa.String(length=100), nullable=False), 24 | sa.Column('review', sa.Text(), nullable=False), 25 | sa.Column('book_id', sa.Integer(), nullable=False), 26 | sa.ForeignKeyConstraint(['book_id'], ['book.id'], ), 27 | sa.PrimaryKeyConstraint('id') 28 | ) 29 | op.alter_column('book', 'image_book', 30 | existing_type=sa.VARCHAR(length=20), 31 | nullable=True) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade(): 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.alter_column('book', 'image_book', 38 | existing_type=sa.VARCHAR(length=20), 39 | nullable=False) 40 | op.drop_table('analysis') 41 | # ### end Alembic commands ### 42 | -------------------------------------------------------------------------------- /projeto/books/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, SubmitField, TextAreaField, SelectField 3 | from flask_wtf.file import FileField, FileAllowed 4 | from wtforms.validators import DataRequired, Required 5 | 6 | GENRES = [ 7 | ('Secret Societies', 'Secret Societies'), 8 | ('Science Fiction', 'Science Fiction'), 9 | ('Spirituality', 'Spirituality',), 10 | ('Folk Tales', 'Folk Tales'), 11 | ('Psychology', 'Psychology'), 12 | ('Philosophy', 'Philosophy'), 13 | ('Dystopian', 'Dystopian'), 14 | ('Fantasy', 'Fantasy'), 15 | ('Horror', 'Horror'), 16 | ('Anime', 'Anime') 17 | ] 18 | 19 | class BookForm(FlaskForm): 20 | title = StringField('Title', validators=[DataRequired()]) 21 | author = StringField('Author', validators=[DataRequired()]) 22 | genre = SelectField('Genre', choices=GENRES, validators=[Required()]) 23 | summary = TextAreaField('Summary', validators=[DataRequired()]) 24 | image_book = FileField('Picture', validators=[FileAllowed(['jpg','png'])]) 25 | submit = SubmitField('Submit') 26 | 27 | CHOICES = [ 28 | ('Extraordinary', 'Extraordinary'), 29 | ('Excelent', 'Excelent'), 30 | ('Great', 'Great',), 31 | ('Good', 'Good') 32 | ] 33 | 34 | class AnalysisForm(FlaskForm): 35 | rating = SelectField('Rating', choices=CHOICES, validators=[Required()]) 36 | review = TextAreaField('Review', validators=[DataRequired()]) 37 | submit = SubmitField('Submit') -------------------------------------------------------------------------------- /migrations/versions/9db4f46dd61b_private_messages.py: -------------------------------------------------------------------------------- 1 | """private messages 2 | 3 | Revision ID: 9db4f46dd61b 4 | Revises: 46e80c86a0fb 5 | Create Date: 2022-05-16 01:53:35.196659 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '9db4f46dd61b' 14 | down_revision = '46e80c86a0fb' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('message', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('sender_id', sa.Integer(), nullable=True), 24 | sa.Column('recipient_id', sa.Integer(), nullable=True), 25 | sa.Column('body', sa.Text(), nullable=False), 26 | sa.Column('timestamp', sa.DateTime(), nullable=True), 27 | sa.ForeignKeyConstraint(['recipient_id'], ['user.id'], ), 28 | sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | op.create_index(op.f('ix_message_timestamp'), 'message', ['timestamp'], unique=False) 32 | op.create_foreign_key(None, 'analysis', 'user', ['user_id'], ['id']) 33 | op.alter_column('book', 'image_book', 34 | existing_type=sa.VARCHAR(length=20), 35 | nullable=True) 36 | op.add_column('user', sa.Column('last_message_read_time', sa.DateTime(), nullable=True)) 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_column('user', 'last_message_read_time') 43 | op.alter_column('book', 'image_book', 44 | existing_type=sa.VARCHAR(length=20), 45 | nullable=False) 46 | op.drop_constraint(None, 'analysis', type_='foreignkey') 47 | op.drop_index(op.f('ix_message_timestamp'), table_name='message') 48 | op.drop_table('message') 49 | # ### end Alembic commands ### 50 | -------------------------------------------------------------------------------- /projeto/templates/message.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Send Message to {{ recipient }}

6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ form.hidden_tag() }} 18 |
19 | Message to {{ recipient }} 20 |
21 | 22 | {% if form.message.errors %} 23 | {{ form.message(class="form-control form-control-lg is-invalid") }} 24 |
25 | {% for error in form.message.errors %} 26 | {{ error }} 27 | {% endfor %} 28 |
29 | {% else %} 30 | {{ form.message(class="form-control form-control-lg") }} 31 | {% endif %} 32 |
33 |
34 | {{ form.submit(class="btn btn-primary") }} 35 |
36 | {% if page == 'user' %} 37 | Cancel 38 | {% elif page == 'messages' %} 39 | Cancel 40 | {% endif %} 41 |
42 |
43 |
44 |
45 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/user_books.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Books by {{ user.username }}

6 | {% if user != current_user and current_user.is_authenticated %} 7 | Send Message 8 | {% endif %} 9 |
10 | {% for book in books.items %} 11 |
12 |
13 |
14 |

Title: {{ book.title }}

15 |
16 |

Author: {{ book.author }}


17 |

Genre: {{ book.genre }}


18 |

Summary: {{ book.summary }}


19 |
20 |

Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

21 |
22 |
23 |
24 |
25 | {% endfor %} 26 | {% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} 27 | {% if page_num %} 28 | {% if books.page == page_num %} 29 | {{ page_num }} 30 | {% else %} 31 | {{ page_num }} 32 | {% endif %} 33 | {% else %} 34 | 35 | {% endif %} 36 | {% endfor %} 37 |
38 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Secret Library

6 |
7 | 8 |
9 |

"Seja muito bem vindo à Grande Biblioteca Secreta!"

10 |

Conteúdo Dinâmico

11 | Desenvolvida com Python-Flask 12 |
13 |
14 |
15 |

Hall of Books

16 |
17 |
18 | 19 |

Search for Books

20 |

Search by: (Title, Author, Genre or Summary)

21 | 22 | 23 |
24 |
25 |

Results for {{ query }}

26 |
27 | {% for book in books.items %} 28 |

Title: {{ book.title }}


29 |

Author: {{ book.author }}


30 |

Genre: {{ book.genre }}


31 |
Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

32 |

Summary: {{ book.summary }}


33 |
34 |

Post by: {{ book.user.username }}


35 |
36 | {% endfor %} 37 |
38 |
39 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Login

6 |
7 | 8 |
9 |
10 | {{ form.hidden_tag() }} 11 |
12 | Login 13 |
14 | {{ form.email.label(class="form-control-label") }} 15 | 16 | 17 | {% if form.email.errors %} 18 | {{ form.email(class="form-control form-control-lg is-invalid") }} 19 |
20 | {% for error in form.email.errors %} 21 | {{ error }} 22 | {% endfor %} 23 |
24 | {% else %} 25 | {{ form.email(class="form-control form-control-lg") }} 26 | {% endif %} 27 |
28 |
29 | {{ form.password.label(class="form-control-label") }} 30 | 31 | 32 | {% if form.password.errors %} 33 | {{ form.password(class="form-control form-control-lg is-invalid") }} 34 |
35 | {% for error in form.password.errors %} 36 | {{ error }} 37 | {% endfor %} 38 |
39 | {% else %} 40 | {{ form.password(class="form-control form-control-lg") }} 41 | {% endif %} 42 |
43 |
44 | {{ form.remember(class="form-check-input") }} 45 | {{ form.remember.label(class="form-check-label") }} 46 |
47 |
48 | {{ form.submit(class="btn btn-primary") }} 49 |
50 | Cancel 51 |
52 |
53 |
54 |
55 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/users/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from flask_wtf.file import FileField, FileAllowed 3 | from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField 4 | from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError 5 | from flask_login import current_user 6 | from projeto.models import User 7 | 8 | class RegistrationForm(FlaskForm): 9 | username = StringField('Username', validators=[DataRequired(), Length(min=2,max=20)]) 10 | email = StringField('Email', validators=[DataRequired(), Email()]) 11 | password = PasswordField('Password', validators=[DataRequired()]) 12 | confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) 13 | submit = SubmitField('Sign Up') 14 | 15 | def validate_username(self, username): 16 | user = User.query.filter_by(username=username.data).first() 17 | if user: 18 | raise ValidationError('That username is taken. Please choose a different one') 19 | 20 | def validate_email(self, email): 21 | user = User.query.filter_by(email=email.data).first() 22 | if user: 23 | raise ValidationError('That email is taken. Please choose a different one') 24 | 25 | class LoginForm(FlaskForm): 26 | email = StringField('Email', validators=[DataRequired(), Email()]) 27 | password = PasswordField('Password', validators=[DataRequired()]) 28 | remember = BooleanField('Remember') 29 | submit = SubmitField('Login') 30 | 31 | class MessageForm(FlaskForm): 32 | message = TextAreaField('Message', validators=[DataRequired()]) 33 | submit = SubmitField('Submit') 34 | 35 | class UpdateAccountForm(FlaskForm): 36 | username = StringField('Username', validators=[DataRequired(), Length(min=2,max=20)]) 37 | email = StringField('Email', validators=[DataRequired(), Email()]) 38 | picture = FileField('Profile Picture', validators=[FileAllowed(['jpg','png'])]) 39 | submit = SubmitField('Update') 40 | 41 | def validate_username(self, username): 42 | if username.data != current_user.username: 43 | user = User.query.filter_by(username=username.data).first() 44 | if user: 45 | raise ValidationError('That username is taken. Please choose a different one') 46 | 47 | def validate_email(self, email): 48 | if email.data != current_user.email: 49 | user = User.query.filter_by(email=email.data).first() 50 | if user: 51 | raise ValidationError('That email is taken. Please choose a different one') -------------------------------------------------------------------------------- /projeto/templates/genre.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Secret Library

6 |
7 | 8 |
9 |

"Seja muito bem vindo à Grande Biblioteca Secreta!"

10 |

Conteúdo Dinâmico

11 | Desenvolvida com Python-Flask 12 |
13 |
14 |
15 |

{{ genre }} books

16 |
17 |
18 | 19 |

Search for Books

20 |

Search by: (Title, Author, Genre or Summary)

21 | 22 | 23 |
24 |
25 | {% for book in books.items %} 26 |

Title: {{ book.title }}


27 |

Author: {{ book.author }}


28 |

Genre: {{ book.genre }}


29 |
Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

30 |

Summary: {{ book.summary }}


31 |
32 |

Post by: {{ book.user.username }}


33 |
34 | {% endfor %} 35 |
36 | {% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} 37 | {% if page_num %} 38 | {% if books.page == page_num %} 39 | {{ page_num }} 40 | {% else %} 41 | {{ page_num }} 42 | {% endif %} 43 | {% else %} 44 | 45 | {% endif %} 46 | {% endfor %} 47 |
48 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/author.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Secret Library

6 |
7 | 8 |
9 |

"Seja muito bem vindo à Grande Biblioteca Secreta!"

10 |

Conteúdo Dinâmico

11 | Desenvolvida com Python-Flask 12 |
13 |
14 |
15 |

Books of {{ author }}

16 |
17 |
18 | 19 |

Search for Books

20 |

Search by: (Title, Author, Genre or Summary)

21 | 22 | 23 |
24 |
25 | {% for book in books.items %} 26 |

Title: {{ book.title }}


27 |

Author: {{ book.author }}


28 |

Genre: {{ book.genre }}


29 |
Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

30 |

Summary: {{ book.summary }}


31 |
32 |

Post by: {{ book.user.username }}


33 |
34 | {% endfor %} 35 |
36 | {% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} 37 | {% if page_num %} 38 | {% if books.page == page_num %} 39 | {{ page_num }} 40 | {% else %} 41 | {{ page_num }} 42 | {% endif %} 43 | {% else %} 44 | 45 | {% endif %} 46 | {% endfor %} 47 |
48 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/analysis.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Book Analysis {{ legend }}

6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ form.hidden_tag() }} 18 |
19 | Book Review {{ legend }} 20 |
21 |
22 | {{ form.rating(class="custom-file-input") }} 23 | {{ form.rating.label(class="custom-file-label") }} 24 |
25 | {% if form.rating.errors %} 26 | {% for error in form.rating.errors %} 27 | {{ error }}
28 | {% endfor %} 29 | {% endif %} 30 |
31 |
32 | {{ form.review.label(class="form-control-label") }} 33 | 34 | 35 | {% if form.review.errors %} 36 | {{ form.review(class="form-control form-control-lg is-invalid") }} 37 |
38 | {% for error in form.review.errors %} 39 | {{ error }} 40 | {% endfor %} 41 |
42 | {% else %} 43 | {{ form.review(class="form-control form-control-lg") }} 44 | {% endif %} 45 |
46 |
47 | {{ form.submit(class="btn btn-primary") }} 48 |
49 | Cancel 50 |
51 |
52 |
53 |
54 | {% endblock content %} 55 | 56 | {% block script %} 57 | 63 | {% endblock script %} -------------------------------------------------------------------------------- /projeto/templates/messages.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Messages of {{ current_user.username }} ({{ total_messages }})

6 | {% if messages %} 7 | 8 | {% endif %} 9 | {% for message in messages %} 10 |
11 | {% if message.author.username != current_user.username %} 12 |
13 | Message 14 |
15 | 16 |
17 |
18 | {% endif %} 19 | 20 |

{{ message.author.username }}

21 |

Message: {{ message.body }}

22 |

Sent in: {{ message.timestamp.strftime('%Y-%m-%d') }}

23 |
24 | {% endfor %} 25 | 26 | 27 | 28 | 29 | 30 | 48 |
49 | {% endblock content %} -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | 'sqlalchemy.url', 25 | str(current_app.extensions['migrate'].db.get_engine().url).replace( 26 | '%', '%%')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = current_app.extensions['migrate'].db.get_engine() 75 | 76 | with connectable.connect() as connection: 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | **current_app.extensions['migrate'].configure_args 81 | ) 82 | 83 | with context.begin_transaction(): 84 | context.run_migrations() 85 | 86 | 87 | if context.is_offline_mode(): 88 | run_migrations_offline() 89 | else: 90 | run_migrations_online() 91 | -------------------------------------------------------------------------------- /projeto/models.py: -------------------------------------------------------------------------------- 1 | from projeto import db, login_manager 2 | from flask import current_app 3 | from datetime import datetime 4 | from flask_login import UserMixin 5 | 6 | @login_manager.user_loader 7 | def load_user(user_id): 8 | return User.query.get(int(user_id)) 9 | 10 | class User(db.Model, UserMixin): 11 | id = db.Column(db.Integer, primary_key=True) 12 | username = db.Column(db.String(20), unique=True, nullable=False) 13 | email = db.Column(db.String(120), unique=True, nullable=False) 14 | image_file = db.Column(db.String(20), default='default.jpg') 15 | password = db.Column(db.String(60), nullable=False) 16 | analysis = db.relationship('Analysis', backref='user', lazy=True) 17 | Book = db.relationship('Book', backref='user', lazy=True) 18 | messages_sent = db.relationship('Message', 19 | foreign_keys='Message.sender_id', 20 | backref='author', lazy='dynamic') 21 | messages_received = db.relationship('Message', 22 | foreign_keys='Message.recipient_id', 23 | backref='recipient', lazy='dynamic') 24 | last_message_read_time = db.Column(db.DateTime) 25 | 26 | def new_messages(self): 27 | last_read_time = self.last_message_read_time or datetime(1900, 1, 1) 28 | return Message.query.filter_by(recipient=self).filter( 29 | Message.timestamp > last_read_time).count() 30 | 31 | def __repr__(self): 32 | return f"User('{self.username}', '{self.email}', '{self.image_file}')" 33 | 34 | class Book(db.Model): 35 | id = db.Column(db.Integer, primary_key=True) 36 | title = db.Column(db.String(100), nullable=False) 37 | author = db.Column(db.String(100), nullable=False) 38 | genre = db.Column(db.String(100), nullable=True) 39 | date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) 40 | summary = db.Column(db.Text, nullable=False) 41 | image_book = db.Column(db.String(20), nullable=True, default='book_default.jpg') 42 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 43 | analysis = db.relationship('Analysis', backref='book', lazy=True) 44 | 45 | def __repr__(self): 46 | return f"Book('{self.title}', '{self.date_posted}')" 47 | 48 | class Analysis(db.Model): 49 | id = db.Column(db.Integer, primary_key=True) 50 | rating = db.Column(db.String(100), nullable=False) 51 | review = db.Column(db.Text, nullable=False) 52 | book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False) 53 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) 54 | 55 | def __repr__(self): 56 | return f"Analysis('{self.user_id}', '{self.rating}')" 57 | 58 | class Message(db.Model): 59 | id = db.Column(db.Integer, primary_key=True) 60 | sender_id = db.Column(db.Integer, db.ForeignKey('user.id')) 61 | recipient_id = db.Column(db.Integer, db.ForeignKey('user.id')) 62 | body = db.Column(db.Text, nullable=False) 63 | timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) 64 | 65 | def __repr__(self): 66 | return ''.format(self.body) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Library 2 | 3 | ![img](/projeto/static/img/Avatar.png) 4 | 5 | A simple library prototype using [Python](https://www.python.org/) and [Flask](https://flask.palletsprojects.com/en/2.1.x/). 6 | 7 | ## Features 8 | 9 | - Create Books 10 | - Search Books 11 | - Delete Books 12 | - Update Books 13 | - Review Books 14 | - Message System 15 | - User Registration & Login 16 | - Account Info & Update 17 | 18 | ## TODO 19 | 20 | - Create a table for Authors. 21 | - Create the interface for Authors. 22 | 23 | ## Installation 24 | 25 | ### Clone the Repository 26 | 27 | ``` 28 | git clone https://github.com/the-akira/Flask-Library.git 29 | ``` 30 | 31 | ### Inside the Main Directory 32 | 33 | Create a Virtual Environment: 34 | 35 | ``` 36 | python -m venv myvenv 37 | ``` 38 | 39 | Activate the Virtual Environment: 40 | 41 | ``` 42 | source myvenv/bin/activate 43 | ``` 44 | 45 | Install Requirements: 46 | 47 | ``` 48 | pip install -r requirements.txt 49 | ``` 50 | 51 | Run the Application: 52 | 53 | ``` 54 | python run.py 55 | ``` 56 | 57 | Open your Web Browser and navigate to `http://127.0.0.1:5000/`. 58 | 59 | ## Managing the Database 60 | 61 | ### Inside the Main Directory 62 | 63 | Start a new [Python REPL](https://python.land/introduction-to-python/the-repl) in your terminal: 64 | 65 | ``` 66 | python 67 | ``` 68 | 69 | Creating a new database: 70 | 71 | ```python 72 | >>> from projeto import db 73 | >>> db.create_all() 74 | ``` 75 | 76 | Initiating a new app context: 77 | 78 | ```python 79 | >>> from projeto import create_app 80 | >>> app = create_app() 81 | >>> app.app_context().push() 82 | ``` 83 | 84 | Importing the database models: 85 | 86 | ```python 87 | >>> from projeto.models import User, Book, Analysis 88 | ``` 89 | 90 | Inserting a new user in the database: 91 | 92 | ```python 93 | user = User( 94 | username='talantyr', 95 | email='talantyr@gmail.com', 96 | image_file='default.jpg', 97 | password='22447755' 98 | ) 99 | db.session.add(user) 100 | db.session.commit() 101 | ``` 102 | 103 | Querying for all users in the database: 104 | 105 | ```python 106 | >>> users = User.query.all() 107 | >>> [user.email for user in users] 108 | ``` 109 | 110 | Search for a user with a specific ID: 111 | 112 | ```python 113 | >>> User.query.get(1) 114 | ``` 115 | 116 | Order users by email ascending: 117 | 118 | ```python 119 | >>> users = User.query.order_by(User.email.asc()) 120 | >>> [user for user in users] 121 | ``` 122 | 123 | Search for a specific user in the database: 124 | 125 | ```python 126 | >>> users = User.query.filter(User.username.contains('aki')) 127 | >>> [user for user in users] 128 | ``` 129 | 130 | Add a new book to the database with the current user: 131 | 132 | ```python 133 | book = Book( 134 | title='Quincas Borba', 135 | author='Machado de Assis', 136 | genre='História', 137 | summary='Clássico Brasileiro', 138 | user=user 139 | ) 140 | db.session.add(book) 141 | db.session.commit() 142 | ``` 143 | 144 | Create a review for the book: 145 | 146 | ```python 147 | review = Analysis( 148 | rating='Muito bom!', 149 | review='Um livro altamente incrível', 150 | book_id=book.id, 151 | user=user 152 | ) 153 | db.session.add(review) 154 | db.session.commit() 155 | ``` 156 | 157 | Have a good read! 158 | -------------------------------------------------------------------------------- /projeto/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Register

6 |
7 | 8 |
9 |
10 | {{ form.hidden_tag() }} 11 |
12 | Join with us 13 |
14 | {{ form.username.label(class="form-control-label") }} 15 | 16 | 17 | {% if form.username.errors %} 18 | {{ form.username(class="form-control form-control-lg is-invalid") }} 19 |
20 | {% for error in form.username.errors %} 21 | {{ error }} 22 | {% endfor %} 23 |
24 | {% else %} 25 | {{ form.username(class="form-control form-control-lg") }} 26 | {% endif %} 27 |
28 |
29 | {{ form.email.label(class="form-control-label") }} 30 | 31 | 32 | {% if form.email.errors %} 33 | {{ form.email(class="form-control form-control-lg is-invalid") }} 34 |
35 | {% for error in form.email.errors %} 36 | {{ error }} 37 | {% endfor %} 38 |
39 | {% else %} 40 | {{ form.email(class="form-control form-control-lg") }} 41 | {% endif %} 42 |
43 |
44 | {{ form.password.label(class="form-control-label") }} 45 | 46 | 47 | {% if form.password.errors %} 48 | {{ form.password(class="form-control form-control-lg is-invalid") }} 49 |
50 | {% for error in form.password.errors %} 51 | {{ error }} 52 | {% endfor %} 53 |
54 | {% else %} 55 | {{ form.password(class="form-control form-control-lg") }} 56 | {% endif %} 57 |
58 |
59 | {{ form.confirm_password.label(class="form-control-label") }} 60 | 61 | 62 | {% if form.confirm_password.errors %} 63 | {{ form.confirm_password(class="form-control form-control-lg is-invalid") }} 64 |
65 | {% for error in form.confirm_password.errors %} 66 | {{ error }} 67 | {% endfor %} 68 |
69 | {% else %} 70 | {{ form.confirm_password(class="form-control form-control-lg") }} 71 | {% endif %} 72 |
73 |
74 | {{ form.submit(class="btn btn-primary mt-2") }} 75 |
76 | Cancel 77 |
78 |
79 |
80 |
81 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | The Secret Library 13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | 34 |
35 | {% with messages = get_flashed_messages(with_categories=true) %} 36 | {% if messages %} 37 | {% for category, message in messages %} 38 | 44 | {% endfor %} 45 | {% endif %} 46 | {% endwith %} 47 |
48 | {% block content %}{% endblock %} 49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | {% block script %}{% endblock script %} 58 | 59 | -------------------------------------------------------------------------------- /projeto/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Secret Library

6 |
7 | 8 |
9 |

"Seja muito bem vindo à Grande Biblioteca Secreta!"

10 |

Conteúdo Dinâmico

11 | Desenvolvida com Python-Flask 12 |
13 |
14 |
15 |

Hall of Books

16 |
17 |
18 | 19 |

Search for Books

20 |

Search by: (Title, Author, Genre or Summary)

21 | 22 | 23 |
24 | 25 |

Books by Genre

26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | {% for book in books.items %} 40 |

Title: {{ book.title }}


41 |

Author: {{ book.author }}


42 |

Genre: {{ book.genre }}


43 |
Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

44 |

Summary: {{ book.summary }}


45 |
46 |

Book posted by: {{ book.user.username }}


47 |
48 | {% endfor %} 49 |
50 | {% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} 51 | {% if page_num %} 52 | {% if books.page == page_num %} 53 | {{ page_num }} 54 | {% else %} 55 | {{ page_num }} 56 | {% endif %} 57 | {% else %} 58 | 59 | {% endif %} 60 | {% endfor %} 61 |
62 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/book.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Secret Library

6 |
7 | 8 |
9 |

"Seja muito bem vindo à Grande Biblioteca Secreta!"

10 |

Conteúdo Dinâmico

11 | Desenvolvida com Python-Flask 12 |
13 |
14 |
15 |
16 |

{{ book.title }}

17 |
18 | {% if book.user == current_user %} 19 |
20 | Update 21 | 22 | Analyze 23 | Information 24 |
25 | {% else %} 26 |
27 | {% if current_user.is_authenticated %} 28 | Analyze 29 | {% endif %} 30 | Information 31 |
32 | {% endif %} 33 |
34 |

Author: {{ book.author }}


35 |

Genre: {{ book.genre }}


36 |
Posted in: {{ book.date_posted.strftime('%Y-%m-%d') }}

37 |

Summary: {{ book.summary }}


38 |
39 |

Post by: {{ book.user.username }}


40 |
41 |
42 | {% if analysis %} 43 |

Reviews

44 | {% endif %} 45 | {% for analyse in analysis %} 46 |
47 | {% if analyse.user == current_user %} 48 |
49 | Update 50 |
51 | 52 |
53 |
54 | {% endif %} 55 | 56 |

{{ analyse.user.username }}

57 |

Rating: {{ analyse.rating }}

58 |

Review: {{ analyse.review }}

59 |
60 | {% endfor %} 61 | 62 | 80 | {% endblock content %} -------------------------------------------------------------------------------- /projeto/templates/create_book.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

{{ legend }}

6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ form.hidden_tag() }} 18 |
19 | {{ legend }} 20 |
21 | {{ form.title.label(class="form-control-label") }} 22 | 23 | 24 | {% if form.title.errors %} 25 | {{ form.title(class="form-control form-control-lg is-invalid") }} 26 |
27 | {% for error in form.title.errors %} 28 | {{ error }} 29 | {% endfor %} 30 |
31 | {% else %} 32 | {{ form.title(class="form-control form-control-lg") }} 33 | {% endif %} 34 |
35 |
36 | {{ form.author.label(class="form-control-label") }} 37 | 38 | 39 | {% if form.author.errors %} 40 | {{ form.author(class="form-control form-control-lg is-invalid") }} 41 |
42 | {% for error in form.author.errors %} 43 | {{ error }} 44 | {% endfor %} 45 |
46 | {% else %} 47 | {{ form.author(class="form-control form-control-lg") }} 48 | {% endif %} 49 |
50 |
51 |
52 | {{ form.genre(class="custom-file-input") }} 53 | {{ form.genre.label(class="custom-file-label") }} 54 |
55 | {% if form.genre.errors %} 56 | {% for error in form.genre.errors %} 57 | {{ error }}
58 | {% endfor %} 59 | {% endif %} 60 |
61 |
62 | {{ form.summary.label(class="form-control-label") }} 63 | 64 | 65 | {% if form.summary.errors %} 66 | {{ form.summary(class="form-control form-control-lg is-invalid") }} 67 |
68 | {% for error in form.summary.errors %} 69 | {{ error }} 70 | {% endfor %} 71 |
72 | {% else %} 73 | {{ form.summary(class="form-control form-control-lg") }} 74 | {% endif %} 75 |
76 |
77 |
78 |
79 | {{ form.image_book(class="custom-file-input") }} 80 | {{ form.image_book.label(class="custom-file-label") }} 81 |
82 | {% if form.image_book.errors %} 83 | {% for error in form.image_book.errors %} 84 | {{ error }}
85 | {% endfor %} 86 | {% endif %} 87 |
88 |
89 | {{ form.submit(class="btn btn-primary") }} 90 |
91 | {% if session.update %} 92 | Cancel 93 | {% else %} 94 | Cancel 95 | {% endif %} 96 |
97 |
98 |
99 |
100 | {% endblock content %} 101 | 102 | {% block script %} 103 | 113 | {% endblock script %} -------------------------------------------------------------------------------- /projeto/books/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import render_template, url_for, flash, redirect, request, abort, Blueprint, session 3 | from flask_login import current_user, login_required 4 | from projeto import db 5 | from projeto.models import Book, Analysis 6 | from projeto.books.forms import BookForm, AnalysisForm 7 | from projeto.utils.utils import save_picture 8 | 9 | books = Blueprint('books', __name__) 10 | 11 | @books.route("/book/new", methods=["GET", "POST"]) 12 | @login_required 13 | def new_book(): 14 | form = BookForm() 15 | if form.validate_on_submit(): 16 | if form.image_book.data: 17 | picture_file = save_picture(form.image_book.data, 'static/book', 300, 480) 18 | book = Book( 19 | title=form.title.data.strip(), 20 | author=form.author.data.strip(), 21 | genre=form.genre.data, 22 | summary=form.summary.data, 23 | image_book=picture_file, 24 | user=current_user 25 | ) 26 | db.session.add(book) 27 | db.session.commit() 28 | else: 29 | book = Book( 30 | title=form.title.data.strip(), 31 | author=form.author.data.strip(), 32 | genre=form.genre.data, 33 | summary=form.summary.data, 34 | user=current_user 35 | ) 36 | db.session.add(book) 37 | db.session.commit() 38 | flash('Your book has been added!', 'success') 39 | return redirect(url_for('main.home')) 40 | session.update = False 41 | return render_template( 42 | 'create_book.html', 43 | title='New Book', 44 | form=form, 45 | legend='New Book', 46 | update=session.update 47 | ) 48 | 49 | @books.route("/book/") 50 | def book(book_id): 51 | book = Book.query.get_or_404(book_id) 52 | analysis = Book.query.get(book_id).analysis 53 | return render_template('book.html', book=book, analysis=analysis) 54 | 55 | @books.route("/author/") 56 | def author(author): 57 | page = request.args.get('page', 1, type=int) 58 | books = Book.query.filter(Book.author.contains(author.strip())).paginate(page=page, per_page=5) 59 | return render_template('author.html', books=books, author=author) 60 | 61 | @books.route("/genre/") 62 | def genre(genre): 63 | page = request.args.get('page', 1, type=int) 64 | books = Book.query.filter(Book.genre.contains(genre)).paginate(page=page, per_page=5) 65 | return render_template('genre.html', books=books, genre=genre) 66 | 67 | @books.route("/analysis/", methods=["GET", "POST"]) 68 | @login_required 69 | def analysis(book_id): 70 | form = AnalysisForm() 71 | book = Book.query.get_or_404(book_id) 72 | if form.validate_on_submit(): 73 | analysis = Analysis( 74 | rating=form.rating.data, 75 | review=form.review.data, 76 | book_id=book_id, 77 | user=current_user 78 | ) 79 | db.session.add(analysis) 80 | db.session.commit() 81 | flash('Your review has been added!', 'success') 82 | return redirect(url_for('books.book', book_id=book_id)) 83 | return render_template('analysis.html', form=form, book_id=book_id) 84 | 85 | @books.route("/analysis///delete", methods=["POST"]) 86 | @login_required 87 | def delete_analysis(analysis_id, book_id): 88 | analysis = Analysis.query.get_or_404(analysis_id) 89 | book = Book.query.get_or_404(book_id) 90 | if analysis.user != current_user: 91 | abort(403) 92 | db.session.delete(analysis) 93 | db.session.commit() 94 | flash('Your review has been deleted', 'success') 95 | return redirect(url_for('books.book', book_id=book.id)) 96 | 97 | @books.route("/book//delete", methods=["POST"]) 98 | @login_required 99 | def delete_book(book_id): 100 | book = Book.query.get_or_404(book_id) 101 | if book.user != current_user: 102 | abort(403) 103 | db.session.delete(book) 104 | db.session.commit() 105 | flash('Your book has been deleted', 'success') 106 | return redirect(url_for('main.home')) 107 | 108 | @books.route("/book//update", methods=["GET", "POST"]) 109 | @login_required 110 | def update_book(book_id): 111 | book = Book.query.get_or_404(book_id) 112 | if book.user != current_user: 113 | abort(403) 114 | form = BookForm() 115 | if form.validate_on_submit(): 116 | if form.image_book.data: 117 | picture_file = save_picture(form.image_book.data, 'static/book', 300, 480) 118 | book.title = form.title.data.strip() 119 | book.author = form.author.data.strip() 120 | book.genre = form.genre.data 121 | book.summary = form.summary.data 122 | book.image_book = picture_file 123 | db.session.commit() 124 | else: 125 | book.title = form.title.data.strip() 126 | book.author = form.author.data.strip() 127 | book.genre = form.genre.data 128 | book.summary = form.summary.data 129 | db.session.commit() 130 | flash('Your book has been updated', 'success') 131 | return redirect(url_for('books.book', book_id=book.id)) 132 | elif request.method == 'GET': 133 | session.update = True 134 | form.title.data = book.title 135 | form.author.data = book.author 136 | form.genre.data = book.genre 137 | form.summary.data = book.summary 138 | form.image_book.data = book.image_book 139 | return render_template( 140 | 'create_book.html', 141 | form=form, 142 | legend='Update Book', 143 | update=session.update, 144 | book_id=book_id 145 | ) 146 | 147 | @books.route("/analysis///update", methods=["GET", "POST"]) 148 | @login_required 149 | def update_analysis(analysis_id, book_id): 150 | book = Book.query.get_or_404(book_id) 151 | analysis = Analysis.query.get_or_404(analysis_id) 152 | if analysis.user != current_user: 153 | abort(403) 154 | form = AnalysisForm() 155 | if form.validate_on_submit(): 156 | analysis.rating = form.rating.data 157 | analysis.review = form.review.data 158 | db.session.commit() 159 | flash('Analysis has been updated', 'success') 160 | return redirect(url_for('books.book', book_id=book.id)) 161 | elif request.method == 'GET': 162 | form.rating.data = analysis.rating 163 | form.review.data = analysis.review 164 | return render_template('analysis.html', form=form, book_id=book_id, legend='Update') -------------------------------------------------------------------------------- /projeto/users/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, url_for, flash, redirect, request, Blueprint, abort 2 | from flask_login import login_user, current_user, logout_user, login_required 3 | from projeto import db, bcrypt 4 | from projeto.models import User, Book, Message 5 | from projeto.users.forms import RegistrationForm, LoginForm, UpdateAccountForm, MessageForm 6 | from projeto.utils.utils import save_picture 7 | from collections import Counter 8 | from datetime import datetime 9 | 10 | users = Blueprint('users', __name__) 11 | 12 | @users.route("/register", methods=["GET", "POST"]) # Permite os métodos GET e POST para essa rota 13 | def register(): 14 | if current_user.is_authenticated: 15 | return redirect(url_for('main.home')) 16 | form = RegistrationForm() 17 | if form.validate_on_submit(): 18 | hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') 19 | user = User(username=form.username.data, email=form.email.data, password=hashed_password) 20 | db.session.add(user) 21 | db.session.commit() 22 | flash(f'Your account has been created! You are now able to log in', 'success') 23 | return redirect(url_for('users.login')) 24 | return render_template('register.html', title='Register', form=form) # Passamos a instância do form para o nosso template 25 | 26 | @users.route("/login", methods=["GET", "POST"]) 27 | def login(): 28 | if current_user.is_authenticated: 29 | return redirect(url_for('main.home')) 30 | form = LoginForm() 31 | if form.validate_on_submit(): 32 | user = User.query.filter_by(email=form.email.data).first() 33 | if user and bcrypt.check_password_hash(user.password, form.password.data): 34 | login_user(user, remember=form.remember.data) 35 | next_page = request.args.get('next') 36 | flash('Login Successful!', 'success') 37 | return redirect(next_page) if next_page else redirect(url_for('main.home')) 38 | else: 39 | flash('Login Unsuccessful. Please check email and password', 'danger') 40 | return render_template('login.html', title='Login', form=form) # Passamos a instância do form para o nosso template 41 | 42 | @users.route("/logout") 43 | def logout(): 44 | flash(f'{current_user.username} logged out.', 'success') 45 | logout_user() 46 | return redirect(url_for('main.home')) 47 | 48 | @users.route("/account", methods=["GET", "POST"]) 49 | @login_required 50 | def account(): 51 | form = UpdateAccountForm() 52 | if form.validate_on_submit(): 53 | if form.picture.data: 54 | picture_file = save_picture(form.picture.data, 'static/profile', 350, 350) 55 | current_user.image_file = picture_file 56 | current_user.username = form.username.data 57 | current_user.email = form.email.data 58 | db.session.commit() 59 | flash('Your account has been updated', 'success') 60 | return redirect(url_for('users.account')) 61 | elif request.method == 'GET': 62 | form.username.data = current_user.username 63 | form.email.data = current_user.email 64 | image_file = url_for('static', filename='profile/' + current_user.image_file) 65 | books = sorted([(book.id, book.author, book.title, book.genre, book.date_posted, len(book.analysis)) for book in current_user.Book], key=lambda book: book[2]) 66 | total_analysis = len(current_user.analysis) 67 | total_books = len(books) 68 | books_author = dict(Counter(sorted([book.author for book in current_user.Book]))) 69 | books_genre = dict(Counter([book.genre for book in current_user.Book]).most_common()) 70 | return render_template( 71 | 'account.html', 72 | title='Account', 73 | image_file=image_file, 74 | form=form, 75 | books=books, 76 | total_books=total_books, 77 | total_analysis=total_analysis, 78 | books_author=books_author, 79 | books_genre=books_genre 80 | ) 81 | 82 | @users.route("/user/") 83 | def user_book(username): 84 | page = request.args.get('page', 1, type=int) 85 | user = User.query.filter_by(username=username).first_or_404() 86 | books = Book.query.filter_by(user=user).order_by(Book.date_posted.desc()).paginate(page=page, per_page=5) 87 | return render_template('user_books.html', books=books, user=user) 88 | 89 | @users.route('/send_message//', methods=['GET', 'POST']) 90 | @login_required 91 | def send_message(recipient, page): 92 | user = User.query.filter_by(username=recipient).first_or_404() 93 | form = MessageForm() 94 | if form.validate_on_submit(): 95 | msg = Message(author=current_user, recipient=user, 96 | body=form.message.data) 97 | db.session.add(msg) 98 | db.session.commit() 99 | flash(f'Your message has been sent to {user.username}', 'success') 100 | if page == 'messages': 101 | return redirect(url_for('users.messages')) 102 | elif page == 'user': 103 | return redirect(url_for('users.user_book', username=recipient)) 104 | return render_template('message.html', form=form, recipient=recipient, page=page) 105 | 106 | @users.route('/messages') 107 | @login_required 108 | def messages(): 109 | current_user.last_message_read_time = datetime.utcnow() 110 | db.session.commit() 111 | page = request.args.get('page', 1, type=int) 112 | messages = current_user.messages_received.order_by( 113 | Message.timestamp.desc()).paginate(page, 5, False) 114 | next_url = url_for('users.messages', page=messages.next_num) \ 115 | if messages.has_next else None 116 | prev_url = url_for('users.messages', page=messages.prev_num) \ 117 | if messages.has_prev else None 118 | return render_template('messages.html', messages=messages.items, 119 | next_url=next_url, prev_url=prev_url, total_messages=messages.total) 120 | 121 | @users.route("/message//delete", methods=["POST"]) 122 | @login_required 123 | def delete_message(message_id): 124 | message = Message.query.get_or_404(message_id) 125 | if message.author == current_user: 126 | abort(403) 127 | db.session.delete(message) 128 | db.session.commit() 129 | flash('Message deleted', 'success') 130 | return redirect(url_for('users.messages')) 131 | 132 | @users.route("/message/delete_all", methods=["POST"]) 133 | @login_required 134 | def delete_all_messages(): 135 | messages = Message.query.filter_by(recipient_id=current_user.id).all() 136 | for message in messages: 137 | if message.author == current_user: 138 | abort(403) 139 | db.session.delete(message) 140 | db.session.commit() 141 | flash('All Messages deleted', 'success') 142 | return redirect(url_for('users.messages')) -------------------------------------------------------------------------------- /projeto/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | background-image: url(/static/img/dark.png); 4 | color: #908d94; 5 | } 6 | 7 | .nav { 8 | padding: 20px; 9 | border: 1px solid white; 10 | margin-top: 15px; 11 | background-color: #222222; 12 | } 13 | 14 | a { 15 | color: white !important; 16 | } 17 | 18 | strong { 19 | color: white; 20 | } 21 | 22 | b { 23 | color: black; 24 | } 25 | 26 | .container { 27 | padding: 20px; 28 | margin-top: 20px; 29 | background-color: #100109; 30 | margin-bottom: 20px; 31 | } 32 | 33 | .caixa-principal { 34 | border: 1px solid white; 35 | box-shadow: 5px 5px 5px #222222; 36 | border-radius: 14px; 37 | } 38 | 39 | .titulo { 40 | text-shadow: 2px 2px #222222, 0 0 4px black; 41 | color: #908d94; 42 | font-weight: bold; 43 | font-style: oblique; 44 | } 45 | 46 | .list-group-item { 47 | margin-bottom: 4px; 48 | background-color: #908d94; 49 | } 50 | 51 | .list-group-item:hover { 52 | background-color: black; 53 | cursor: pointer; 54 | } 55 | 56 | hr { 57 | background-color: white; 58 | } 59 | 60 | .btn-primary { 61 | margin-top: 10px; 62 | background-color: gray !important; 63 | border: 1px solid white !important; 64 | } 65 | 66 | .btn-primary:hover { 67 | opacity: 0.85; 68 | } 69 | 70 | .btn-primary:focus { 71 | box-shadow: 0 0 0 0.1rem rgb(255 255 255 / 50%) !important; 72 | } 73 | 74 | .form-control:focus { 75 | box-shadow: 0 0 0 0.1rem rgb(255 255 255 / 50%) !important; 76 | } 77 | 78 | .btn-success:hover { 79 | cursor: pointer; 80 | } 81 | 82 | .btn-info { 83 | margin: 0.25rem; 84 | } 85 | 86 | .btn-info:hover { 87 | cursor: pointer; 88 | } 89 | 90 | .page-current { 91 | background-color:#222222 !important; 92 | } 93 | 94 | .livro { 95 | color: white; 96 | } 97 | 98 | .skull { 99 | display: block; 100 | margin-left: auto; 101 | margin-right: auto; 102 | margin-bottom: 8px; 103 | width: 500px; 104 | height: auto; 105 | opacity: 0.7 !important; 106 | border-radius: 10px !important; 107 | } 108 | 109 | .account-img { 110 | display: block; 111 | margin-left: auto; 112 | margin-right: auto; 113 | width: 250px; 114 | height: 250px; 115 | opacity: 0.7 !important; 116 | } 117 | 118 | .center { 119 | display: block; 120 | margin-left: auto; 121 | margin-right: auto; 122 | width: 50%; 123 | } 124 | 125 | .list-group-horizontal .list-group-item { 126 | margin-bottom: 4px !important; 127 | } 128 | 129 | form { 130 | padding: 20px; 131 | border: 1px solid #908d94; 132 | opacity: 0.8; 133 | box-shadow: 5px 6px 6px #222222; 134 | border-radius: 10px; 135 | background-image: url(/static/img/dark.png); 136 | } 137 | 138 | input { 139 | width: 100%; 140 | padding: 6px 12px; 141 | border-radius: 10px; 142 | } 143 | 144 | textarea { 145 | resize: none; 146 | display: block; 147 | height: 160px !important; 148 | } 149 | 150 | .table { 151 | color: #908d94 !important; 152 | } 153 | 154 | .modal-content { 155 | background-color: #100109 !important; 156 | } 157 | 158 | .delete-form { 159 | border: none !important; 160 | box-shadow: none !important; 161 | } 162 | 163 | .close-modal { 164 | color: white !important; 165 | } 166 | 167 | .table-icon { 168 | margin-bottom: 0; 169 | } 170 | 171 | .icons { 172 | margin-top: 12px; 173 | margin-bottom: 12px; 174 | display: block; 175 | margin-left: auto; 176 | margin-right: auto; 177 | width: 60%; 178 | text-align: center; 179 | } 180 | 181 | .btn-warning { 182 | background-color: #cc5c3d !important; 183 | width: 100% !important; 184 | border: 1px solid white !important 185 | } 186 | 187 | .btn-warning:hover { 188 | cursor: pointer; 189 | opacity: 0.85; 190 | } 191 | 192 | i { 193 | margin-right: 14px; 194 | color: #908d94; 195 | } 196 | 197 | ::-webkit-scrollbar { 198 | width: 20px; 199 | } 200 | 201 | ::-webkit-scrollbar-track { 202 | box-shadow: inset 0 0 25px #908d94; 203 | } 204 | 205 | ::-webkit-scrollbar-thumb { 206 | background: #100109; 207 | border-radius: 0.25rem; 208 | border: 2px solid white; 209 | } 210 | 211 | ::-webkit-scrollbar-thumb:hover { 212 | background: #908d94; 213 | } 214 | 215 | .date { 216 | font-size: 1.1rem; 217 | } 218 | 219 | .genres { 220 | display: flex; 221 | flex-wrap: wrap; 222 | align-items: center; 223 | justify-content: center; 224 | padding: 20px; 225 | border: 1px solid #908d94; 226 | opacity: 0.8; 227 | box-shadow: 5px 6px 6px #222222; 228 | border-radius: 10px; 229 | background-image: url(/static/img/dark.png); 230 | } 231 | 232 | .genres > h2 { 233 | text-align: center; 234 | } 235 | 236 | .image-genre { 237 | border: 1px solid #dee2e6; 238 | margin: 15px 15px 15px 15px; 239 | border-radius: 10px; 240 | width: 165px; 241 | height: 165px; 242 | } 243 | 244 | .image-genre:hover { 245 | border: 1px solid #dee2e6; 246 | opacity: 0.8; 247 | cursor: pointer; 248 | } 249 | 250 | .fa-user { 251 | margin-right: 0; 252 | } 253 | 254 | .fa-envelope-square { 255 | margin-right: 0; 256 | } 257 | 258 | .table { 259 | background-image: url(/static/img/dark.png); 260 | } 261 | 262 | tr:hover { 263 | background-color: black; 264 | } 265 | 266 | .review { 267 | padding: 20px; 268 | border: 1px solid #908d94; 269 | opacity: 0.8; 270 | box-shadow: 5px 6px 6px #222222; 271 | border-radius: 10px; 272 | background-image: url(/static/img/dark.png); 273 | } 274 | 275 | .delete-review { 276 | padding: 0; 277 | border: none; 278 | box-shadow: none; 279 | opacity: 1; 280 | } 281 | 282 | .delete-review > input { 283 | width: 80px; 284 | margin-bottom: 10px; 285 | margin-left: 8px; 286 | float: right; 287 | } 288 | 289 | .delete-message { 290 | padding: 0; 291 | border: none; 292 | box-shadow: none; 293 | opacity: 1; 294 | } 295 | 296 | .delete-message > input { 297 | width: 80px; 298 | margin-bottom: 10px; 299 | margin-left: 8px; 300 | } 301 | 302 | .update-review { 303 | width: 80px; 304 | margin-bottom: 10px; 305 | margin-right: 8px; 306 | float: left; 307 | } 308 | 309 | .message-btn, .delete-btn { 310 | width: 95px !important; 311 | } 312 | 313 | .btn-light:hover { 314 | opacity: 0.9; 315 | } 316 | 317 | .btn-light:focus { 318 | box-shadow: 0 0 0 0.2rem rgb(94 18 224 / 50%) !important; 319 | } 320 | 321 | .avatar-review { 322 | width: 100px; 323 | height: 100px; 324 | opacity: 0.7 !important; 325 | background-color: #fff; 326 | border: 3px solid white; 327 | margin-bottom: 7px; 328 | border-radius: 50%; 329 | margin-top: 3px; 330 | } 331 | 332 | #tableAllBooks, 333 | #tableAllAuthors, 334 | #tableAllGenres, 335 | #tableTotalReviews { 336 | display: none; 337 | } 338 | 339 | #buttonAllBooks, 340 | #buttonAllAuthors, 341 | #buttonAllGenres, 342 | #buttonTotalReviews { 343 | width: 150px; 344 | } 345 | 346 | @media only screen and (max-width: 768px) { 347 | .skull { 348 | width: 80%; 349 | } 350 | .movie-title { 351 | font-size: 1.6rem; 352 | } 353 | .table-date { 354 | display: none; 355 | } 356 | } 357 | 358 | @media only screen and (max-width: 534px) { 359 | .image-genre { 360 | width: 130px; 361 | height: 130px; 362 | } 363 | } 364 | 365 | @media only screen and (max-width: 463px) { 366 | .image-genre { 367 | width: 100px; 368 | height: 100px; 369 | } 370 | .table-genre { 371 | display: none; 372 | } 373 | } 374 | 375 | @media only screen and (max-width: 403px) { 376 | .image-genre { 377 | width: 200px; 378 | height: 200px; 379 | } 380 | .account-img { 381 | width: 80%; 382 | height: auto; 383 | } 384 | .table { 385 | width: 75% !important; 386 | } 387 | } 388 | 389 | @media only screen and (max-width: 380px) { 390 | .text-secondary { 391 | font-size: 1.2rem; 392 | } 393 | .account-heading { 394 | font-size: 1.2rem; 395 | } 396 | .delete-review > input, .update-review { 397 | float: none; 398 | margin: 0px 0px 10px 0px; 399 | } 400 | } 401 | 402 | @media only screen and (max-width: 343px) { 403 | .table-author { 404 | display: none; 405 | } 406 | .skull { 407 | width: 97%; 408 | height: auto; 409 | } 410 | } 411 | 412 | @media only screen and (max-width: 329px) { 413 | .image-genre { 414 | width: 120px; 415 | height: 120px; 416 | } 417 | .list-group-item > strong { 418 | font-size: 1rem; 419 | } 420 | .titulo { 421 | font-size: 1.8rem; 422 | } 423 | .table { 424 | width: 80% !important; 425 | } 426 | .form-group > h2 { 427 | font-size: 1.4rem; 428 | } 429 | } -------------------------------------------------------------------------------- /projeto/templates/account.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
5 |

Account Information

6 |
7 | 8 |
9 |
10 |
11 |
12 | 13 |

{{ current_user.email }}

14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for book in books %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |

{{ book[2] }}{{ book[1] }}{{ book[3] }}{{ book[4].strftime('%Y-%m-%d') }}{{ book[5] }}
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for author, number in books_author.items() %} 49 | 50 | 51 | 52 | 53 | {% endfor %} 54 | 55 |

{{ author }}{{ number }}
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {% for genre, number in books_genre.items() %} 67 | 68 | 69 | 70 | 71 | {% endfor %} 72 | 73 | 74 | 75 | 76 | 77 |

{{ genre }}{{ number }}
Total{{ total_books }}
78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |

Total{{ total_analysis }}
94 |
95 |
96 |
97 |
98 |
99 | {{ form.hidden_tag() }} 100 |
101 | Update Account Info 102 |
103 | {{ form.username.label(class="form-control-label") }} 104 | 105 | 106 | {% if form.username.errors %} 107 | {{ form.username(class="form-control form-control-lg is-invalid") }} 108 |
109 | {% for error in form.username.errors %} 110 | {{ error }} 111 | {% endfor %} 112 |
113 | {% else %} 114 | {{ form.username(class="form-control form-control-lg") }} 115 | {% endif %} 116 |
117 |
118 | {{ form.email.label(class="form-control-label") }} 119 | 120 | 121 | {% if form.email.errors %} 122 | {{ form.email(class="form-control form-control-lg is-invalid") }} 123 |
124 | {% for error in form.email.errors %} 125 | {{ error }} 126 | {% endfor %} 127 |
128 | {% else %} 129 | {{ form.email(class="form-control form-control-lg") }} 130 | {% endif %} 131 |
132 |
133 |
134 |
135 | {{ form.picture(class="custom-file-input") }} 136 | {{ form.picture.label(class="custom-file-label") }} 137 |
138 | {% if form.picture.errors %} 139 | {% for error in form.picture.errors %} 140 | {{ error }}
141 | {% endfor %} 142 | {% endif %} 143 |
144 |
145 | {{ form.submit(class="btn btn-primary") }} 146 |
147 | Cancel 148 |
149 |
150 |
151 | {% endblock content %} 152 | 153 | {% block script %} 154 | 175 | {% endblock script %} --------------------------------------------------------------------------------