diff --git a/Chapter08_User/Projects/Readme.md b/Chapter08_User/Projects/Readme.md index d36b8ae..49c69dc 100644 --- a/Chapter08_User/Projects/Readme.md +++ b/Chapter08_User/Projects/Readme.md @@ -2,5 +2,5 @@ მაგალითი: - სახელი გვარი | [პროექტი](/მისამართი) - +- giorgi kelenjeridze | [Food Order App](/Chapter08_User/Projects/giorgikelenjeridze/app.py) ### 2025 ზაფხული diff --git a/Chapter08_User/Projects/giorgikelenjeridze/app.py b/Chapter08_User/Projects/giorgikelenjeridze/app.py new file mode 100644 index 0000000..ce3460f --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/app.py @@ -0,0 +1,6 @@ +from src import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/README b/Chapter08_User/Projects/giorgikelenjeridze/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py new file mode 100644 index 0000000..4476bfd --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py @@ -0,0 +1,32 @@ +"""Added description to Product + +Revision ID: 2be0fc404953 +Revises: +Create Date: 2025-05-31 19:42:43.908055 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2be0fc404953' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.drop_column('description') + + # ### end Alembic commands ### diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py new file mode 100644 index 0000000..5ab938d --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py @@ -0,0 +1,38 @@ +"""Changed field + +Revision ID: 9b4f3a94e264 +Revises: 2be0fc404953 +Create Date: 2025-06-02 15:49:00.933969 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9b4f3a94e264' +down_revision = '2be0fc404953' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('_password', sa.String(), nullable=True)) + batch_op.create_unique_constraint(None, ['email']) + batch_op.create_unique_constraint(None, ['username']) + batch_op.drop_column('password') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('_password') + + # ### end Alembic commands ### diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py new file mode 100644 index 0000000..93fd1c1 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py @@ -0,0 +1,50 @@ +from flask import Flask +from src.config import Config +from src.ext import db, migrate, login_manager +from src.view import product_blueprint, user_blueprint, main_blueprint +from src.commands import init_db, populate_db +from src.models import User + +Blueprints = [product_blueprint, user_blueprint, main_blueprint] +Commands = [init_db, populate_db] + + +def create_app(): + + app = Flask(__name__) + app.config.from_object(Config) + + register_extensions(app) + register_blueprints(app) + register_commands(app) + + return app + + +def register_extensions(app): + + # Flask-SQLAlchemy + db.init_app(app) + + # Flask-Migrate + migrate.init_app(app, db) + + # Flask-LoginManager + login_manager.init_app(app) + login_manager.login_view = 'user.login' + + @login_manager.user_loader + def load_user(_id): + return User.query.get(_id) + + +def register_blueprints(app): + for blueprint in Blueprints: + app.register_blueprint(blueprint) + + +def register_commands(app): + for command in Commands: + app.cli.add_command(command) + + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py b/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py new file mode 100644 index 0000000..8cfb5fa --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py @@ -0,0 +1,80 @@ +from flask.cli import with_appcontext +import datetime +import click +from src.ext import db +from src.models import Product, Person, IDcard + + +@click.command("init_db") +@with_appcontext +def init_db(): + click.echo("Initializing db...") + + db.drop_all() + db.create_all() + + click.echo("Initializing is over!") + + +@click.command("populate_db") +@with_appcontext +def populate_db(): + + + products = [ + { + "id": 0, + "name": "შაურმა პატარა", + "price": 11, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 1, + "name": "შაურმა სტანდარტი", + "price": 13, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 2, + "name": "შაურმა დიდი", + "price": 18, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 3, + "name": "კოლა", + "price": 3, + "image": "cola.jfif", + "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ" + }, + { + "id": 4, + "name": "სპრაიტი", + "price": 3, + "image": "sprite.jfif", + "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ" + }, + { + "id": 5, + "name": "ბურგერი", + "price": 10, + "image": "burger.png", + "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი" + } + ] + + for product in products: + new_product = Product(name=product["name"], price=product["price"], description=product["description"], + image=product["image"]) + db.session.add(new_product) + + idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now()) + db.session.add(idcard) + db.session.commit() + person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id) + db.session.add(person) + db.session.commit() + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/config.py b/Chapter08_User/Projects/giorgikelenjeridze/src/config.py new file mode 100644 index 0000000..abbf13b --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/config.py @@ -0,0 +1,14 @@ +from os import environ + + +class Config: + + DB_USER = environ.get("DB_USER") + DB_PASSWORD = environ.get("DB_PASSWORD") + DB_DATABASE = environ.get("DB_DATABASE") + + print(DB_USER) + print(DB_PASSWORD) + print(DB_DATABASE) + SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}" + SECRET_KEY = environ.get("SECRET_KEY") \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py b/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py new file mode 100644 index 0000000..6377be4 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py @@ -0,0 +1,7 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py new file mode 100644 index 0000000..9e8a271 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py @@ -0,0 +1,4 @@ +from src.models.base import BaseModel +from src.models.person import Person, IDcard +from src.models.product import Product +from src.models.user import User \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py new file mode 100644 index 0000000..6f33fa7 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py @@ -0,0 +1,19 @@ +from src.ext import db + + +class BaseModel(db.Model): + + __abstract__ = True + + def create(self, commit=True): + db.session.add(self) + if commit: + self.save() + + def save(self): + db.session.commit() + + def delete(self, commit=True): + db.session.delete(self) + if commit: + self.save() diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py new file mode 100644 index 0000000..93875a4 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py @@ -0,0 +1,26 @@ +from src.ext import db +from src.models import BaseModel + + +class Person(BaseModel): + + __tablename__ = "persons" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + surname = db.Column(db.String) + birthday = db.Column(db.Date) + + idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id")) + + idcard = db.relationship("IDcard", back_populates="person") + + +class IDcard(BaseModel): + + __tablename__ = "idcards" + id = db.Column(db.Integer, primary_key=True) + serial_number = db.Column(db.String) + expiry_data = db.Column(db.Date) + + person = db.relationship("Person", back_populates="idcard", uselist=False) + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py new file mode 100644 index 0000000..da5ae3c --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py @@ -0,0 +1,22 @@ +from src.ext import db +from src.models import BaseModel + + +class Product(BaseModel): + + __tablename__ = "products" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + price = db.Column(db.Float) + description = db.Column(db.String) + image = db.Column(db.String) + + def __init__(self, name, price, description, image): + self.name = name + self.price = price + self.description = description + self.image = image + + def __repr__(self): + return f"This is {self.name} and costs {self.price}$" + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py new file mode 100644 index 0000000..1b43670 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py @@ -0,0 +1,38 @@ +from src.ext import db +from src.models import BaseModel +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(BaseModel, UserMixin): + + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, unique=True) + email = db.Column(db.String, unique=True) + _password = db.Column(db.String) + + def __init__(self, username, email, password): + self.username = username + self.email = email + self.password = password + + def __repr__(self): + return f"User name: {self.username}" + + def edit(self, form): + self.username = form.username.data + self.password = form.password.data + db.session.commit() + + def check_password(self, password): + return check_password_hash(self.password, password) + + @property + def password(self): + return self._password + + @password.setter + def password(self, password): + self._password = generate_password_hash(password) + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png new file mode 100644 index 0000000..c5bb973 Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png differ diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif new file mode 100644 index 0000000..4d83f30 Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif new file mode 100644 index 0000000..9cc4780 Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif new file mode 100644 index 0000000..e775e42 Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif new file mode 100644 index 0000000..128f4c4 Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html new file mode 100644 index 0000000..4674ec6 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html @@ -0,0 +1,5 @@ +{% macro is_active(endpoint) %} + {% if endpoint==request.endpoint %} + active + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html new file mode 100644 index 0000000..792387f --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html @@ -0,0 +1,72 @@ +{% extends "partials/base.html" %} +{% block title %}feedback{% endblock %} +{% block body %} + +
+

We Value Your Feedback

+ + +
+ + +
+ + +
+ + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +{% endblock %} diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html new file mode 100644 index 0000000..cd35a37 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html @@ -0,0 +1,48 @@ +{% extends "partials/base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+

My Simple "Food Order" Web Application

+
+ Main Shaurma + +
+

ჩვენი მენიუ

+ +
+ + + +
+ გაგვიზიარე შეფასება +
+ + +
+

ჩვენი ფილიალები

+ +
+ +
+ +{% endblock %} diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html new file mode 100644 index 0000000..6821ae2 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html @@ -0,0 +1,33 @@ + + + + + Kelenjo {% block title %} {% endblock %} + + + + + {% include "partials/navbar.html" %} +{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #} + +{% if get_flashed_messages(with_categories=True) %} +{% for category, message in get_flashed_messages(with_categories=True) %} + +{% endfor %} +{% endif %} + {% block body %} + {% endblock %} + + + \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html new file mode 100644 index 0000000..0bccff3 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html @@ -0,0 +1,23 @@ +{% from "macros.html" import is_active %} + diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html new file mode 100644 index 0000000..d696014 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html @@ -0,0 +1,23 @@ +{% extends "partials/base.html" %} + +{% block title %}{{product['name']}}{% endblock %} + +{% block body %} +
+
+
+
+ {{ product.name }} +
+
{{ product.name }}
+

{{ product.description }}

+
+
${{ product.price }}
+
+
+
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html new file mode 100644 index 0000000..6454ecc --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html @@ -0,0 +1,30 @@ +{% extends "partials/base.html" %} +{% block title%}Log In{% endblock %} + +{% block body %} +
+
+

Login

+ + + +
+ {{ form.hidden_tag() }} + +
+ {{ form.username.label(class="form-label") }} + {{ form.username(class="form-control", placeholder="Username") }} +
+ +
+ {{ form.password.label(class="form-label") }} + {{ form.password(class="form-control", placeholder="Password") }} +
+ +
+ {{ form.submit(class="btn btn-primary") }} +
+
+
+
+{% endblock %} diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html new file mode 100644 index 0000000..d611686 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html @@ -0,0 +1,46 @@ +{% extends "partials/base.html" %} +{% block title %}Register{% endblock %} + +{% block body %} + + +
+
+
+

Create Your Account

+
+ {% for errors in form.errors.values() %} + {% for error in errors %} +

*{{ error }}

+ {% endfor %} + {% endfor %} + {{ form.hidden_tag() }} + + {{ form.username(placeholder=form.username.label.text, class="form-control") }} +
+ {{ form.email(placeholder=form.email.label.text, class="form-control") }} +
+ {{ form.password(placeholder=form.password.label.text, class="form-control") }} +
+ {{ form.submit(placeholder=form.submit.label.text, class="form-control") }} +
+
+
+
+ + + + +{% endblock %} diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py new file mode 100644 index 0000000..fc5db26 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py @@ -0,0 +1,3 @@ +from src.view.product.routes import product_blueprint +from src.view.user.routes import user_blueprint +from src.view.main.routes import main_blueprint \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py new file mode 100644 index 0000000..79835cc --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +main_blueprint = Blueprint("main", __name__) + + +@main_blueprint.route("/") +def index(): + products = Product.query.all() + return render_template("main/index.html", products=products) + + +@main_blueprint.route("/feedback") +def feedback(): + return render_template("main/feedback.html") \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py new file mode 100644 index 0000000..a2f8d83 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +product_blueprint = Blueprint("product", __name__) + + +@product_blueprint.route("/view/") +def view(product_id): + product = Product.query.get(product_id) + print(product) + return render_template("product/view_product.html", product=product) \ No newline at end of file diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py new file mode 100644 index 0000000..eda7766 --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py @@ -0,0 +1,39 @@ +from flask_wtf import FlaskForm +from wtforms.fields import StringField, PasswordField, EmailField, SubmitField +from wtforms.validators import DataRequired, length, ValidationError +from string import ascii_lowercase, ascii_uppercase, digits + + +class RegisterForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)]) + email = EmailField("Enter email", validators=[DataRequired()]) + password = PasswordField("Enter password", validators=[DataRequired()]) + + submit = SubmitField("Register") + + def validate_password(self, field): + contains_upcase = False + contains_lowcase = False + contains_digits = False + + for char in field.data: + if char in ascii_uppercase: + contains_upcase=True + if char in ascii_lowercase: + contains_lowcase=True + if char in digits: + contains_digits=True + + if contains_upcase == False: + raise ValidationError("Password must contain UpperCase Letter") + if contains_lowcase == False: + raise ValidationError("Password must contain LowerCase Letter") + if contains_digits == False: + raise ValidationError("Password must contain Digits Letter") + + +class LoginForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired()]) + password = PasswordField("Enter Password", validators=[DataRequired()]) + + submit = SubmitField("Log in") diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py new file mode 100644 index 0000000..c62d8ce --- /dev/null +++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py @@ -0,0 +1,78 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request +from src.view.user.forms import RegisterForm, LoginForm +from src.models.user import User +from flask_login import login_user, logout_user, login_required + + +user_blueprint = Blueprint("user", __name__) + + +@user_blueprint.route("/register", methods=['GET', 'POST']) +def register(): + form = RegisterForm() + if form.validate_on_submit(): + print(form.username.data) + print(form.email.data) + print(form.password.data) + flash("Sagol shen daregistrirdi", "info") + print('ar xar') + user = User(form.username.data, form.email.data, form.password.data) + user.create() + # return render_template('index.html') + else: + print('bad xar1') + print(form.errors) + print('bad xaar2') + return render_template("user/register.html", form=form) + + +@user_blueprint.route("/login", methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter(User.username == form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + + next = request.args.get("next") + + if next: + return redirect(next) + flash("Logged in!", "success") + return redirect(url_for("main.index")) + else: + flash("Username or Password is incorrect", "danger") + + return render_template("user/login.html", form=form) + + +@user_blueprint.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("main.index")) + + +@user_blueprint.route("/edit/", methods=['GET', 'POST']) +@login_required +def edit(user_id): + user = User.query.get(user_id) + oldname = user.username + form = RegisterForm(username=user.username, email=user.email, password=user.password) + if form.validate_on_submit(): + user.edit(form) + print(f"{oldname} has changed it own name to {user.username}") + else: + print(form.errors) + return render_template("user/register.html", form=form) + + +@user_blueprint.route("/delete/") +@login_required +def delete(user_id): + usertodel = User.query.get(user_id) + User.query.filter_by() + print(usertodel) + usertodel.delete() + print(url_for("index")) + return redirect(url_for("main.index")) \ No newline at end of file diff --git a/Chapter09_Admin/Projects/Readme.md b/Chapter09_Admin/Projects/Readme.md index b684ad0..b3bc3f4 100644 --- a/Chapter09_Admin/Projects/Readme.md +++ b/Chapter09_Admin/Projects/Readme.md @@ -2,5 +2,5 @@ მაგალითი: - სახელი გვარი | [პროექტი](/მისამართი) - +- giorgi kelenjeridze | [Food Order app](/Chapter09_Admin/Projects/giorgikelenjeridze/app.py) ### 2025 ზაფხული diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/app.py b/Chapter09_Admin/Projects/giorgikelenjeridze/app.py new file mode 100644 index 0000000..ce3460f --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/app.py @@ -0,0 +1,6 @@ +from src import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt new file mode 100644 index 0000000..a37387c Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py new file mode 100644 index 0000000..4476bfd --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py @@ -0,0 +1,32 @@ +"""Added description to Product + +Revision ID: 2be0fc404953 +Revises: +Create Date: 2025-05-31 19:42:43.908055 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2be0fc404953' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.drop_column('description') + + # ### end Alembic commands ### diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py new file mode 100644 index 0000000..5ab938d --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py @@ -0,0 +1,38 @@ +"""Changed field + +Revision ID: 9b4f3a94e264 +Revises: 2be0fc404953 +Create Date: 2025-06-02 15:49:00.933969 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9b4f3a94e264' +down_revision = '2be0fc404953' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('_password', sa.String(), nullable=True)) + batch_op.create_unique_constraint(None, ['email']) + batch_op.create_unique_constraint(None, ['username']) + batch_op.drop_column('password') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('_password') + + # ### end Alembic commands ### diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt new file mode 100644 index 0000000..3bc5b0f Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py new file mode 100644 index 0000000..23d4bda --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py @@ -0,0 +1,60 @@ +from flask import Flask +from src.admin_views.base import SecureModelView +from src.admin_views import UserView, ProductView +from src.config import Config +from src.ext import db, migrate, login_manager, admin +from src.view import product_blueprint, auth_blueprint, main_blueprint +from src.commands import init_db, populate_db +from src.models import User, Product +from flask_admin.menu import MenuLink + +Blueprints = [product_blueprint, auth_blueprint, main_blueprint] +Commands = [init_db, populate_db] + + +def create_app(): + + app = Flask(__name__) + app.config.from_object(Config) + + register_extensions(app) + register_blueprints(app) + register_commands(app) + + return app + + +def register_extensions(app): + + # Flask-SQLAlchemy + db.init_app(app) + + # Flask-Migrate + migrate.init_app(app, db) + + # Flask-LoginManager + login_manager.init_app(app) + login_manager.login_view = 'user.login' + + @login_manager.user_loader + def load_user(_id): + return User.query.get(_id) + + # Flask-Admin + admin.init_app(app) + admin.add_view(UserView(User, db.session)) + admin.add_view(ProductView(Product, db.session, endpoint="product_admin")) + + admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out")) + + +def register_blueprints(app): + for blueprint in Blueprints: + app.register_blueprint(blueprint) + + +def register_commands(app): + for command in Commands: + app.cli.add_command(command) + + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py new file mode 100644 index 0000000..8e52cd1 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py @@ -0,0 +1,2 @@ +from src.admin_views.user import UserView +from src.admin_views.product import ProductView diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py new file mode 100644 index 0000000..4f3c2d9 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py @@ -0,0 +1,24 @@ +from flask_admin.contrib.sqla import ModelView +from flask_admin import AdminIndexView +from flask_login import current_user +from flask import redirect, url_for + + +class SecureModelView(ModelView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + +class SecureIndexView(AdminIndexView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py new file mode 100644 index 0000000..950ef52 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py @@ -0,0 +1,33 @@ +from src.admin_views.base import SecureModelView +from src.config import Config + +from flask_admin.form import ImageUploadField +from os import path +from uuid import uuid4 +from markupsafe import Markup + + +def generate_filename(obj, file): + name, extension = path.splitext(file.filename) + return f"{uuid4()}{extension}" + + +class ProductView(SecureModelView): + create_modal = True + edit_modal = True + + column_editable_list = ("name", "price", "description") + column_filters = ["price",] + + column_formatters = { + "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...", + "image": lambda v,c,m,n: Markup(f"") + } + + form_overrides = {"image": ImageUploadField} + form_args = { + "image": { + "base_path": Config.UPLOAD_PATH, + "namegen": generate_filename + } + } \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py new file mode 100644 index 0000000..22dbec3 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py @@ -0,0 +1,10 @@ +from src.admin_views.base import SecureModelView + + +class UserView(SecureModelView): + can_view_details = True + can_delete = False + + column_list = ("username", "email") + column_details_list = ("username", "email", "_password") + column_searchable_list = ["username"] \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py new file mode 100644 index 0000000..8cfb5fa --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py @@ -0,0 +1,80 @@ +from flask.cli import with_appcontext +import datetime +import click +from src.ext import db +from src.models import Product, Person, IDcard + + +@click.command("init_db") +@with_appcontext +def init_db(): + click.echo("Initializing db...") + + db.drop_all() + db.create_all() + + click.echo("Initializing is over!") + + +@click.command("populate_db") +@with_appcontext +def populate_db(): + + + products = [ + { + "id": 0, + "name": "შაურმა პატარა", + "price": 11, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 1, + "name": "შაურმა სტანდარტი", + "price": 13, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 2, + "name": "შაურმა დიდი", + "price": 18, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 3, + "name": "კოლა", + "price": 3, + "image": "cola.jfif", + "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ" + }, + { + "id": 4, + "name": "სპრაიტი", + "price": 3, + "image": "sprite.jfif", + "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ" + }, + { + "id": 5, + "name": "ბურგერი", + "price": 10, + "image": "burger.png", + "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი" + } + ] + + for product in products: + new_product = Product(name=product["name"], price=product["price"], description=product["description"], + image=product["image"]) + db.session.add(new_product) + + idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now()) + db.session.add(idcard) + db.session.commit() + person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id) + db.session.add(person) + db.session.commit() + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py new file mode 100644 index 0000000..8b1ac4e --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py @@ -0,0 +1,17 @@ +from os import environ, path + + +class Config: + + DB_USER = environ.get("DB_USER") + DB_PASSWORD = environ.get("DB_PASSWORD") + DB_DATABASE = environ.get("DB_DATABASE") + + # print(DB_USER) + # print(DB_PASSWORD) + # print(DB_DATABASE) + BASE_DIRECTORY = path.abspath(path.dirname(__file__)) + UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets") + SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}" + SECRET_KEY = environ.get("SECRET_KEY") + FLASK_ADMIN_SWATCH="Flatly" \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py new file mode 100644 index 0000000..b1e63af --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py @@ -0,0 +1,10 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +from flask_admin import Admin +from src.admin_views.base import SecureIndexView + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() +admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html") \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py new file mode 100644 index 0000000..9e8a271 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py @@ -0,0 +1,4 @@ +from src.models.base import BaseModel +from src.models.person import Person, IDcard +from src.models.product import Product +from src.models.user import User \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py new file mode 100644 index 0000000..6f33fa7 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py @@ -0,0 +1,19 @@ +from src.ext import db + + +class BaseModel(db.Model): + + __abstract__ = True + + def create(self, commit=True): + db.session.add(self) + if commit: + self.save() + + def save(self): + db.session.commit() + + def delete(self, commit=True): + db.session.delete(self) + if commit: + self.save() diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py new file mode 100644 index 0000000..93875a4 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py @@ -0,0 +1,26 @@ +from src.ext import db +from src.models import BaseModel + + +class Person(BaseModel): + + __tablename__ = "persons" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + surname = db.Column(db.String) + birthday = db.Column(db.Date) + + idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id")) + + idcard = db.relationship("IDcard", back_populates="person") + + +class IDcard(BaseModel): + + __tablename__ = "idcards" + id = db.Column(db.Integer, primary_key=True) + serial_number = db.Column(db.String) + expiry_data = db.Column(db.Date) + + person = db.relationship("Person", back_populates="idcard", uselist=False) + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py new file mode 100644 index 0000000..da5ae3c --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py @@ -0,0 +1,22 @@ +from src.ext import db +from src.models import BaseModel + + +class Product(BaseModel): + + __tablename__ = "products" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + price = db.Column(db.Float) + description = db.Column(db.String) + image = db.Column(db.String) + + def __init__(self, name, price, description, image): + self.name = name + self.price = price + self.description = description + self.image = image + + def __repr__(self): + return f"This is {self.name} and costs {self.price}$" + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py new file mode 100644 index 0000000..476a7c0 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py @@ -0,0 +1,38 @@ +from src.ext import db +from src.models import BaseModel +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(BaseModel, UserMixin): + + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, unique=True) + email = db.Column(db.String, unique=True) + _password = db.Column(db.String) + + def __init__(self, username, email, password): + self.username = username + self.email = email + self._password = password + + def __repr__(self): + return f"User name: {self.username}" + + def edit(self, form): + self.username = form.username.data + self.password = form.password.data + db.session.commit() + + def check_password(self, password): + return check_password_hash(self.password, password) + + @property + def password(self): + return self._password + + @password.setter + def password(self, password): + self._password = generate_password_hash(password) + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png new file mode 100644 index 0000000..61f8ccd Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png new file mode 100644 index 0000000..c5bb973 Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif new file mode 100644 index 0000000..4d83f30 Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif new file mode 100644 index 0000000..9cc4780 Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif new file mode 100644 index 0000000..e775e42 Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif new file mode 100644 index 0000000..128f4c4 Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html new file mode 100644 index 0000000..7e73a33 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html @@ -0,0 +1,42 @@ +{% extends "admin/base.html" %} + +{% block page_body %} + + +
+ + {% block messages %} + {{ layout.messages() }} + {% endblock %} + + {# store the jinja2 context for form_rules rendering logic #} + {% set render_ctx = h.resolve_ctx() %} + + {% block body %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html new file mode 100644 index 0000000..6454ecc --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html @@ -0,0 +1,30 @@ +{% extends "partials/base.html" %} +{% block title%}Log In{% endblock %} + +{% block body %} +
+ +
+{% endblock %} diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html new file mode 100644 index 0000000..d611686 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html @@ -0,0 +1,46 @@ +{% extends "partials/base.html" %} +{% block title %}Register{% endblock %} + +{% block body %} + + +
+
+
+

Create Your Account

+
+ {% for errors in form.errors.values() %} + {% for error in errors %} +

*{{ error }}

+ {% endfor %} + {% endfor %} + {{ form.hidden_tag() }} + + {{ form.username(placeholder=form.username.label.text, class="form-control") }} +
+ {{ form.email(placeholder=form.email.label.text, class="form-control") }} +
+ {{ form.password(placeholder=form.password.label.text, class="form-control") }} +
+ {{ form.submit(placeholder=form.submit.label.text, class="form-control") }} +
+
+
+
+ + + + +{% endblock %} diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html new file mode 100644 index 0000000..4674ec6 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html @@ -0,0 +1,5 @@ +{% macro is_active(endpoint) %} + {% if endpoint==request.endpoint %} + active + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html new file mode 100644 index 0000000..792387f --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html @@ -0,0 +1,72 @@ +{% extends "partials/base.html" %} +{% block title %}feedback{% endblock %} +{% block body %} + +
+

We Value Your Feedback

+ + +
+ + +
+ + +
+ + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +{% endblock %} diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html new file mode 100644 index 0000000..cd35a37 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html @@ -0,0 +1,48 @@ +{% extends "partials/base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+

My Simple "Food Order" Web Application

+
+ Main Shaurma + +
+

ჩვენი მენიუ

+ +
+ + + + + + +
+

ჩვენი ფილიალები

+
    +
  • გლდანი, ა მიკრო
  • +
  • გლანი, I მიკრო
  • +
  • საბურთალო
  • +
+
+ +
+ +{% endblock %} diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html new file mode 100644 index 0000000..6821ae2 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html @@ -0,0 +1,33 @@ + + + + + Kelenjo {% block title %} {% endblock %} + + + + + {% include "partials/navbar.html" %} +{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #} + +{% if get_flashed_messages(with_categories=True) %} +{% for category, message in get_flashed_messages(with_categories=True) %} + +{% endfor %} +{% endif %} + {% block body %} + {% endblock %} + + + \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html new file mode 100644 index 0000000..4e06a3e --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html @@ -0,0 +1,23 @@ +{% from "macros.html" import is_active %} + diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html new file mode 100644 index 0000000..d696014 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html @@ -0,0 +1,23 @@ +{% extends "partials/base.html" %} + +{% block title %}{{product['name']}}{% endblock %} + +{% block body %} +
+
+
+
+ {{ product.name }} +
+
{{ product.name }}
+

{{ product.description }}

+
+
${{ product.price }}
+
+
+
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py new file mode 100644 index 0000000..7e31f71 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py @@ -0,0 +1,3 @@ +from src.view.product.routes import product_blueprint +from src.view.auth.routes import auth_blueprint +from src.view.main.routes import main_blueprint \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py new file mode 100644 index 0000000..eda7766 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py @@ -0,0 +1,39 @@ +from flask_wtf import FlaskForm +from wtforms.fields import StringField, PasswordField, EmailField, SubmitField +from wtforms.validators import DataRequired, length, ValidationError +from string import ascii_lowercase, ascii_uppercase, digits + + +class RegisterForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)]) + email = EmailField("Enter email", validators=[DataRequired()]) + password = PasswordField("Enter password", validators=[DataRequired()]) + + submit = SubmitField("Register") + + def validate_password(self, field): + contains_upcase = False + contains_lowcase = False + contains_digits = False + + for char in field.data: + if char in ascii_uppercase: + contains_upcase=True + if char in ascii_lowercase: + contains_lowcase=True + if char in digits: + contains_digits=True + + if contains_upcase == False: + raise ValidationError("Password must contain UpperCase Letter") + if contains_lowcase == False: + raise ValidationError("Password must contain LowerCase Letter") + if contains_digits == False: + raise ValidationError("Password must contain Digits Letter") + + +class LoginForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired()]) + password = PasswordField("Enter Password", validators=[DataRequired()]) + + submit = SubmitField("Log in") diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py new file mode 100644 index 0000000..a14c092 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py @@ -0,0 +1,78 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request +from src.view.auth.forms import RegisterForm, LoginForm +from src.models import User +from flask_login import login_user, logout_user, login_required + + +auth_blueprint = Blueprint("auth", __name__) + + +@auth_blueprint.route("/register", methods=['GET', 'POST']) +def register(): + form = RegisterForm() + if form.validate_on_submit(): + print(form.username.data) + print(form.email.data) + print(form.password.data) + flash("Sagol shen daregistrirdi", "info") + print('ar xar') + user = User(form.username.data, form.email.data, form.password.data) + user.create() + # return render_template('index.html') + else: + print('bad xar1') + print(form.errors) + print('bad xaar2') + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/login", methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter(User.username == form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + + next = request.args.get("next") + + if next: + return redirect(next) + flash("Logged in!", "success") + return redirect(url_for("main.index")) + else: + flash("Username or Password is incorrect", "danger") + + return render_template("auth/login.html", form=form) + + +@auth_blueprint.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("main.index")) + + +@auth_blueprint.route("/edit/", methods=['GET', 'POST']) +@login_required +def edit(user_id): + user = User.query.get(user_id) + oldname = user.username + form = RegisterForm(username=user.username, email=user.email, password=user.password) + if form.validate_on_submit(): + user.edit(form) + print(f"{oldname} has changed it own name to {user.username}") + else: + print(form.errors) + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/delete/") +@login_required +def delete(user_id): + usertodel = User.query.get(user_id) + User.query.filter_by() + print(usertodel) + usertodel.delete() + print(url_for("index")) + return redirect(url_for("main.index")) \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py new file mode 100644 index 0000000..79835cc --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +main_blueprint = Blueprint("main", __name__) + + +@main_blueprint.route("/") +def index(): + products = Product.query.all() + return render_template("main/index.html", products=products) + + +@main_blueprint.route("/feedback") +def feedback(): + return render_template("main/feedback.html") \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py new file mode 100644 index 0000000..a2f8d83 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +product_blueprint = Blueprint("product", __name__) + + +@product_blueprint.route("/view/") +def view(product_id): + product = Product.query.get(product_id) + print(product) + return render_template("product/view_product.html", product=product) \ No newline at end of file diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf b/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf new file mode 100644 index 0000000..13cb465 --- /dev/null +++ b/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf @@ -0,0 +1,15 @@ +[uwsgi] + +master = true +module = src:create_app() +processes = 2 +threads = 4 +socket = :5000 +chmod-socket = 600 +die-on-term = true +vacuum = true + +env = DB_DATABASE=python_intern-db +env = DB_PASSWORD=Wyali!12345 +env = DB_USER=python_intern-user +env = SECRET_KEY=MYS!GMASECRETKEY!@ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py new file mode 100644 index 0000000..ce3460f --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py @@ -0,0 +1,6 @@ +from src import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt new file mode 100644 index 0000000..a37387c Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py new file mode 100644 index 0000000..4476bfd --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py @@ -0,0 +1,32 @@ +"""Added description to Product + +Revision ID: 2be0fc404953 +Revises: +Create Date: 2025-05-31 19:42:43.908055 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2be0fc404953' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.drop_column('description') + + # ### end Alembic commands ### diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py new file mode 100644 index 0000000..5ab938d --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py @@ -0,0 +1,38 @@ +"""Changed field + +Revision ID: 9b4f3a94e264 +Revises: 2be0fc404953 +Create Date: 2025-06-02 15:49:00.933969 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9b4f3a94e264' +down_revision = '2be0fc404953' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('_password', sa.String(), nullable=True)) + batch_op.create_unique_constraint(None, ['email']) + batch_op.create_unique_constraint(None, ['username']) + batch_op.drop_column('password') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('_password') + + # ### end Alembic commands ### diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt new file mode 100644 index 0000000..d79aec7 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py new file mode 100644 index 0000000..1b7a8cf --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py @@ -0,0 +1,64 @@ +from flask import Flask +from src.admin_views.base import SecureModelView +from src.admin_views import UserView, ProductView +from src.config import Config +from src.ext import db, migrate, login_manager, admin +from src.view import product_blueprint, auth_blueprint, main_blueprint +from src.commands import init_db_command, populate_db_command +from src.models import User, Product +from flask_admin.menu import MenuLink + +Blueprints = [product_blueprint, auth_blueprint, main_blueprint] +Commands = [init_db_command, populate_db_command] + + +def create_app(test_config=None): + + app = Flask(__name__) + app.config.from_object(Config) + + # Then override with test config if passed + if test_config: + app.config.update(test_config) + + register_extensions(app) + register_blueprints(app) + register_commands(app) + + return app + + +def register_extensions(app): + + # Flask-SQLAlchemy + db.init_app(app) + + # Flask-Migrate + migrate.init_app(app, db) + + # Flask-LoginManager + login_manager.init_app(app) + login_manager.login_view = 'user.login' + + @login_manager.user_loader + def load_user(_id): + return User.query.get(_id) + + # Flask-Admin + admin.init_app(app) + admin.add_view(UserView(User, db.session)) + admin.add_view(ProductView(Product, db.session, endpoint="product_admin")) + + admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out")) + + +def register_blueprints(app): + for blueprint in Blueprints: + app.register_blueprint(blueprint) + + +def register_commands(app): + for command in Commands: + app.cli.add_command(command) + + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py new file mode 100644 index 0000000..8e52cd1 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py @@ -0,0 +1,2 @@ +from src.admin_views.user import UserView +from src.admin_views.product import ProductView diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py new file mode 100644 index 0000000..4f3c2d9 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py @@ -0,0 +1,24 @@ +from flask_admin.contrib.sqla import ModelView +from flask_admin import AdminIndexView +from flask_login import current_user +from flask import redirect, url_for + + +class SecureModelView(ModelView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + +class SecureIndexView(AdminIndexView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py new file mode 100644 index 0000000..950ef52 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py @@ -0,0 +1,33 @@ +from src.admin_views.base import SecureModelView +from src.config import Config + +from flask_admin.form import ImageUploadField +from os import path +from uuid import uuid4 +from markupsafe import Markup + + +def generate_filename(obj, file): + name, extension = path.splitext(file.filename) + return f"{uuid4()}{extension}" + + +class ProductView(SecureModelView): + create_modal = True + edit_modal = True + + column_editable_list = ("name", "price", "description") + column_filters = ["price",] + + column_formatters = { + "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...", + "image": lambda v,c,m,n: Markup(f"") + } + + form_overrides = {"image": ImageUploadField} + form_args = { + "image": { + "base_path": Config.UPLOAD_PATH, + "namegen": generate_filename + } + } \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py new file mode 100644 index 0000000..22dbec3 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py @@ -0,0 +1,10 @@ +from src.admin_views.base import SecureModelView + + +class UserView(SecureModelView): + can_view_details = True + can_delete = False + + column_list = ("username", "email") + column_details_list = ("username", "email", "_password") + column_searchable_list = ["username"] \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py new file mode 100644 index 0000000..721340b --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py @@ -0,0 +1,93 @@ +from flask.cli import with_appcontext +import datetime +import click +from src.ext import db +from src.models import Product, Person, IDcard, User + + +def init_db(): + + db.drop_all() + db.create_all() + + +@click.command("init_db") +@with_appcontext +def init_db_command(): + click.echo("Initializing db...") + + init_db() + + click.echo("Initializing is over!") + + +def populate_db(): + products = [ + { + "id": 0, + "name": "შაურმა პატარა", + "price": 11, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 1, + "name": "შაურმა სტანდარტი", + "price": 13, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 2, + "name": "შაურმა დიდი", + "price": 18, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 3, + "name": "კოლა", + "price": 3, + "image": "cola.jfif", + "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ" + }, + { + "id": 4, + "name": "სპრაიტი", + "price": 3, + "image": "sprite.jfif", + "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ" + }, + { + "id": 5, + "name": "ბურგერი", + "price": 10, + "image": "burger.png", + "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი" + } + ] + + for product in products: + new_product = Product(name=product["name"], price=product["price"], description=product["description"], + image=product["image"]) + db.session.add(new_product) + + idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now()) + db.session.add(idcard) + db.session.commit() + person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id) + db.session.add(person) + user = User("mari", "marikuna@gmail.com", "Mari123") + db.session.add(user) + db.session.commit() + + +@click.command("populate_db") +@with_appcontext +def populate_db_command(): + + populate_db() + + + + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py new file mode 100644 index 0000000..dcb42c2 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py @@ -0,0 +1,17 @@ +from os import environ, path + + +class Config: + + DB_USER = environ.get("DB_USER") + DB_PASSWORD = environ.get("DB_PASSWORD") + DB_DATABASE = environ.get("DB_DATABASE") + + # print(DB_USER) + # print(DB_PASSWORD) + # print(DB_DATABASE) + BASE_DIRECTORY = path.abspath(path.dirname(__file__)) + UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets") + SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}" + SECRET_KEY = environ.get("SECRET_KEY") + FLASK_ADMIN_SWATCH="Flatly" \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py new file mode 100644 index 0000000..b1e63af --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py @@ -0,0 +1,10 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +from flask_admin import Admin +from src.admin_views.base import SecureIndexView + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() +admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html") \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py new file mode 100644 index 0000000..9e8a271 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py @@ -0,0 +1,4 @@ +from src.models.base import BaseModel +from src.models.person import Person, IDcard +from src.models.product import Product +from src.models.user import User \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py new file mode 100644 index 0000000..6f33fa7 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py @@ -0,0 +1,19 @@ +from src.ext import db + + +class BaseModel(db.Model): + + __abstract__ = True + + def create(self, commit=True): + db.session.add(self) + if commit: + self.save() + + def save(self): + db.session.commit() + + def delete(self, commit=True): + db.session.delete(self) + if commit: + self.save() diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py new file mode 100644 index 0000000..93875a4 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py @@ -0,0 +1,26 @@ +from src.ext import db +from src.models import BaseModel + + +class Person(BaseModel): + + __tablename__ = "persons" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + surname = db.Column(db.String) + birthday = db.Column(db.Date) + + idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id")) + + idcard = db.relationship("IDcard", back_populates="person") + + +class IDcard(BaseModel): + + __tablename__ = "idcards" + id = db.Column(db.Integer, primary_key=True) + serial_number = db.Column(db.String) + expiry_data = db.Column(db.Date) + + person = db.relationship("Person", back_populates="idcard", uselist=False) + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py new file mode 100644 index 0000000..da5ae3c --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py @@ -0,0 +1,22 @@ +from src.ext import db +from src.models import BaseModel + + +class Product(BaseModel): + + __tablename__ = "products" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + price = db.Column(db.Float) + description = db.Column(db.String) + image = db.Column(db.String) + + def __init__(self, name, price, description, image): + self.name = name + self.price = price + self.description = description + self.image = image + + def __repr__(self): + return f"This is {self.name} and costs {self.price}$" + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py new file mode 100644 index 0000000..3a3129f --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py @@ -0,0 +1,38 @@ +from src.ext import db +from src.models import BaseModel +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(BaseModel, UserMixin): + + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, unique=True) + email = db.Column(db.String, unique=True) + _password = db.Column(db.String) + + def __init__(self, username, email, password): + self.username = username + self.email = email + self._password = generate_password_hash(password) + + def __repr__(self): + return f"User name: {self.username}" + + def edit(self, form): + self.username = form.username.data + self.password = form.password.data + db.session.commit() + + def check_password(self, password): + return check_password_hash(self.password, password) + + @property + def password(self): + return self._password + + @password.setter + def password(self, password): + self._password = generate_password_hash(password) + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png new file mode 100644 index 0000000..61f8ccd Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png new file mode 100644 index 0000000..c5bb973 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif new file mode 100644 index 0000000..4d83f30 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif new file mode 100644 index 0000000..9cc4780 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif new file mode 100644 index 0000000..e775e42 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif new file mode 100644 index 0000000..128f4c4 Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html new file mode 100644 index 0000000..7e73a33 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html @@ -0,0 +1,42 @@ +{% extends "admin/base.html" %} + +{% block page_body %} + + +
+ + {% block messages %} + {{ layout.messages() }} + {% endblock %} + + {# store the jinja2 context for form_rules rendering logic #} + {% set render_ctx = h.resolve_ctx() %} + + {% block body %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html new file mode 100644 index 0000000..6454ecc --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html @@ -0,0 +1,30 @@ +{% extends "partials/base.html" %} +{% block title%}Log In{% endblock %} + +{% block body %} +
+ +
+{% endblock %} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html new file mode 100644 index 0000000..d611686 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html @@ -0,0 +1,46 @@ +{% extends "partials/base.html" %} +{% block title %}Register{% endblock %} + +{% block body %} + + +
+
+
+

Create Your Account

+
+ {% for errors in form.errors.values() %} + {% for error in errors %} +

*{{ error }}

+ {% endfor %} + {% endfor %} + {{ form.hidden_tag() }} + + {{ form.username(placeholder=form.username.label.text, class="form-control") }} +
+ {{ form.email(placeholder=form.email.label.text, class="form-control") }} +
+ {{ form.password(placeholder=form.password.label.text, class="form-control") }} +
+ {{ form.submit(placeholder=form.submit.label.text, class="form-control") }} +
+
+
+
+ + + + +{% endblock %} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html new file mode 100644 index 0000000..4674ec6 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html @@ -0,0 +1,5 @@ +{% macro is_active(endpoint) %} + {% if endpoint==request.endpoint %} + active + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html new file mode 100644 index 0000000..792387f --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html @@ -0,0 +1,72 @@ +{% extends "partials/base.html" %} +{% block title %}feedback{% endblock %} +{% block body %} + +
+

We Value Your Feedback

+ + +
+ + +
+ + +
+ + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +{% endblock %} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html new file mode 100644 index 0000000..cd35a37 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html @@ -0,0 +1,48 @@ +{% extends "partials/base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+

My Simple "Food Order" Web Application

+
+ Main Shaurma + +
+

ჩვენი მენიუ

+ +
+ + + + + + +
+

ჩვენი ფილიალები

+
    +
  • გლდანი, ა მიკრო
  • +
  • გლანი, I მიკრო
  • +
  • საბურთალო
  • +
+
+ +
+ +{% endblock %} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html new file mode 100644 index 0000000..6821ae2 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html @@ -0,0 +1,33 @@ + + + + + Kelenjo {% block title %} {% endblock %} + + + + + {% include "partials/navbar.html" %} +{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #} + +{% if get_flashed_messages(with_categories=True) %} +{% for category, message in get_flashed_messages(with_categories=True) %} + +{% endfor %} +{% endif %} + {% block body %} + {% endblock %} + + + \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html new file mode 100644 index 0000000..4e06a3e --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html @@ -0,0 +1,23 @@ +{% from "macros.html" import is_active %} + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html new file mode 100644 index 0000000..57051ab --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html @@ -0,0 +1,29 @@ +{% extends "partials/base.html" %} + +{% block title %}{{ product.name if product is not none else "Product Not Found" }}{% endblock %} + +{% block body %} +
+
+
+
+ {% if product is not none %} + {{ product.name }} +
+
{{ product.name }}
+

{{ product.description }}

+
+
${{ product.price }}
+
+
+ {% else %} +
+
Product not found
+

The product you are looking for does not exist.

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py new file mode 100644 index 0000000..7e31f71 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py @@ -0,0 +1,3 @@ +from src.view.product.routes import product_blueprint +from src.view.auth.routes import auth_blueprint +from src.view.main.routes import main_blueprint \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py new file mode 100644 index 0000000..eda7766 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py @@ -0,0 +1,39 @@ +from flask_wtf import FlaskForm +from wtforms.fields import StringField, PasswordField, EmailField, SubmitField +from wtforms.validators import DataRequired, length, ValidationError +from string import ascii_lowercase, ascii_uppercase, digits + + +class RegisterForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)]) + email = EmailField("Enter email", validators=[DataRequired()]) + password = PasswordField("Enter password", validators=[DataRequired()]) + + submit = SubmitField("Register") + + def validate_password(self, field): + contains_upcase = False + contains_lowcase = False + contains_digits = False + + for char in field.data: + if char in ascii_uppercase: + contains_upcase=True + if char in ascii_lowercase: + contains_lowcase=True + if char in digits: + contains_digits=True + + if contains_upcase == False: + raise ValidationError("Password must contain UpperCase Letter") + if contains_lowcase == False: + raise ValidationError("Password must contain LowerCase Letter") + if contains_digits == False: + raise ValidationError("Password must contain Digits Letter") + + +class LoginForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired()]) + password = PasswordField("Enter Password", validators=[DataRequired()]) + + submit = SubmitField("Log in") diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py new file mode 100644 index 0000000..ec57fcb --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py @@ -0,0 +1,78 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request +from src.view.auth.forms import RegisterForm, LoginForm +from src.models import User +from flask_login import login_user, logout_user, login_required + + +auth_blueprint = Blueprint("auth", __name__) + + +@auth_blueprint.route("/register", methods=['GET', 'POST']) +def register(): + form = RegisterForm() + if form.validate_on_submit(): + print(form.username.data) + print(form.email.data) + print(form.password.data) + flash("Sagol shen daregistrirdi", "info") + print('ar xar') + user = User(form.username.data, form.email.data, form.password.data) + user.create() + # return render_template('index.html') + else: + print('bad xar1') + print(form.errors) + print('bad xaar2') + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/login", methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter(User.username == form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + + next = request.args.get("next") + + if next: + return redirect(next) + flash("Logged in!", "success") + return redirect(url_for("main.index")) + else: + flash("Username or Password is incorrect", "danger") + + return render_template("auth/login.html", form=form) + + +@auth_blueprint.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("main.index")) + + +@auth_blueprint.route("/edit/", methods=['GET', 'POST']) +@login_required +def edit(user_id): + user = User.query.get(user_id) + oldname = user.username + form = RegisterForm(username=user.username, email=user.email, password=user.password) + if form.validate_on_submit(): + user.edit(form) + print(f"{oldname} has changed it own name to {user.username}") + else: + print(form.errors) + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/delete/") +@login_required +def delete(user_id): + usertodel = User.query.get(user_id) + # User.query.filter_by() + print(usertodel) + usertodel.delete() + print(url_for("main.index")) + return redirect(url_for("main.index")) \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py new file mode 100644 index 0000000..79835cc --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +main_blueprint = Blueprint("main", __name__) + + +@main_blueprint.route("/") +def index(): + products = Product.query.all() + return render_template("main/index.html", products=products) + + +@main_blueprint.route("/feedback") +def feedback(): + return render_template("main/feedback.html") \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py new file mode 100644 index 0000000..a2f8d83 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +product_blueprint = Blueprint("product", __name__) + + +@product_blueprint.route("/view/") +def view(product_id): + product = Product.query.get(product_id) + print(product) + return render_template("product/view_product.html", product=product) \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py new file mode 100644 index 0000000..d4526e9 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py @@ -0,0 +1,76 @@ +from flask_login import current_user +from src.models import User + + +def test_login(client): + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + print(client.get().data) + assert current_user.is_authenticated == True + + client.get('/logout') + assert current_user.is_authenticated == False + + client.post("/login", data={"username": "mari", "password": "Mari13"}) + print(client.get().data) + assert current_user.is_authenticated == False + + +def test_register_get(client): + response = client.get("/register") + assert response.status_code == 200 + assert b"Register" in response.data or b"username" in response.data + + +def test_register_post_valid(client, app): + response = client.post("/register", data={ + "username": "gio", + "email": "gio@gmail.com", + "password": "Gio123" + }, follow_redirects=True) + + assert b"Sagol shen daregistrirdi" in response.data + + with app.app_context(): + user = User.query.filter_by(username="gio").first() + assert user is not None + assert user.email == "gio@gmail.com" + + +def test_logout(client, app): + # with app.app_context(): + # user = User("mari", "mari@gmail.com", "Mari123") + # user.create() + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + assert current_user.is_authenticated == True + + response = client.get("/logout", follow_redirects=True) + assert not current_user.is_authenticated + + +def test_edit(client, app): + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + + client.post("/edit/1", data={"username": "marikuna", "email": "mari@gmail.com", "password": "Mari1234"}) + + with app.app_context(): + user = User.query.get(1) + assert user.username=="marikuna" + + +def test_delete(client, app): + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + response = client.get(f'/delete/{1}', follow_redirects=True) + assert response.status_code == 200 + + with app.app_context(): + user = User.query.get(1) + assert user is None + + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py new file mode 100644 index 0000000..30593c7 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py @@ -0,0 +1,43 @@ +import tempfile +import os +import pytest +from src import create_app +from src.ext import admin +from src.commands import init_db, populate_db + + +@pytest.fixture +def app(): + # Create a temporary file + db_fd, db_path = tempfile.mkstemp() + + test_config = { + "TESTING": True, + "WTF_CSRF_ENABLED": False, + "DEBUG": False, + "SECRET_KEY": "test-secret-key", + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}.sqlite", # Use temporary SQLite file + } + + app = create_app(test_config=test_config) + admin._views = [] + + with app.app_context(): + init_db() + populate_db() + + yield app + + # Cleanup + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture +def client(app): + return app.test_client() + + +@pytest.fixture +def server(app): + return app.test_cli_runner() diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py new file mode 100644 index 0000000..1ae9a2a --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py @@ -0,0 +1,12 @@ +# from tests.conftest import client, app, server + + +def test_main(client): + response = client.get('/') + assert response.status_code == 200 + + +def test_feedback(client): + response = client.get('/feedback') + assert response.status_code == 200 + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py new file mode 100644 index 0000000..96bce4e --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py @@ -0,0 +1,15 @@ + +def test_view(client): + response = client.get('/view/1') + + assert response.status_code == 200 + + assert b"11.0" in response.data + + response = client.get('/view/7') + + assert response.status_code == 200 + + assert b"Product not found" in response.data + + diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py new file mode 100644 index 0000000..545f28b --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py @@ -0,0 +1,59 @@ +# import pytest +# +# +# def sum(a, b): +# return a+b +# +# +# def test_sum(): +# assert sum(5, 5) == 10 +# assert sum(5, 0) == 5 +# +# +# class User: +# real_person = True +# +# def __init__(self, name, age): +# self.name = name +# self.age = age +# +# def sum(self, x): +# return self.age + x +# +# @classmethod +# def is_real_person(cls): +# return cls.real_person +# +# @classmethod +# def get_class(cls): +# return cls +# +# +# def test_class(): +# user = User("gio", 15) +# assert user.sum(-2) == 13 +# assert user.is_real_person() == True +# assert User.is_real_person() == True +# assert User.get_class().real_person == True +# assert user.get_class().real_person == True +# +# +# @pytest.fixture(autouse=True) +# def ini(request): +# if request.cls is not None: # Only for class-based tests +# request.cls.user = User("mari", 15) +# request.cls.p = 3.14159 +# +# +# @pytest.mark.usefixtures("ini") +# class Testsmt: +# def test_user(self): +# assert self.user.name=="mari" +# +# @pytest.mark.skip +# def test_pruwi(self): +# assert self.p*5==3.14159*0 +# +# +# +# diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf new file mode 100644 index 0000000..9ca0486 --- /dev/null +++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf @@ -0,0 +1,10 @@ +[uwsgi] + +master = true +module = src:create_app() +processes = 2 +threads = 4 +socket = :5000 +chmod-socket = 600 +die-on-term = true +vacuum = true \ No newline at end of file diff --git a/Chapter10_TestDrivenDevelopment/Projects/readme.md b/Chapter10_TestDrivenDevelopment/Projects/readme.md index 99fb713..0bd7f37 100644 --- a/Chapter10_TestDrivenDevelopment/Projects/readme.md +++ b/Chapter10_TestDrivenDevelopment/Projects/readme.md @@ -1,6 +1,6 @@ # Unit ტესტინგის დირექტორია - სახელი გვარი | [პროექტი](/მისამართი) - +- giorgi kelenjeridze | [Food order app](/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py) ### 2025 ზაფხული diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile b/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile new file mode 100644 index 0000000..a2bbee7 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.10-slim-bookworm + +WORKDIR /giorgikelenjeridze + +COPY . . + +RUN apt-get update && \ + apt-get -y install gcc build-essential python3-dev postgresql-client && \ + pip install -r prod-requirements.txt + +COPY ./entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + +EXPOSE 5000 + +CMD ["sh", "./entrypoint.sh"] diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/app.py b/Chapter11_Docker/Projects/giorgikelenjeridze/app.py new file mode 100644 index 0000000..ce3460f --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/app.py @@ -0,0 +1,6 @@ +from src import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt new file mode 100644 index 0000000..a37387c Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml b/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml new file mode 100644 index 0000000..92b0953 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml @@ -0,0 +1,40 @@ + +services: + nginx: + build: ./nginx + container_name: nginx + ports: + - "80:80" + restart: always + + flask: + build: . + container_name: flask_docker + expose: + - 5000 + environment: + SECRET_KEY: Real_SecreT-S!gma-Key_!23 + DB_USER: python_intern-user + DB_PASSWORD: Wyali!12345 + DB_DATABASE: python_intern-db + DB_HOST: postgres_db + DB_PORT: 5432 + depends_on: + - postgres_db + restart: always + + postgres_db: + image: postgres:15 + container_name: postgres_db + environment: + POSTGRES_USER: python_intern-user + POSTGRES_PASSWORD: Wyali!12345 + POSTGRES_DB: python_intern-db + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + +volumes: + pgdata: \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh b/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh new file mode 100644 index 0000000..f0a6d19 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh @@ -0,0 +1,19 @@ +echo "Waiting for Postgres to be ready..." +while ! pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER"; do + sleep 1 +done +echo "Postgres is ready." + +# Check if 'products' table exists +TABLE_EXISTS=$(psql "postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_DATABASE" -tAc "SELECT to_regclass('public.products');") + +if [ "$TABLE_EXISTS" = "" ]; then + echo "Database not initialized. Running migrations and populating data..." + flask init_db + flask populate_db +else + echo "Database already initialized. Skipping init." +fi + +# Start Flask app with uwsgi +exec uwsgi uwsgi.ini \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py new file mode 100644 index 0000000..4476bfd --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py @@ -0,0 +1,32 @@ +"""Added description to Product + +Revision ID: 2be0fc404953 +Revises: +Create Date: 2025-05-31 19:42:43.908055 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2be0fc404953' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.add_column(sa.Column('description', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('products', schema=None) as batch_op: + batch_op.drop_column('description') + + # ### end Alembic commands ### diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py new file mode 100644 index 0000000..5ab938d --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py @@ -0,0 +1,38 @@ +"""Changed field + +Revision ID: 9b4f3a94e264 +Revises: 2be0fc404953 +Create Date: 2025-06-02 15:49:00.933969 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9b4f3a94e264' +down_revision = '2be0fc404953' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('_password', sa.String(), nullable=True)) + batch_op.create_unique_constraint(None, ['email']) + batch_op.create_unique_constraint(None, ['username']) + batch_op.drop_column('password') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True)) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('_password') + + # ### end Alembic commands ### diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile new file mode 100644 index 0000000..19306bf --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.29.0 + +RUN rm /etc/nginx/conf.d/default.conf +COPY default.conf /etc/nginx/conf.d/ \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf new file mode 100644 index 0000000..19267d2 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name 127.0.0.1; + + location / { + include uwsgi_params; + uwsgi_pass flask_docker:5000; + + } + + +} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt new file mode 100644 index 0000000..233b715 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py new file mode 100644 index 0000000..1b7a8cf --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py @@ -0,0 +1,64 @@ +from flask import Flask +from src.admin_views.base import SecureModelView +from src.admin_views import UserView, ProductView +from src.config import Config +from src.ext import db, migrate, login_manager, admin +from src.view import product_blueprint, auth_blueprint, main_blueprint +from src.commands import init_db_command, populate_db_command +from src.models import User, Product +from flask_admin.menu import MenuLink + +Blueprints = [product_blueprint, auth_blueprint, main_blueprint] +Commands = [init_db_command, populate_db_command] + + +def create_app(test_config=None): + + app = Flask(__name__) + app.config.from_object(Config) + + # Then override with test config if passed + if test_config: + app.config.update(test_config) + + register_extensions(app) + register_blueprints(app) + register_commands(app) + + return app + + +def register_extensions(app): + + # Flask-SQLAlchemy + db.init_app(app) + + # Flask-Migrate + migrate.init_app(app, db) + + # Flask-LoginManager + login_manager.init_app(app) + login_manager.login_view = 'user.login' + + @login_manager.user_loader + def load_user(_id): + return User.query.get(_id) + + # Flask-Admin + admin.init_app(app) + admin.add_view(UserView(User, db.session)) + admin.add_view(ProductView(Product, db.session, endpoint="product_admin")) + + admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out")) + + +def register_blueprints(app): + for blueprint in Blueprints: + app.register_blueprint(blueprint) + + +def register_commands(app): + for command in Commands: + app.cli.add_command(command) + + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py new file mode 100644 index 0000000..8e52cd1 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py @@ -0,0 +1,2 @@ +from src.admin_views.user import UserView +from src.admin_views.product import ProductView diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py new file mode 100644 index 0000000..4f3c2d9 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py @@ -0,0 +1,24 @@ +from flask_admin.contrib.sqla import ModelView +from flask_admin import AdminIndexView +from flask_login import current_user +from flask import redirect, url_for + + +class SecureModelView(ModelView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + +class SecureIndexView(AdminIndexView): + def is_accessible(self): + return current_user.is_authenticated + + def inaccessible_callback(self, name, **kwargs): + if not self.is_accessible(): + return redirect(url_for("auth.login")) + + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py new file mode 100644 index 0000000..950ef52 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py @@ -0,0 +1,33 @@ +from src.admin_views.base import SecureModelView +from src.config import Config + +from flask_admin.form import ImageUploadField +from os import path +from uuid import uuid4 +from markupsafe import Markup + + +def generate_filename(obj, file): + name, extension = path.splitext(file.filename) + return f"{uuid4()}{extension}" + + +class ProductView(SecureModelView): + create_modal = True + edit_modal = True + + column_editable_list = ("name", "price", "description") + column_filters = ["price",] + + column_formatters = { + "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...", + "image": lambda v,c,m,n: Markup(f"") + } + + form_overrides = {"image": ImageUploadField} + form_args = { + "image": { + "base_path": Config.UPLOAD_PATH, + "namegen": generate_filename + } + } \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py new file mode 100644 index 0000000..22dbec3 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py @@ -0,0 +1,10 @@ +from src.admin_views.base import SecureModelView + + +class UserView(SecureModelView): + can_view_details = True + can_delete = False + + column_list = ("username", "email") + column_details_list = ("username", "email", "_password") + column_searchable_list = ["username"] \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py new file mode 100644 index 0000000..721340b --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py @@ -0,0 +1,93 @@ +from flask.cli import with_appcontext +import datetime +import click +from src.ext import db +from src.models import Product, Person, IDcard, User + + +def init_db(): + + db.drop_all() + db.create_all() + + +@click.command("init_db") +@with_appcontext +def init_db_command(): + click.echo("Initializing db...") + + init_db() + + click.echo("Initializing is over!") + + +def populate_db(): + products = [ + { + "id": 0, + "name": "შაურმა პატარა", + "price": 11, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 1, + "name": "შაურმა სტანდარტი", + "price": 13, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 2, + "name": "შაურმა დიდი", + "price": 18, + "image": "shaurma.jfif", + "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი" + }, + { + "id": 3, + "name": "კოლა", + "price": 3, + "image": "cola.jfif", + "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ" + }, + { + "id": 4, + "name": "სპრაიტი", + "price": 3, + "image": "sprite.jfif", + "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ" + }, + { + "id": 5, + "name": "ბურგერი", + "price": 10, + "image": "burger.png", + "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი" + } + ] + + for product in products: + new_product = Product(name=product["name"], price=product["price"], description=product["description"], + image=product["image"]) + db.session.add(new_product) + + idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now()) + db.session.add(idcard) + db.session.commit() + person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id) + db.session.add(person) + user = User("mari", "marikuna@gmail.com", "Mari123") + db.session.add(user) + db.session.commit() + + +@click.command("populate_db") +@with_appcontext +def populate_db_command(): + + populate_db() + + + + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py new file mode 100644 index 0000000..3c26a29 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py @@ -0,0 +1,17 @@ +from os import environ, path + + +class Config: + + DB_USER = environ.get("DB_USER") + DB_PASSWORD = environ.get("DB_PASSWORD") + DB_DATABASE = environ.get("DB_DATABASE") + + # print(DB_USER) + # print(DB_PASSWORD) + # print(DB_DATABASE) + BASE_DIRECTORY = path.abspath(path.dirname(__file__)) + UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets") + SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@postgres_db:5432/{DB_DATABASE}" + SECRET_KEY = environ.get("SECRET_KEY") + FLASK_ADMIN_SWATCH="Flatly" \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py new file mode 100644 index 0000000..b1e63af --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py @@ -0,0 +1,10 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +from flask_admin import Admin +from src.admin_views.base import SecureIndexView + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() +admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html") \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py new file mode 100644 index 0000000..9e8a271 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py @@ -0,0 +1,4 @@ +from src.models.base import BaseModel +from src.models.person import Person, IDcard +from src.models.product import Product +from src.models.user import User \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py new file mode 100644 index 0000000..6f33fa7 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py @@ -0,0 +1,19 @@ +from src.ext import db + + +class BaseModel(db.Model): + + __abstract__ = True + + def create(self, commit=True): + db.session.add(self) + if commit: + self.save() + + def save(self): + db.session.commit() + + def delete(self, commit=True): + db.session.delete(self) + if commit: + self.save() diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py new file mode 100644 index 0000000..93875a4 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py @@ -0,0 +1,26 @@ +from src.ext import db +from src.models import BaseModel + + +class Person(BaseModel): + + __tablename__ = "persons" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + surname = db.Column(db.String) + birthday = db.Column(db.Date) + + idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id")) + + idcard = db.relationship("IDcard", back_populates="person") + + +class IDcard(BaseModel): + + __tablename__ = "idcards" + id = db.Column(db.Integer, primary_key=True) + serial_number = db.Column(db.String) + expiry_data = db.Column(db.Date) + + person = db.relationship("Person", back_populates="idcard", uselist=False) + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py new file mode 100644 index 0000000..da5ae3c --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py @@ -0,0 +1,22 @@ +from src.ext import db +from src.models import BaseModel + + +class Product(BaseModel): + + __tablename__ = "products" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String) + price = db.Column(db.Float) + description = db.Column(db.String) + image = db.Column(db.String) + + def __init__(self, name, price, description, image): + self.name = name + self.price = price + self.description = description + self.image = image + + def __repr__(self): + return f"This is {self.name} and costs {self.price}$" + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py new file mode 100644 index 0000000..3a3129f --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py @@ -0,0 +1,38 @@ +from src.ext import db +from src.models import BaseModel +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(BaseModel, UserMixin): + + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, unique=True) + email = db.Column(db.String, unique=True) + _password = db.Column(db.String) + + def __init__(self, username, email, password): + self.username = username + self.email = email + self._password = generate_password_hash(password) + + def __repr__(self): + return f"User name: {self.username}" + + def edit(self, form): + self.username = form.username.data + self.password = form.password.data + db.session.commit() + + def check_password(self, password): + return check_password_hash(self.password, password) + + @property + def password(self): + return self._password + + @password.setter + def password(self, password): + self._password = generate_password_hash(password) + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png new file mode 100644 index 0000000..61f8ccd Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png new file mode 100644 index 0000000..c5bb973 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif new file mode 100644 index 0000000..4d83f30 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif new file mode 100644 index 0000000..9cc4780 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif new file mode 100644 index 0000000..e775e42 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif new file mode 100644 index 0000000..128f4c4 Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html new file mode 100644 index 0000000..7e73a33 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html @@ -0,0 +1,42 @@ +{% extends "admin/base.html" %} + +{% block page_body %} + + +
+ + {% block messages %} + {{ layout.messages() }} + {% endblock %} + + {# store the jinja2 context for form_rules rendering logic #} + {% set render_ctx = h.resolve_ctx() %} + + {% block body %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html new file mode 100644 index 0000000..6454ecc --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html @@ -0,0 +1,30 @@ +{% extends "partials/base.html" %} +{% block title%}Log In{% endblock %} + +{% block body %} +
+ +
+{% endblock %} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html new file mode 100644 index 0000000..d611686 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html @@ -0,0 +1,46 @@ +{% extends "partials/base.html" %} +{% block title %}Register{% endblock %} + +{% block body %} + + +
+
+
+

Create Your Account

+
+ {% for errors in form.errors.values() %} + {% for error in errors %} +

*{{ error }}

+ {% endfor %} + {% endfor %} + {{ form.hidden_tag() }} + + {{ form.username(placeholder=form.username.label.text, class="form-control") }} +
+ {{ form.email(placeholder=form.email.label.text, class="form-control") }} +
+ {{ form.password(placeholder=form.password.label.text, class="form-control") }} +
+ {{ form.submit(placeholder=form.submit.label.text, class="form-control") }} +
+
+
+
+ + + + +{% endblock %} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html new file mode 100644 index 0000000..4674ec6 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html @@ -0,0 +1,5 @@ +{% macro is_active(endpoint) %} + {% if endpoint==request.endpoint %} + active + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html new file mode 100644 index 0000000..792387f --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html @@ -0,0 +1,72 @@ +{% extends "partials/base.html" %} +{% block title %}feedback{% endblock %} +{% block body %} + +
+

We Value Your Feedback

+ + +
+ + +
+ + +
+ + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +{% endblock %} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html new file mode 100644 index 0000000..cd35a37 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html @@ -0,0 +1,48 @@ +{% extends "partials/base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+

My Simple "Food Order" Web Application

+
+ Main Shaurma + +
+

ჩვენი მენიუ

+ +
+ + + + + + +
+

ჩვენი ფილიალები

+
    +
  • გლდანი, ა მიკრო
  • +
  • გლანი, I მიკრო
  • +
  • საბურთალო
  • +
+
+ +
+ +{% endblock %} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html new file mode 100644 index 0000000..6821ae2 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html @@ -0,0 +1,33 @@ + + + + + Kelenjo {% block title %} {% endblock %} + + + + + {% include "partials/navbar.html" %} +{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #} + +{% if get_flashed_messages(with_categories=True) %} +{% for category, message in get_flashed_messages(with_categories=True) %} + +{% endfor %} +{% endif %} + {% block body %} + {% endblock %} + + + \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html new file mode 100644 index 0000000..4e06a3e --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html @@ -0,0 +1,23 @@ +{% from "macros.html" import is_active %} + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html new file mode 100644 index 0000000..57051ab --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html @@ -0,0 +1,29 @@ +{% extends "partials/base.html" %} + +{% block title %}{{ product.name if product is not none else "Product Not Found" }}{% endblock %} + +{% block body %} +
+
+
+
+ {% if product is not none %} + {{ product.name }} +
+
{{ product.name }}
+

{{ product.description }}

+
+
${{ product.price }}
+
+
+ {% else %} +
+
Product not found
+

The product you are looking for does not exist.

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py new file mode 100644 index 0000000..7e31f71 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py @@ -0,0 +1,3 @@ +from src.view.product.routes import product_blueprint +from src.view.auth.routes import auth_blueprint +from src.view.main.routes import main_blueprint \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py new file mode 100644 index 0000000..eda7766 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py @@ -0,0 +1,39 @@ +from flask_wtf import FlaskForm +from wtforms.fields import StringField, PasswordField, EmailField, SubmitField +from wtforms.validators import DataRequired, length, ValidationError +from string import ascii_lowercase, ascii_uppercase, digits + + +class RegisterForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)]) + email = EmailField("Enter email", validators=[DataRequired()]) + password = PasswordField("Enter password", validators=[DataRequired()]) + + submit = SubmitField("Register") + + def validate_password(self, field): + contains_upcase = False + contains_lowcase = False + contains_digits = False + + for char in field.data: + if char in ascii_uppercase: + contains_upcase=True + if char in ascii_lowercase: + contains_lowcase=True + if char in digits: + contains_digits=True + + if contains_upcase == False: + raise ValidationError("Password must contain UpperCase Letter") + if contains_lowcase == False: + raise ValidationError("Password must contain LowerCase Letter") + if contains_digits == False: + raise ValidationError("Password must contain Digits Letter") + + +class LoginForm(FlaskForm): + username = StringField("Enter Username", validators=[DataRequired()]) + password = PasswordField("Enter Password", validators=[DataRequired()]) + + submit = SubmitField("Log in") diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py new file mode 100644 index 0000000..ec57fcb --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py @@ -0,0 +1,78 @@ +from flask import Blueprint, render_template, flash, redirect, url_for, request +from src.view.auth.forms import RegisterForm, LoginForm +from src.models import User +from flask_login import login_user, logout_user, login_required + + +auth_blueprint = Blueprint("auth", __name__) + + +@auth_blueprint.route("/register", methods=['GET', 'POST']) +def register(): + form = RegisterForm() + if form.validate_on_submit(): + print(form.username.data) + print(form.email.data) + print(form.password.data) + flash("Sagol shen daregistrirdi", "info") + print('ar xar') + user = User(form.username.data, form.email.data, form.password.data) + user.create() + # return render_template('index.html') + else: + print('bad xar1') + print(form.errors) + print('bad xaar2') + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/login", methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter(User.username == form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + + next = request.args.get("next") + + if next: + return redirect(next) + flash("Logged in!", "success") + return redirect(url_for("main.index")) + else: + flash("Username or Password is incorrect", "danger") + + return render_template("auth/login.html", form=form) + + +@auth_blueprint.route("/logout") +@login_required +def logout(): + logout_user() + return redirect(url_for("main.index")) + + +@auth_blueprint.route("/edit/", methods=['GET', 'POST']) +@login_required +def edit(user_id): + user = User.query.get(user_id) + oldname = user.username + form = RegisterForm(username=user.username, email=user.email, password=user.password) + if form.validate_on_submit(): + user.edit(form) + print(f"{oldname} has changed it own name to {user.username}") + else: + print(form.errors) + return render_template("auth/register.html", form=form) + + +@auth_blueprint.route("/delete/") +@login_required +def delete(user_id): + usertodel = User.query.get(user_id) + # User.query.filter_by() + print(usertodel) + usertodel.delete() + print(url_for("main.index")) + return redirect(url_for("main.index")) \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py new file mode 100644 index 0000000..79835cc --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +main_blueprint = Blueprint("main", __name__) + + +@main_blueprint.route("/") +def index(): + products = Product.query.all() + return render_template("main/index.html", products=products) + + +@main_blueprint.route("/feedback") +def feedback(): + return render_template("main/feedback.html") \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py new file mode 100644 index 0000000..a2f8d83 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from src.models.product import Product + +product_blueprint = Blueprint("product", __name__) + + +@product_blueprint.route("/view/") +def view(product_id): + product = Product.query.get(product_id) + print(product) + return render_template("product/view_product.html", product=product) \ No newline at end of file diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py new file mode 100644 index 0000000..d4526e9 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py @@ -0,0 +1,76 @@ +from flask_login import current_user +from src.models import User + + +def test_login(client): + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + print(client.get().data) + assert current_user.is_authenticated == True + + client.get('/logout') + assert current_user.is_authenticated == False + + client.post("/login", data={"username": "mari", "password": "Mari13"}) + print(client.get().data) + assert current_user.is_authenticated == False + + +def test_register_get(client): + response = client.get("/register") + assert response.status_code == 200 + assert b"Register" in response.data or b"username" in response.data + + +def test_register_post_valid(client, app): + response = client.post("/register", data={ + "username": "gio", + "email": "gio@gmail.com", + "password": "Gio123" + }, follow_redirects=True) + + assert b"Sagol shen daregistrirdi" in response.data + + with app.app_context(): + user = User.query.filter_by(username="gio").first() + assert user is not None + assert user.email == "gio@gmail.com" + + +def test_logout(client, app): + # with app.app_context(): + # user = User("mari", "mari@gmail.com", "Mari123") + # user.create() + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + assert current_user.is_authenticated == True + + response = client.get("/logout", follow_redirects=True) + assert not current_user.is_authenticated + + +def test_edit(client, app): + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + + client.post("/edit/1", data={"username": "marikuna", "email": "mari@gmail.com", "password": "Mari1234"}) + + with app.app_context(): + user = User.query.get(1) + assert user.username=="marikuna" + + +def test_delete(client, app): + + with client: + client.post("/login", data={"username": "mari", "password": "Mari123"}) + response = client.get(f'/delete/{1}', follow_redirects=True) + assert response.status_code == 200 + + with app.app_context(): + user = User.query.get(1) + assert user is None + + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py new file mode 100644 index 0000000..30593c7 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py @@ -0,0 +1,43 @@ +import tempfile +import os +import pytest +from src import create_app +from src.ext import admin +from src.commands import init_db, populate_db + + +@pytest.fixture +def app(): + # Create a temporary file + db_fd, db_path = tempfile.mkstemp() + + test_config = { + "TESTING": True, + "WTF_CSRF_ENABLED": False, + "DEBUG": False, + "SECRET_KEY": "test-secret-key", + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}.sqlite", # Use temporary SQLite file + } + + app = create_app(test_config=test_config) + admin._views = [] + + with app.app_context(): + init_db() + populate_db() + + yield app + + # Cleanup + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture +def client(app): + return app.test_client() + + +@pytest.fixture +def server(app): + return app.test_cli_runner() diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py new file mode 100644 index 0000000..1ae9a2a --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py @@ -0,0 +1,12 @@ +# from tests.conftest import client, app, server + + +def test_main(client): + response = client.get('/') + assert response.status_code == 200 + + +def test_feedback(client): + response = client.get('/feedback') + assert response.status_code == 200 + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py new file mode 100644 index 0000000..96bce4e --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py @@ -0,0 +1,15 @@ + +def test_view(client): + response = client.get('/view/1') + + assert response.status_code == 200 + + assert b"11.0" in response.data + + response = client.get('/view/7') + + assert response.status_code == 200 + + assert b"Product not found" in response.data + + diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py new file mode 100644 index 0000000..545f28b --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py @@ -0,0 +1,59 @@ +# import pytest +# +# +# def sum(a, b): +# return a+b +# +# +# def test_sum(): +# assert sum(5, 5) == 10 +# assert sum(5, 0) == 5 +# +# +# class User: +# real_person = True +# +# def __init__(self, name, age): +# self.name = name +# self.age = age +# +# def sum(self, x): +# return self.age + x +# +# @classmethod +# def is_real_person(cls): +# return cls.real_person +# +# @classmethod +# def get_class(cls): +# return cls +# +# +# def test_class(): +# user = User("gio", 15) +# assert user.sum(-2) == 13 +# assert user.is_real_person() == True +# assert User.is_real_person() == True +# assert User.get_class().real_person == True +# assert user.get_class().real_person == True +# +# +# @pytest.fixture(autouse=True) +# def ini(request): +# if request.cls is not None: # Only for class-based tests +# request.cls.user = User("mari", 15) +# request.cls.p = 3.14159 +# +# +# @pytest.mark.usefixtures("ini") +# class Testsmt: +# def test_user(self): +# assert self.user.name=="mari" +# +# @pytest.mark.skip +# def test_pruwi(self): +# assert self.p*5==3.14159*0 +# +# +# +# diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini b/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini new file mode 100644 index 0000000..9ca0486 --- /dev/null +++ b/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini @@ -0,0 +1,10 @@ +[uwsgi] + +master = true +module = src:create_app() +processes = 2 +threads = 4 +socket = :5000 +chmod-socket = 600 +die-on-term = true +vacuum = true \ No newline at end of file diff --git a/Chapter11_Docker/Projects/readme.md b/Chapter11_Docker/Projects/readme.md index f5ad8b1..7bd71a0 100644 --- a/Chapter11_Docker/Projects/readme.md +++ b/Chapter11_Docker/Projects/readme.md @@ -1,5 +1,5 @@ # დოკერის დირექტორია - სახელი გვარი | [პროექტი](/მისამართი) - +- giorgi kelenjeridze | [Food order App](https://hub.docker.com/r/kelenjo/flask_app) ### 2025 ზაფხული \ No newline at end of file