That page does not exist. Please try a different location
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 |
14 |
15 |
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 |
15 |
16 |
17 |
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 |
24 |
25 |
Results for {{ query }}
26 |
27 | {% for book in books.items %}
28 |
29 |
30 |
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 |
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 |
24 |
25 | {% for book in books.items %}
26 |
27 |
28 |
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 |
24 |
25 | {% for book in books.items %}
26 |
27 |
28 |
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 |
14 |
15 |
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 |
18 | {% endif %}
19 |
 }})
20 |
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 | 
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 |
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 |
20 |
21 |
22 |
23 | Home
24 | {% if current_user.is_authenticated %}
25 | New Book
26 | Account
27 | Messages
28 | Logout
29 | {% else %}
30 | Register
31 | Login
32 | {% endif %}
33 |
34 |
35 | {% with messages = get_flashed_messages(with_categories=true) %}
36 | {% if messages %}
37 | {% for category, message in messages %}
38 |
39 | {{ message }}
40 |
43 |
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 |
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 |
41 |
42 |
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 |
25 | {% else %}
26 |
27 | {% if current_user.is_authenticated %}
28 |
Analyze
29 | {% endif %}
30 |
Information
31 |
32 | {% endif %}
33 |
34 |
35 |
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 |
54 | {% endif %}
55 |
 }})
56 |
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 |
14 |
15 |
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 |
97 |
98 |
150 |
151 | {% endblock content %}
152 |
153 | {% block script %}
154 |
175 | {% endblock script %}
--------------------------------------------------------------------------------