diff --git a/README.md b/README.md index 0d72aff..4591e37 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ # Python Demo Projects * [Movie Collection App](https://github.com/valeriybercha/python-demos/tree/master/movie-collection-app) -* [Tic-Tac-Toe game](https://github.com/valeriybercha/python-demos/tree/master/tic-tac-toe) \ No newline at end of file +* [Tic-Tac-Toe game](https://github.com/valeriybercha/python-demos/tree/master/tic-tac-toe) +* [Text Pro App](https://github.com/valeriybercha/python-demos/tree/master/text-pro-app) +* [Tkinter](https://github.com/valeriybercha/python-demos/tree/master/tkinter) +* [GUI Calculator](https://github.com/valeriybercha/python-demos/tree/master/gui-calculator) +* [Password Generator](https://github.com/valeriybercha/python-demos/tree/master/password-generator) +* [To Do App with Flask framework](https://github.com/valeriybercha/py-todo-app) +* [Covid Telegram Bot](https://github.com/valeriybercha/py-covid-telegram-bot) +* [To Do App simplified version](https://github.com/valeriybercha/py-todo-s-app) + diff --git a/flask-add-users/.gitignore b/flask-add-users/.gitignore new file mode 100644 index 0000000..92afa22 --- /dev/null +++ b/flask-add-users/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +venv/ diff --git a/flask-add-users/README.md b/flask-add-users/README.md new file mode 100644 index 0000000..088ccce --- /dev/null +++ b/flask-add-users/README.md @@ -0,0 +1,21 @@ +# Flask demo application for adding users to the database + +### App functionality: +- Add users to the database +- Display added users +- Delete users + +### Usage: + +Pre-requisites: +- Python should be already installed on the machine ```python3 --version``` to verify python installation + +1. Clone or download the project +2. Create a virtual environment inside the project: ```python3 -m venv venv``` +3. Activate the virtual environment: ```source venv/bin/activate``` +4. Install Flask package: ```pip install flask``` +5. Install Flask SQLAlchemy package: ```pip install flask-sqlalchemy``` +6. Start the application server: + - ```export FLASK_DEBUG=1``` + - ```python app.py``` +7. Open the browser at: ```localhost:5000``` diff --git a/flask-add-users/app.py b/flask-add-users/app.py new file mode 100644 index 0000000..34c6373 --- /dev/null +++ b/flask-add-users/app.py @@ -0,0 +1,76 @@ +# 'Add users to the database' demo app on Python and Flask +# Programming language version: Python 3.6.9 +# Developer: Valeriy B. + +from flask import Flask, render_template, url_for, request, redirect +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +app = Flask(__name__) + +app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///user.db" +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + +app_title = "Add Users App" + + +# Creating models for adding user to the database +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + first_name = db.Column(db.String(30), nullable=False) + last_name = db.Column(db.String(30), nullable=False) + date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + def __repr__(self): + return f"User {self.id}" + + +# Routing for index page +@app.route('/', methods=["POST", "GET"]) +def index(): + if request.method == "POST": + if request.form['fname'] == "": + first_name = "First name: Blank" + else: + first_name = request.form['fname'].title() + if request.form['lname'] == "": + last_name = "Last name: Blank" + else: + last_name = request.form['lname'].title() + user = User(first_name=first_name, last_name=last_name) + db.session.add(user) + db.session.commit() + users = User.query.all() + return render_template('index.html', app_title=app_title, users=users) + else: + users = User.query.all() + return render_template('index.html', app_title=app_title, users=users) + + +# Routing for updating user information +@app.route('/update/', methods=['POST', 'GET']) +def user_update(id): + if request.method == "POST": + updated_user = User.query.get_or_404(id) + updated_user.first_name = request.form['fname'] + updated_user.last_name = request.form['lname'] + db.session.commit() + return redirect(url_for('index')) + else: + user_to_update = User.query.get_or_404(id) + return render_template('update.html', user=user_to_update) + + +# Routing for deleting a user from the database +@app.route('/del/') +def user_delete(id): + user_to_delete = User.query.get_or_404(id) + db.session.delete(user_to_delete) + db.session.commit() + return redirect(url_for('index')) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-add-users/requirements.txt b/flask-add-users/requirements.txt new file mode 100644 index 0000000..cf76820 --- /dev/null +++ b/flask-add-users/requirements.txt @@ -0,0 +1,13 @@ +click==8.0.3 +dataclasses==0.8 +Flask==2.0.2 +Flask-SQLAlchemy==2.5.1 +importlib-metadata==4.8.1 +itsdangerous==2.0.1 +Jinja2==3.0.2 +MarkupSafe==2.0.1 +pkg-resources==0.0.0 +SQLAlchemy==1.4.26 +typing-extensions==3.10.0.2 +Werkzeug==2.0.2 +zipp==3.6.0 diff --git a/flask-add-users/static/main.css b/flask-add-users/static/main.css new file mode 100644 index 0000000..a349e85 --- /dev/null +++ b/flask-add-users/static/main.css @@ -0,0 +1,11 @@ +header { + padding: 50px 0 30px 0; +} + +h1.app, h2.app { + font-family: 'Comfortaa', cursive; +} + +h2.app { + padding-bottom: 20px; +} diff --git a/flask-add-users/templates/index.html b/flask-add-users/templates/index.html new file mode 100644 index 0000000..87343f9 --- /dev/null +++ b/flask-add-users/templates/index.html @@ -0,0 +1,52 @@ +{% extends 'layout.html' %} + +{% block main %} +
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + {% for user in users[::-1] %} + + + + + + + + {% endfor %} + +
#First NameLast NameDate AddedAction
{{ users[::-1].index(user) + 1 }}{{ user.first_name }}{{ user.last_name }}{{ user.date_added.strftime('%B %d %Y - %H:%M:%S') }} + Edit + Delete +
+
+
+
+{% endblock main %} diff --git a/flask-add-users/templates/layout.html b/flask-add-users/templates/layout.html new file mode 100644 index 0000000..183ec7d --- /dev/null +++ b/flask-add-users/templates/layout.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + {{ app_title }} + + +
+
+

{{ app_title }}

+
+
+ {% block main %}{% endblock main %} +
+
+ + + + + + + + diff --git a/flask-add-users/templates/update.html b/flask-add-users/templates/update.html new file mode 100644 index 0000000..06c9420 --- /dev/null +++ b/flask-add-users/templates/update.html @@ -0,0 +1,23 @@ +{% extends 'layout.html' %} + +{% block main %} +
+

Update User

+
+
+
+
+ + +
+
+ + +
+ + Back home +
+
+
+
+{% endblock main %} diff --git a/flask-add-users/user.db b/flask-add-users/user.db new file mode 100644 index 0000000..36efe70 Binary files /dev/null and b/flask-add-users/user.db differ diff --git a/flask-app-register-login/.README.md.swp b/flask-app-register-login/.README.md.swp new file mode 100644 index 0000000..642ad44 Binary files /dev/null and b/flask-app-register-login/.README.md.swp differ diff --git a/flask-app-register-login/.gitignore b/flask-app-register-login/.gitignore new file mode 100644 index 0000000..93526df --- /dev/null +++ b/flask-app-register-login/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ diff --git a/flask-app-register-login/README.md b/flask-app-register-login/README.md new file mode 100644 index 0000000..e0cecf3 --- /dev/null +++ b/flask-app-register-login/README.md @@ -0,0 +1,36 @@ +# User registration and login app with Flask, Flask-SQLAlchemy, Flask-WTForms and Flask-Login + +### Features: +- Register a user to the database: + - Registration form fields and fields validation: + - Username (Unique, Length: 4 - 20) + - Full Name (Length: 4 - 120) + - Email (Unique, Length: 5 - 50) + - Password (Length: 8 - 20) + - Confirm password (EqualTo: password) +- Verification if username or email already exists in the database when registering a new user +- Login into the system for registered users: + - Login form fields: + - Email (Required) + - Password (Required) +- Successfull message display when users are registered or logged into the system +- Logged user can view registered users page +- Logged user can logout from the system + +### Install and running the application: +- Clone the project from github repository: ```git svn clone https://github.com/valeriybercha/python-demos/trunk/flask-app-register-login``` +- Create project virtual environment: ```python3 -m venv venv``` +- Start the virtual environment: ```source venv/bin/activate``` +- Install the needed libraries: + - Flask: ```pip install flask``` + - Flask SQLAlchemy database: ```pip install flask-sqlalchemy``` + - Flask WTForms for forms validation: ```pip install flask-wtf``` + - WTForms email validation: ```pip install wtforms[email]``` + - Flask-Login: ```pip install flask-login``` + + OR, install all libraries from ```requirements.txt``` file: ```pip install -r requirements.txt``` +- Enable the debug mode for development server: ```export FLASK_DEBUG=1``` +- Start flask project: ```export FLASK_APP=app.py``` and ```flask run``` +- Open the project in browser with address: ```localhost:5000``` + +App was created in WSL (Windows Subsystem for Linux) environment using Nano editor diff --git a/flask-app-register-login/app.db b/flask-app-register-login/app.db new file mode 100644 index 0000000..c551031 Binary files /dev/null and b/flask-app-register-login/app.db differ diff --git a/flask-app-register-login/app.py b/flask-app-register-login/app.py new file mode 100644 index 0000000..21b3413 --- /dev/null +++ b/flask-app-register-login/app.py @@ -0,0 +1,120 @@ +# Basic app registration app created with Flask, Flask-SQLAlchemy, Flask-WTForms +# The app was created in WSL (Windows Subsystem for Linux) environment using the Nano editor + +from flask import Flask, render_template, url_for, redirect, flash +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField +from wtforms.validators import DataRequired, Email, EqualTo, ValidationError, Length +from flask_login import LoginManager, UserMixin, login_user, logout_user +from werkzeug.security import generate_password_hash, check_password_hash + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = "your_secret_key" + +db = SQLAlchemy(app) +login_manager = LoginManager(app) + + +################ FORMS SECTION - forms.py ############### + +# Creating models for user registration for FLask-SQLALchemy +class UserRegistration(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(20), unique=True, nullable=False) + fullname = db.Column(db.String(120), nullable=False) + email = db.Column(db.String(50), unique=True, nullable=False) + password = db.Column(db.String(20), nullable=False) + + def __repr__(self): + return f'User({self.username}, {self.fullname}, {self.email})' + + +# User registration form validation with Flask-WTForms +class UserForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), Length(min=4, max=20)]) + fullname = StringField('Full Name', validators=[DataRequired(), Length(min=4, max=120)]) + email = StringField('Email', validators=[DataRequired(), Email(), Length(min=5, max=50)]) + password = PasswordField('Password', validators=[DataRequired(), Length(min=8, max=20)]) + confirmPassword = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Register') + + def validate_username(self, username): + form= UserForm() + check_username = UserRegistration.query.filter_by(username=form.username.data).first() + + if check_username: + raise ValidationError("This username already exists. Choose another one") + + def validate_email(self, email): + form = UserForm() + check_email = UserRegistration.query.filter_by(email=form.email.data).first() + if check_email: + raise ValidationError("This email already exists. Go to Login page") + + +# User login form validation with Flask-WTForms +class UserLogin(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') + + +################ ROUTING SECTION - routes.py ################### + +# Creating login manager session +@login_manager.user_loader +def load_user(user_id): + return UserRegistration.query.get(user_id) + + +# Routing for 'index/registartion' page with form for user registration +@app.route('/', methods=["POST", "GET"]) +def index(): + form = UserForm() + if form.validate_on_submit(): + user_to_register = UserRegistration(username=form.username.data.lower(), fullname=form.fullname.data.title(), email=form.email.data.lower(), password=generate_password_hash(form.password.data)) + db.session.add(user_to_register) + db.session.commit() + return render_template('success.html', message="Register") + return render_template('index.html', form=form) + + +# Routing for 'login' function +@app.route('/login', methods=["GET", "POST"]) +def login(): + form = UserLogin() + if form.validate_on_submit(): + user = UserRegistration.query.filter_by(email=form.email.data).first() + if user and check_password_hash(user.password, form.password.data): + login_user(user) + return render_template('success.html', message="Login") + else: + flash('Incorrect login or password. Try again') + return render_template('login.html', form=form) + + +# Routing for 'logout' function +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('index')) + + +# Routing for displaying all users +@app.route('/users') +def users(): + users = UserRegistration.query.all() + return render_template('users.html', users=users) + + +# Routing for 'success' function +@app.route('/success') +def success(): + return render_template('success.html') + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-app-register-login/requirements.txt b/flask-app-register-login/requirements.txt new file mode 100644 index 0000000..cc1b823 --- /dev/null +++ b/flask-app-register-login/requirements.txt @@ -0,0 +1,15 @@ +click==8.0.3 +dnspython==2.1.0 +email-validator==1.1.3 +Flask==2.0.2 +Flask-Login==0.5.0 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==1.0.0 +greenlet==1.1.2 +idna==3.3 +itsdangerous==2.0.1 +Jinja2==3.0.3 +MarkupSafe==2.0.1 +SQLAlchemy==1.4.27 +Werkzeug==2.0.2 +WTForms==3.0.0 diff --git a/flask-app-register-login/templates/index.html b/flask-app-register-login/templates/index.html new file mode 100644 index 0000000..91e09b5 --- /dev/null +++ b/flask-app-register-login/templates/index.html @@ -0,0 +1,52 @@ + +{% extends 'layout.html' %} +{% block title %}Home/Register{% endblock title %} +{% block header %}User registration and login{% endblock header %} +{% block main %} +

Register a user to the database

+
+ {{ form.csrf_token }} + {{ form.username.label }}
+ {{ form.username(size=25) }}
+ {{ form.fullname.label }}
+ {{ form.fullname(size=25) }}
+ {{ form.email.label }}
+ {{ form.email(size=25) }}
+ {{ form.password.label }}
+ {{ form.password(size=25) }}
+ {{ form.confirmPassword.label }}
+ {{ form.confirmPassword(size=25) }}
+ {{ form.submit() }} +
+ {% if form.username.errors %} + + {% elif form.fullname.errors %} + + {% elif form.email.errors %} + + {% elif form.password.errors %} + + {% elif form.confirmPassword.errors %} + + {% endif %} +{% endblock main %} diff --git a/flask-app-register-login/templates/layout.html b/flask-app-register-login/templates/layout.html new file mode 100644 index 0000000..343994d --- /dev/null +++ b/flask-app-register-login/templates/layout.html @@ -0,0 +1,24 @@ + + + + + {% block title %}{% endblock title %} - User registration and login + + +

{% block header %}{% endblock header %}

+

+ Register | + {% if not current_user.is_authenticated %} + Login + {% endif %} + {% if current_user.is_authenticated %} + Logout | + Users + {% endif %} + +

+ {% block main %}{% endblock main %} +
+

* Demo application created using Python, Flask, Flask-SQLAlchemy, Flask-WTForms, Flask-Login + + diff --git a/flask-app-register-login/templates/login.html b/flask-app-register-login/templates/login.html new file mode 100644 index 0000000..805b64a --- /dev/null +++ b/flask-app-register-login/templates/login.html @@ -0,0 +1,33 @@ + +{% extends 'layout.html' %} +{% block title %}Login{% endblock title %} +{% block header %}User login{% endblock header %} +{% block main %} +

Login a user to the database

+
+ {{ form.csrf_token }} + {{ form.email.label }}
+ {{ form.email(size=25) }}
+ {{ form.password.label }}
+ {{ form.password(size=25) }}
+ {{ form.submit() }} +
+ {% if form.email.errors %} + + {% elif form.password.errors %} + + {% endif %} + {% for message in get_flashed_messages() %} + + {% endfor %} +{% endblock main %} diff --git a/flask-app-register-login/templates/success.html b/flask-app-register-login/templates/success.html new file mode 100644 index 0000000..0601e43 --- /dev/null +++ b/flask-app-register-login/templates/success.html @@ -0,0 +1,7 @@ + +{% extends 'layout.html' %} +{% block title %}Success{% endblock title %} +{% block header %}{{ message }} successfully!{% endblock header %} +{% block main %} +

You {{ message.lower() }} successfully!

+{% endblock main %} diff --git a/flask-app-register-login/templates/users.html b/flask-app-register-login/templates/users.html new file mode 100644 index 0000000..b43be54 --- /dev/null +++ b/flask-app-register-login/templates/users.html @@ -0,0 +1,21 @@ + +{% extends 'layout.html' %} +{% block title %}Users{% endblock title %} +{% block header %}All registered users{% endblock header %} +{% block main %} +

There are currently {{ users|length }} users registered in the database

+ + + + + + + {% for user in users[::-1] %} + + + + + + {% endfor %} +
FULL NAMEUSERNAMEEMAIL
{{ user.fullname }}{{ user.username }}{{ user.email }}
+{% endblock main %} diff --git a/flask-blog-corey/.gitignore b/flask-blog-corey/.gitignore new file mode 100644 index 0000000..b407793 --- /dev/null +++ b/flask-blog-corey/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +venv/ +flaskblog/__pycache__/ +flaskblog/users/__pycache__/ +flaskblog/errors/__pycache__/ +flaskblog/main/__pycache__/ +flaskblog/posts/__pycache__/ + diff --git a/flask-blog-corey/README.md b/flask-blog-corey/README.md new file mode 100644 index 0000000..ff73440 --- /dev/null +++ b/flask-blog-corey/README.md @@ -0,0 +1,192 @@ +# Demo blog with Flask framework on Python + +This blog is created under [Corey Schafer](https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g) youtube tutorials + +### Blog functionality: + +- User registration +- User login +- Add post +- Update post +- Delete post +- Blog pagination +- Display posts by author + +## Setting up and create a blog using Flask + +Pre-requisites: +- Python should be already installed on the system. To check this: ```python3 --version``` +- Python virtual environment package should be installed: ```sudo apt install python3-venv``` + +Additional for Linux: update and install header files and static libraries for Python Dev: +``` +sudo apt-get install python-dev # for python2.x installs +sudo apt-get install python3-dev # for python3.x installs +``` + +#### Modules and packages needed to be installed: + +- Flask: ```pip install flask``` +- Flask forms: ```pip install flask-wtf``` +- Wtf forms 'email_validator' package: ```pip install wtforms[email]``` +- Data base 'SQLAlchemy' for Flask: ```pip install flask-sqlalchemy``` +- 'Flask-Bcrypt' for hashing passwords: ```pip install flask-bcrypt``` +- Flask Login: ```pip install flask-login``` +- 'Pillow' package for resizing images: ```pip install Pillow``` +- 'Flask Email' for sending emails: ```pip install flask-mail``` + +### Setting up a Flask project on Linux: + +1. Create a project folder: ```mkdir flask-blog``` +2. Navigate to the project folder: ```cd flask-blog``` +3. Create a virtual environment inside your project directory: ```python3 -m venv venv``` +4. Activate the virtual environment: ```source venv/bin/activate``` +5. Install Flask: ```pip install Flask``` +6. To verify if Flask was installed on the system: ```python -m flask --version``` +7. Create the simpliest 'Hello, World!' program. Create a python file: ```touch flaskblog.py``` +8. Open the 'flaskblog.py' file and paste in it the following code: +``` +from flask import Flask +app = Flask(__name__) + +@ app.route('/') +def hello_world(): + return "Hello, World!" +``` +9. Launch the development server: ```export FLASK_APP=flaskblog.py``` and then run the Flask app: ```flask run``` +10. The ```http://127.0.01:5000``` or ```http://localhost:5000``` server address should be displayed. Open it in the browser and the 'Hello, World!' message should be displayed +11. Use ```deactivate``` command to stop the server + +### Start the development server in Debug mode + +1. Paste the following code in the python file ```flaskblog.py```: +``` +if __name__=='__main__': + app.run(debug=True) +``` +2. Run the command: ```export FLASK_DEBUG=1``` +3. Then run the flask app with the command: ```flask run``` or ```python flaskblog.py``` + +### Flask Blog Project Structure (before creating packages) + +- flask-blog (project directory): + - venv (virtaul environment folder: ```python3 -m venv venv```) + - templates (templates folder: home.html, layout.html ...): + - layout.html + - home.html + - about.html + - login.html + - register.html + - static (static folder: css, js files ...): + - main.css + - flaskblog.py (main project file) + - forms.py (Login and Registration logic creation) + - site.db (blog SQLite database) + - models.py (storing project models) + +### Creating and testing SQLite project database with SQLAlchemy + +1. Install Flask SQLAlchemy package: ```pip install flask-sqlalchemy``` +2. Create the tables for the project: +``` +from flask_sqlalchemy import SQLAlchemy + +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' + +db = SQLAlchemy(app) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(20), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + image_file = db.Column(db.String(20), nullable=False, default='default.jpg') + password = db.Column(db.String(60), nullable=False) + post = db.relationship('Post', backref='author', lazy=True) + + def __repr__(self): + return f"User('{self.username}', '{self.email}', '{self.image_file}')" + + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + content = db.Column(db.Text, nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def __repr__(self): + return f"User('{self.title}', '{self.date_posted}')" +``` +3. Open ```python3``` in terminal in the virtual environment +4. From application 'flaskblog' import 'db', 'User' and 'Post': + - ```from flaskblog import db``` + - ```from flaskblog import User, Post``` +5. Create a database with the command: ```db.create_all()``` "site.db" file shoulf be created in the project directory +6. Create a new user: +``` +user_1 = User(username="John", email="JohnDoe@company.com", password="password") +db.session.add(user_1) +db.session.commit() +``` +7. Verify if users were added to database (will display all the users): ```User.query.all()``` +8. Verify the first user added: ```User.query.first()``` +9. Filter users added by name (case sensitive): ```User.query.filter_by(username="John").all()``` +10. Additional queries: + - Set up a variable "user" ```user = User.query.first()``` + - display user id: ```user.id``` + - display the user by id: ```User.query.get(1)``` + - display users posts: ```user.post``` +11. Create a new post: +``` +post_1 = Post(title="First blog post", content="First post content", user_id=user.id) +db.session.add(post_1) +db.session.commit() +``` +12. Additional queries: + - ```user = User.query.get(1)``` + - ```for post in user.post: print(post)``` + - ```post.author``` +13. Delete all data: ```db.drop_all()``` +14. Create a new blank database: ```db.create_all()``` + +### Creating project packages - Package Structure + +- flask-blog (project directory): + - venv (virtaul environment folder: ```python3 -m venv venv```) + - run.py (renaming 'flaskblog.py' in the 'run.py' file) + - flaskblog (project python package): + - __init__.py (project package file) + - templates (templates folder: home.html, layout.html ...): + - layout.html + - home.html + - about.html + - login.html + - register.html + - static (static folder: css, js files ...): + - main.css + - forms.py (Login and Registration logic creation) + - site.db (blog SQLite database) + - models.py (storing project models) + - routes.py (project routes file) + +Note: After creating project packages when deleting or creating tables in database an error could occur. The solution is to create and push application context when deleting or creating tables in project database. If it is needed to remove or create tables in the database: +- ```from flaskblog import db``` +- ```from flaskblog import create_app``` +- ```app = create_app()``` +- ```app.app_context().push()``` +- ```db.create_all()``` or ```db.drop_all()``` + +### Using Flask-Bcrypt package for hashing passwords in the application project + +Pre-requisites (for Linux): +- In order to install 'py-bcrypt' package Python Development Headers need to be installed: ```sudo apt-get install python3-dev``` + +Steps: +1. Install the package: ```pip install flask-bcrypt``` +2. Open ```python``` in terminal +3. Import the package: ```from flask_bcrypt import Bcrypt``` +4. Initiate a variable ```bcrypt```: ```bcrypt = Bcrypt()``` +5. Generate a hashed version of 'testing' password: ```bcrypt.generate_password_hash('testing').decode('utf-8')``` +6. Create a variable 'hashed_pas': ```hashed_pas = bcrypt.generate_password_hash('testing').decode('utf-8')``` +7. Verify if the hashed variable == hashed password: ```bcrypt.check_password_hash(hashed_pas, 'testing')``` +8. The result should equal ```True``` diff --git a/flask-blog-corey/flaskblog/__init__.py b/flask-blog-corey/flaskblog/__init__.py new file mode 100644 index 0000000..5fdba3e --- /dev/null +++ b/flask-blog-corey/flaskblog/__init__.py @@ -0,0 +1,38 @@ +# Importin modules and packages + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_bcrypt import Bcrypt +from flask_login import LoginManager +from flask_mail import Mail +from flaskblog.config import Config + + +db = SQLAlchemy() +bcrypt = Bcrypt() +login_manager = LoginManager() +login_manager.login_view = 'users.login' +login_manager.login_message_category = 'info' +mail = Mail() + + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(Config) + + db.init_app(app) + bcrypt.init_app(app) + login_manager.init_app(app) + mail.init_app(app) + + from flaskblog.users.routes import users + from flaskblog.posts.routes import posts + from flaskblog.main.routes import main + from flaskblog.errors.handlers import errors + + app.register_blueprint(users) + app.register_blueprint(posts) + app.register_blueprint(main) + app.register_blueprint(errors) + + return app diff --git a/flask-blog-corey/flaskblog/config.py b/flask-blog-corey/flaskblog/config.py new file mode 100644 index 0000000..6613b2b --- /dev/null +++ b/flask-blog-corey/flaskblog/config.py @@ -0,0 +1,20 @@ +# Project configuration file + + +class Config: + """ + Creating secret key string variable: + - open python interpreter in terminal: 'python3' + - import 'secrets' module: 'import secrets' + - generating random token: 'secrets.token_hex(16)' + """ + SECRET_KEY = 'your_secret_key' + SQLALCHEMY_DATABASE_URI = 'sqlite:///site.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Mail configuration for password restoring by email + MAIL_SERVER = 'smtp.googlemail.com' + MAIL_PORT = 587 + MAIL_USE_TLS = True + MAIL_USERNAME = 'your_email_here' + MAIL_PASSWORD = 'your_email_password_here' diff --git a/flask-blog-corey/flaskblog/errors/__init__.py b/flask-blog-corey/flaskblog/errors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask-blog-corey/flaskblog/errors/handlers.py b/flask-blog-corey/flaskblog/errors/handlers.py new file mode 100644 index 0000000..d798b31 --- /dev/null +++ b/flask-blog-corey/flaskblog/errors/handlers.py @@ -0,0 +1,18 @@ +from flask import Blueprint, render_template + +errors = Blueprint('errors', __name__) + + +@errors.app_errorhandler(404) +def error_404(error): + return render_template('errors/404.html'), 404 + + +@errors.app_errorhandler(403) +def error_403(error): + return render_template('errors/403.html'), 403 + + +@errors.app_errorhandler(500) +def error_500(error): + return render_template('errors/500.html'), 500 diff --git a/flask-blog-corey/flaskblog/main/__init__.py b/flask-blog-corey/flaskblog/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask-blog-corey/flaskblog/main/routes.py b/flask-blog-corey/flaskblog/main/routes.py new file mode 100644 index 0000000..90eb4f3 --- /dev/null +++ b/flask-blog-corey/flaskblog/main/routes.py @@ -0,0 +1,21 @@ +# Routes for Main package + +from flask import render_template, request, Blueprint +from flaskblog.models import Post + +main = Blueprint('main', __name__) + + +# Routing for Home page +@main.route('/') +@main.route('/home') +def home(): + page = request.args.get('page', 1, type=int) + posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=5) + return render_template('home.html', posts=posts) + + +# Routing for About page +@main.route('/about') +def about(): + return render_template('about.html', title="About") diff --git a/flask-blog-corey/flaskblog/models.py b/flask-blog-corey/flaskblog/models.py new file mode 100644 index 0000000..5cf436a --- /dev/null +++ b/flask-blog-corey/flaskblog/models.py @@ -0,0 +1,43 @@ +from flaskblog import db, login_manager +from flask import current_app +from flask_login import UserMixin +from datetime import datetime +from itsdangerous import TimedJSONWebSignatureSerializer as Serializer + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(20), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + image_file = db.Column(db.String(20), nullable=False, default='default.jpg') + password = db.Column(db.String(60), nullable=False) + post = db.relationship('Post', backref='author', lazy=True) + + def get_reset_token(self, expires_sec=1800): + s = Serializer(current_app.config['SECRET_KEY'], expires_sec) + return s.dumps({'user_id': self.id}).decode('utf-8') + + @staticmethod + def verify_reset_token(token): + s = Serializer(current_app.config['SECRET_KEY']) + try: + user_id = s.loads(token)['user_id'] + except: + return None + return User.query.get(user_id) + + def __repr__(self): + return f"User('{self.username}', '{self.email}', '{self.image_file}')" + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + content = db.Column(db.Text, nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def __repr__(self): + return f"User('{self.title}', '{self.date_posted}')" diff --git a/flask-blog-corey/flaskblog/posts/__init__.py b/flask-blog-corey/flaskblog/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask-blog-corey/flaskblog/posts/forms.py b/flask-blog-corey/flaskblog/posts/forms.py new file mode 100644 index 0000000..0460e57 --- /dev/null +++ b/flask-blog-corey/flaskblog/posts/forms.py @@ -0,0 +1,10 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField +from wtforms.validators import DataRequired + + +# Post form +class PostForm(FlaskForm): + title = StringField('Title', validators=[DataRequired()]) + content = TextAreaField('Content', validators=[DataRequired()]) + submit = SubmitField('Post') diff --git a/flask-blog-corey/flaskblog/posts/routes.py b/flask-blog-corey/flaskblog/posts/routes.py new file mode 100644 index 0000000..2d30aef --- /dev/null +++ b/flask-blog-corey/flaskblog/posts/routes.py @@ -0,0 +1,61 @@ +# Routes for Posts package + +from flask import (render_template, url_for, flash, + redirect, request, abort, Blueprint) +from flask_login import current_user, login_required +from flaskblog import db +from flaskblog.models import Post +from flaskblog.posts.forms import PostForm + +posts = Blueprint('posts', __name__) + + +# Routing for Posts +@posts.route('/post/new', methods=["GET", "POST"]) +@login_required +def new_post(): + form = PostForm() + if form.validate_on_submit(): + post = Post(title=form.title.data, content=form.content.data, author=current_user) + db.session.add(post) + db.session.commit() + flash("Your post has been created", "success") + return redirect(url_for('main.home')) + return render_template('create_post.html', title='New Post', form=form, legend='New Post') + + +@posts.route('/post/') +def post(post_id): + post = Post.query.get_or_404(post_id) + return render_template('post.html', title=post.title,post=post) + + +@posts.route('/post//update', methods=["GET", "POST"]) +@login_required +def update_post(post_id): + post = Post.query.get_or_404(post_id) + if post.author != current_user: + abort(403) + form = PostForm() + if form.validate_on_submit(): + post.title = form.title.data + post.content = form.content.data + db.session.commit() + flash('Your post has been updated', 'success') + return redirect(url_for('posts.post', post_id=post.id)) + elif request.method == "GET": + form.title.data = post.title + form.content.data = post.content + return render_template('create_post.html', title='Update Post', form=form, legend='Update Post') + + +@posts.route('/post//delete', methods=["POST"]) +@login_required +def delete_post(post_id): + post = Post.query.get_or_404(post_id) + if post.author != current_user: + abort(403) + db.session.delete(post) + db.session.commit() + flash('Your post has been deleted', 'success') + return redirect(url_for('main.home')) diff --git a/flask-blog-corey/flaskblog/site.db b/flask-blog-corey/flaskblog/site.db new file mode 100644 index 0000000..3532e38 Binary files /dev/null and b/flask-blog-corey/flaskblog/site.db differ diff --git a/flask-blog-corey/flaskblog/static/main.css b/flask-blog-corey/flaskblog/static/main.css new file mode 100644 index 0000000..67b75ff --- /dev/null +++ b/flask-blog-corey/flaskblog/static/main.css @@ -0,0 +1,3 @@ +.main { + padding-top: 4em; +} diff --git a/flask-blog-corey/flaskblog/templates/about.html b/flask-blog-corey/flaskblog/templates/about.html new file mode 100644 index 0000000..a613235 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/about.html @@ -0,0 +1,4 @@ +{% extends 'layout.html' %} +{% block content %} +

About page

+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/account.html b/flask-blog-corey/flaskblog/templates/account.html new file mode 100644 index 0000000..9835467 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/account.html @@ -0,0 +1,68 @@ +{% extends 'layout.html' %} +{% block content %} +
+

Account page

+
+ +
+

{{ current_user.username.title() }}

+

{{ current_user.email }}

+
+
+ +
+
+
+ + {{ form.hidden_tag() }} +
+ Account Info + +
+ {{ form.username.label(class="form-control-label") }} + + + {% if form.username.errors %} + {{ form.username(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + + {{ form.username(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.email.label(class="form-control-label") }} + {% if form.email.errors %} + {{ form.email(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ form.email(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.picture.label() }} + {{ form.picture(class="form-control-file") }} + {% if form.picture.errors %} + {% for error in form.picture.errors %} + {{ error }}
+ {% endfor %} + {% endif %} +
+
+
+ {{ form.submit(class="btn btn-outline-info") }} +
+
+
+
+ +
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/create_post.html b/flask-blog-corey/flaskblog/templates/create_post.html new file mode 100644 index 0000000..6856362 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/create_post.html @@ -0,0 +1,46 @@ +{% extends 'layout.html' %} +{% block content %} +

Create Post page

+
+
+ + {{ form.hidden_tag() }} +
+ {{ legend }} + +
+ {{ form.title.label(class="form-control-label") }} + + + {% if form.title.errors %} + {{ form.title(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.title.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + + {{ form.title(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.content.label(class="form-control-label") }} + {% if form.content.errors %} + {{ form.content(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.content.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ form.content(class="form-control form-control-lg") }} + {% endif %} +
+
+
+ {{ form.submit(class="btn btn-outline-info") }} +
+
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/errors/403.html b/flask-blog-corey/flaskblog/templates/errors/403.html new file mode 100644 index 0000000..7102e02 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/errors/403.html @@ -0,0 +1,8 @@ +{% extends 'layout.html' %} +{% block content %} +

Error Page

+
+

You don't have the permission to access this page (403)

+

Check your account and try again

+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/errors/404.html b/flask-blog-corey/flaskblog/templates/errors/404.html new file mode 100644 index 0000000..85e95f1 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/errors/404.html @@ -0,0 +1,8 @@ +{% extends 'layout.html' %} +{% block content %} +

Error Page

+
+

Oops. Page Not found (404)

+

Page does not exist. Try again

+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/errors/500.html b/flask-blog-corey/flaskblog/templates/errors/500.html new file mode 100644 index 0000000..995ffb1 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/errors/500.html @@ -0,0 +1,8 @@ +{% extends 'layout.html' %} +{% block content %} +

Error Page

+
+

Something went wrong (500)

+

We are experiencing some troubles. Try again later

+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/home.html b/flask-blog-corey/flaskblog/templates/home.html new file mode 100644 index 0000000..590421f --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/home.html @@ -0,0 +1,34 @@ +{% extends 'layout.html' %} +{% block content %} +

Home page

+ {% for post in posts.items %} + + +
+
+ World +

+ {{ post.title }} +

+
+ {{ post.date_posted.strftime('%Y-%m-%d') }} by + {{ post.author.username }} +
+

{{ post.content }}

+ Continue reading +
+ +
+ {% endfor %} + {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} + {% if page_num %} + {% if posts.page == page_num %} + {{ page_num }} + {% else %} + {{ page_num }} + {% endif %} + {% else %} + ... + {% endif %} + {% endfor %} +{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/layout.html b/flask-blog-corey/flaskblog/templates/layout.html new file mode 100644 index 0000000..cb0a3cb --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/layout.html @@ -0,0 +1,108 @@ + + + + + + + + {% if title %} + Flask blog - {{ title }} + {% else %} + Flask blog + {% endif %} + + + + + + + + + + + +
+
+
+ + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock content %} +
+ + +
+

Sidebar section

+

Polpular posts

+

Calendar

+

Tags

+
+
+
+
+ + + + + + + + diff --git a/flask-blog-corey/flaskblog/templates/login.html b/flask-blog-corey/flaskblog/templates/login.html new file mode 100644 index 0000000..1d10138 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/login.html @@ -0,0 +1,39 @@ +{% extends 'layout.html' %} +{% block content %} +

Login page

+
+
+
+ + {{ form.hidden_tag() }} +
+ Login + +
+ {{ form.email.label(class="form-control-label") }} + {{ form.email(class="form-control form-control-lg") }} +
+
+ {{ form.password.label(class="form-control-label") }} + {{ form.password(class="form-control form-control-lg") }} +
+
+ {{ form.remember(class="form-check-input") }} + {{ form.remember.label(class="form-check-label") }} +
+
+
+ {{ form.submit(class="btn btn-outline-success") }} +
+ + Forgot Password? + +
+
+
+ + Need an account? Sign Up Now + +
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/post.html b/flask-blog-corey/flaskblog/templates/post.html new file mode 100644 index 0000000..83ee0f5 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/post.html @@ -0,0 +1,41 @@ +{% extends 'layout.html' %} +{% block content %} +
+
+ {% if post.author == current_user %} + Update + + + + {% endif %} +
+

{{ post.title }}

+
+ {{ post.date_posted.strftime('%Y-%m-%d') }} by + {{ post.author.username }} +
+

{{ post.content }}

+
+
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/register.html b/flask-blog-corey/flaskblog/templates/register.html new file mode 100644 index 0000000..5c63693 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/register.html @@ -0,0 +1,81 @@ +{% extends 'layout.html' %} +{% block content %} +

Register page

+
+
+
+
+ + {{ form.hidden_tag() }} +
+ Join Today + +
+ {{ form.username.label(class="form-control-label") }} + + + {% if form.username.errors %} + {{ form.username(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + + {{ form.username(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.email.label(class="form-control-label") }} + {% if form.email.errors %} + {{ form.email(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ form.email(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.password.label(class="form-control-label") }} + {% if form.password.errors %} + {{ form.password(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ form.password(class="form-control form-control-lg") }} + {% endif %} +
+
+ {{ form.password_confirmation.label(class="form-control-label") }} + {% if form.password_confirmation.errors %} + {{ form.password_confirmation(class="form-control form-control-lg is-invalid") }} +
+ {% for error in form.password_confirmation.errors %} + {{ error }} + {% endfor %} +
+ {% else %} + {{ form.password_confirmation(class="form-control form-control-lg") }} + {% endif %} +
+
+
+ {{ form.submit(class="btn btn-outline-info") }} +
+
+
+
+
+ + Already have an account? Sign in + +
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/reset_request.html b/flask-blog-corey/flaskblog/templates/reset_request.html new file mode 100644 index 0000000..7b5b9a2 --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/reset_request.html @@ -0,0 +1,26 @@ +{% extends 'layout.html' %} +{% block content %} +

Reset login

+
+
+
+ + {{ form.hidden_tag() }} +
+ Reset Password + +
+ {{ form.email.label(class="form-control-label") }} + {{ form.email(class="form-control form-control-lg") }} +
+
+
+ {{ form.submit(class="btn btn-outline-success") }} +
+ + Forgot Password? + +
+
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/reset_token.html b/flask-blog-corey/flaskblog/templates/reset_token.html new file mode 100644 index 0000000..bdd027c --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/reset_token.html @@ -0,0 +1,30 @@ +{% extends 'layout.html' %} +{% block content %} +

Reset login

+
+
+
+ + {{ form.hidden_tag() }} +
+ Reset Password + +
+ {{ form.password.label(class="form-control-label") }} + {{ form.password(class="form-control form-control-lg") }} +
+
+ {{ form.password_confirmation.label(class="form-control-label") }} + {{ form.password_confirmation(class="form-control form-control-lg") }} +
+
+
+ {{ form.submit(class="btn btn-outline-success") }} +
+ + Forgot Password? + +
+
+
+{% endblock content %} diff --git a/flask-blog-corey/flaskblog/templates/user_post.html b/flask-blog-corey/flaskblog/templates/user_post.html new file mode 100644 index 0000000..611005d --- /dev/null +++ b/flask-blog-corey/flaskblog/templates/user_post.html @@ -0,0 +1,33 @@ +{% extends 'layout.html' %} +{% block content %} +

Posts by {{ user.username }} ({{ posts.total }})

+ {% for post in posts.items %} + +
+
+ World +

+ {{ post.title }} +

+
+ {{ post.date_posted.strftime('%Y-%m-%d') }} by + {{ post.author.username }} +
+

{{ post.content }}

+ Continue reading +
+ +
+ {% endfor %} + {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} + {% if page_num %} + {% if posts.page == page_num %} + {{ page_num }} + {% else %} + {{ page_num }} + {% endif %} + {% else %} + ... + {% endif %} + {% endfor %} +{% endblock content %} diff --git a/flask-blog-corey/flaskblog/users/__init__.py b/flask-blog-corey/flaskblog/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask-blog-corey/flaskblog/users/forms.py b/flask-blog-corey/flaskblog/users/forms.py new file mode 100644 index 0000000..8398178 --- /dev/null +++ b/flask-blog-corey/flaskblog/users/forms.py @@ -0,0 +1,72 @@ +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import StringField, PasswordField, SubmitField, BooleanField +from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError +from flask_login import current_user +from flaskblog.models import User + +# Registration form variables +class RegistrationForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), + Length(min=2, max=20)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + password_confirmation = PasswordField('Password Confirmation', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Sign Up') + + def validate_username(self, username): + user = User.query.filter_by(username=username.data).first() + if user: + raise ValidationError('That username already exist. Please choose different one!') + + def validate_email(self, email): + user = User.query.filter_by(email=email.data).first() + if user: + raise ValidationError('That email already exist. Please choose different one!') + + +# Login form variables +class LoginForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + remember = BooleanField('Remember Me') + submit = SubmitField('Login') + + +# Account form +class UpdateAccountForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), + Length(min=2, max=20)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + picture = FileField('Update Profile Picture', validators=[FileAllowed(['jpg', 'png'])]) + submit = SubmitField('Update') + + def validate_username(self, username): + if username.data != current_user.username: + user = User.query.filter_by(username=username.data).first() + if user: + raise ValidationError('That username already exist. Please choose different one!') + + def validate_email(self, email): + if email.data != current_user.email: + user = User.query.filter_by(email=email.data).first() + if user: + raise ValidationError('That email already exist. Please choose different one!') + + +# Password reset form +class RequestResetForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + submit = SubmitField('Request Password Reset') + + def validate_email(self, email): + user = User.query.filter_by(email=email.data).first() + if user is None: + raise ValidationError('There is no account with that email. Register first!') + + +class ResetPasswordForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + password_confirmation = PasswordField('Password Confirmation', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Reset Password') + diff --git a/flask-blog-corey/flaskblog/users/routes.py b/flask-blog-corey/flaskblog/users/routes.py new file mode 100644 index 0000000..544b639 --- /dev/null +++ b/flask-blog-corey/flaskblog/users/routes.py @@ -0,0 +1,116 @@ +# Routes for Users package + +from flask import render_template, url_for, flash, redirect, request, Blueprint +from flask_login import login_user, current_user, logout_user, login_required +from flaskblog import db, bcrypt +from flaskblog.models import User, Post +from flaskblog.users.forms import (RegistrationForm, LoginForm, UpdateAccountForm, + RequestResetForm, ResetPasswordForm) +from flaskblog.users.utils import save_picture, send_reset_email + +users = Blueprint('users', __name__) + + +# Routing for Register page +@users.route('/register', methods=["GET", "POST"]) # Applying 'get' and 'post' requests methods to register page +def register(): + if current_user.is_authenticated: + return redirect(url_for('main.home')) + form = RegistrationForm() + # Printing the success message if validation was successful + if form.validate_on_submit(): + hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') + user = User(username=form.username.data, email=form.email.data, password=hashed_password) + db.session.add(user) + db.session.commit() + flash(f'Your account has been created! You can now log in', 'success') # 'success' is the bootstrap class marked in green + return redirect(url_for('users.login')) + return render_template('register.html', title="Register", form=form) + + +# Routing for Login page +@users.route('/login', methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for('main.home')) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user and bcrypt.check_password_hash(user.password, form.password.data): + login_user(user, remember=form.remember.data) + next_page = request.args.get('next') + return redirect(next_page) if next_page else redirect(url_for('main.home')) + else: + flash('Login unsuccessful. Please check email and password again', 'danger') + return render_template('login.html', title="Login", form=form) + + +# Routing for Logout +@users.route('/logout') +def logout(): + logout_user() + return redirect(url_for('main.home')) + + +# Routing for account +@users.route('/account', methods=["GET", "POST"]) +@login_required +def account(): + form = UpdateAccountForm() + if form.validate_on_submit(): + if form.picture.data: + picture_file = save_picture(form.picture.data) + current_user.image_file = picture_file + current_user.username = form.username.data + current_user.email = form.email.data + db.session.commit() + flash('Your account has been updated', 'success') + return redirect(url_for('users.account')) + elif request.method == 'GET': + form.username.data = current_user.username + form.email.data = current_user.email + image_file = url_for('static', filename='profile_pics/' + current_user.image_file) + return render_template('account.html', title='Account', image_file=image_file, form=form) + + +# Routing for display user specific posts +@users.route('/user/') +def user_post(username): + page = request.args.get('page', 1, type=int) + user = User.query.filter_by(username=username).first_or_404() + posts = Post.query.filter_by(author=user)\ + .order_by(Post.date_posted.desc())\ + .paginate(page=page, per_page=5) + return render_template('user_post.html', posts=posts, user=user) + + +# Routing password reset +@users.route('/reset_password', methods=["GET", "POST"]) +def reset_request(): + if current_user.is_authenticated: + return redirect(url_for('main.home')) + form = RequestResetForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + send_reset_email(user) + flash('An email has been sent with instructions', 'info') + return redirect(url_for('main.home')) + return render_template('reset_request.html', title='Reset Password', form=form) + +@users.route('/reset_password/', methods=["GET", "POST"]) +def reset_token(token): + if current_user.is_authenticated: + return redirect(url_for('main.home')) + user = User.verify_reset_token(token) + if user is None: + flash('That is invalid token', 'warning') + return redirect(url_for('reset_request')) + form = ResetPasswordForm() + if form.validate_on_submit(): + hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') + user.password = hashed_password + db.session.commit() + flash(f'Your password has been updated! You can now log in', 'success') # 'success' is the bootstrap class marked in green + return redirect(url_for('user.login')) + return render_template('reset_token.html', title='Reset Password', form=form) + diff --git a/flask-blog-corey/flaskblog/users/utils.py b/flask-blog-corey/flaskblog/users/utils.py new file mode 100644 index 0000000..72016e2 --- /dev/null +++ b/flask-blog-corey/flaskblog/users/utils.py @@ -0,0 +1,34 @@ +# User utils file + +import os +import secrets +from PIL import Image +from flask import url_for, current_app +from flask_mail import Message +from flaskblog import mail + + +def save_picture(form_picture): + + # creating a random picture name + random_hex = secrets.token_hex(8) + _, f_ext = os.path.splitext(form_picture.filename) + picture_fn = random_hex + f_ext + picture_path = os.path.join(current_app.root_path, 'static/profile_pics', picture_fn) + + # resizing the downloaded picture to the size of 'output size' + output_size = (200, 200) + i = Image.open(form_picture) + i.thumbnail(output_size) + i.save(picture_path) + return picture_fn + + +def send_reset_email(user): + token = user.get_reset_token() + msg = Message('Password Reset Request', sender='noreply@demo.com', recipients=[user.email]) + msg.body = f''' To reset your password, visit the following link: +{url_for('reset_token', token=token, _external=True)} +If you did not made this request, ignore the message +''' + mail.send(msg) diff --git a/flask-blog-corey/requirements.txt b/flask-blog-corey/requirements.txt new file mode 100644 index 0000000..50c0225 --- /dev/null +++ b/flask-blog-corey/requirements.txt @@ -0,0 +1,29 @@ +bcrypt==3.2.0 +blinker==1.4 +cffi==1.14.6 +click==8.0.1 +dataclasses==0.8 +dnspython==2.1.0 +email-validator==1.1.3 +Flask==2.0.1 +Flask-Bcrypt==0.7.1 +Flask-Login==0.5.0 +Flask-Mail==0.9.1 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==0.15.1 +idna==3.2 +importlib-metadata==4.8.1 +itsdangerous==2.0.1 +Jinja2==3.0.1 +MarkupSafe==2.0.1 +pep517==0.11.0 +Pillow==8.3.2 +pkg_resources==0.0.0 +pycparser==2.20 +six==1.16.0 +SQLAlchemy==1.4.23 +tomli==1.2.1 +typing-extensions==3.10.0.2 +Werkzeug==2.0.1 +WTForms==2.3.3 +zipp==3.5.0 diff --git a/flask-blog-corey/run.py b/flask-blog-corey/run.py new file mode 100644 index 0000000..c8be7bc --- /dev/null +++ b/flask-blog-corey/run.py @@ -0,0 +1,15 @@ +# Flask Application created under Corey Schaffer youtube tutorials + +from flaskblog import create_app + +app = create_app() + +""" + Activating the debug mode. + Run in terminal: + - 'export FLASK_DEBUG=1' + - 'flask run' or 'python flaskblog.py' +""" +if __name__=='__main__': + app.run(debug=True) + diff --git a/flask-minimal-app/.gitignore b/flask-minimal-app/.gitignore new file mode 100644 index 0000000..93526df --- /dev/null +++ b/flask-minimal-app/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ diff --git a/flask-minimal-app/README.md b/flask-minimal-app/README.md new file mode 100644 index 0000000..fe13d78 --- /dev/null +++ b/flask-minimal-app/README.md @@ -0,0 +1,9 @@ +# Flask minimal demo applications + +- ['Hello, World' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/hello.py) +- ['Greeting' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/greeting.py) +- ['Page counter' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/site_count.py) +- ['Posts' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/posts.py) +- ['Display posts from database' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/databases.py) +- ['Add post' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/add_posts.py) +- ['Url's' app](https://github.com/valeriybercha/python-demos/blob/master/flask-minimal-app/url.py) diff --git a/flask-minimal-app/add_posts.py b/flask-minimal-app/add_posts.py new file mode 100644 index 0000000..9ba5add --- /dev/null +++ b/flask-minimal-app/add_posts.py @@ -0,0 +1,72 @@ +# Add posts app on Flask + +from flask import Flask, render_template, url_for, request, redirect +from flask_sqlalchemy import SQLAlchemy +from databases import BlogPost, db + +app = Flask(__name__) + +# App configuration file for the database creation +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + +# Display all posts configuration +@app.route('/') +def all_posts(): + posts = BlogPost.query.order_by(BlogPost.date_posted.desc()).all() + return render_template('add-posts-all.html', posts=posts) + + +# Add a new post configuration +@app.route('/add-post', methods=['GET', 'POST']) +def add_posts(): + if request.method == "POST": + post_title = request.form['title'].title() + if request.form['author'] == "": + post_author = "Author Unknown" + else: + post_author = request.form['author'].title() + post_content = request.form['content'] + new_post = BlogPost(title=post_title, author=post_author, content=post_content) + db.session.add(new_post) + db.session.commit() + return redirect(url_for('all_posts')) + return render_template('add-posts.html') + + +# Detailed post view configuration +@app.route('/posts/') +def post(post_id): + post = BlogPost.query.get_or_404(post_id) + return render_template('add-posts-posts.html', post=post) + + +# Delete a post configuration +@app.route('/posts/delete/') +def delete_post(post_id): + post = BlogPost.query.get_or_404(post_id) + db.session.delete(post) + db.session.commit() + return redirect(url_for('all_posts')) + + +# Update a post configuration +@app.route('/posts/update/', methods=["POST", "GET"]) +def update_post(post_id): + if request.method == "POST": + updated_post = BlogPost.query.get_or_404(post_id) + updated_post.title = request.form['title'].title() + updated_post.author = request.form['author'].title() + updated_post.content = request.form['content'] + db.session.commit() + return redirect(url_for('all_posts')) + else: + post = BlogPost.query.get_or_404(post_id) + return render_template('add-posts-update.html', post=post) + + +db.init_app(app) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/count.txt b/flask-minimal-app/count.txt new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/flask-minimal-app/count.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/flask-minimal-app/database.db b/flask-minimal-app/database.db new file mode 100644 index 0000000..e5e8be5 Binary files /dev/null and b/flask-minimal-app/database.db differ diff --git a/flask-minimal-app/databases.py b/flask-minimal-app/databases.py new file mode 100644 index 0000000..0699f52 --- /dev/null +++ b/flask-minimal-app/databases.py @@ -0,0 +1,36 @@ +# Minimalistic app on Flask with SQLAlchemy database + +from flask import Flask, render_template +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +app = Flask(__name__) + +# App configuration file for the database creation +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# Creating the 'db' database +db = SQLAlchemy(app) + + +# Creating 'Post' models for the database +class BlogPost(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + author = db.Column(db.String(50), nullable=False, default='Author Unknown') + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + def __repr__(self): + return 'Blog post ' + str(self.id) + + +@app.route('/') +def index(): + posts = BlogPost.query.all() + return render_template('databases_index.html', posts=posts) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/greeting.py b/flask-minimal-app/greeting.py new file mode 100644 index 0000000..74f5eb3 --- /dev/null +++ b/flask-minimal-app/greeting.py @@ -0,0 +1,16 @@ +# Simplistic greeting Flask app + +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "

Hello there! Type your name in the browser address bar (right after '/', e.g.: '/john')

" + +@app.route("/") +def greetings(user): + return f"

Hello, {user.title()}! Great to see you here

" + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/hello.py b/flask-minimal-app/hello.py new file mode 100644 index 0000000..ecfa740 --- /dev/null +++ b/flask-minimal-app/hello.py @@ -0,0 +1,12 @@ +# Flask Minimal App - Hello World page + +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello_world(): + return f"

Hello, World!

" + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/instruction.md b/flask-minimal-app/instruction.md new file mode 100644 index 0000000..a3fcc0f --- /dev/null +++ b/flask-minimal-app/instruction.md @@ -0,0 +1,89 @@ +# Minimal Flask Apps + +### Pre-requisites: +- Python must be installed on the system: ```python3 --version``` to verify python installation +- Python virtual environment package should be installed: ```sudo apt install python3-venv``` + +Steps: +1. Create a project directory: ```mkdir flask-minimal-app``` +2. Navigate to the project folder: ```cd flask-blog``` +3. Create a virtual environment inside your project directory: ```python3 -m venv venv``` +4. Activate the virtual environment: ```source venv/bin/activate``` +5. Install Flask: ```pip install Flask``` +6. To verify if Flask was installed on the system: ```python -m flask --version``` +7. Create the simpliest 'Hello, World!' program. Create a python file: ```touch hello.py``` +8. Open the 'hello.py' file and paste in it the following code: +``` +from flask import Flask +app = Flask(__name__) + +@ app.route('/') +def hello_world(): + return "

Hello, World!

" + +if __name__ == '__main__': + app.run(debug=True) +``` +9. Launch the development server: ```export FLASK_APP=flaskblog.py``` and then run the Flask app: ```flask run``` +10. The ```http://127.0.01:5000``` or ```http://localhost:5000``` server address should be displayed. Open it in the browser and the 'Hello, World!' message should be displayed +11. Use ```deactivate``` command to stop the server + + +### Creating databses + +- Create a python file: ```touch databases.py``` +- Paste the minimal flask code in it: +``` +from flask import Flask, render_template + +app = Flask(__name__) + +@ app.route('/') +def index(): + return "

Hello, World!

" + +if __name__ == '__main__': + app.run(debug=True) +``` +- Install in the project virtual environment ```source venv/bin/activate``` the 'SQL Alchemy' database: ```pip install flask-sqlalchemy``` +- Import 'SQL Alchemy' library: ```from flask_sqlalchemy import SQLAlchemy +- Create an app configuration for the database: ```app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db' +- Create the database: ```db = SQLAlchemy(app) +- Create 'Post' model for the database: +``` +class BlogPost(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + author = db.Column(db.String(50), nullable=False, default='Author Unknown') + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + def __repr__(self): + return 'Blog post ' + str(self.id) +``` +- Open 'python' in terminal ```python``` +- Import the database: ```from app databases import db``` +- Create the database: ```db.create_all()``` +- Testing the created database: + - ```from databases import BlogPost``` (import the 'BlogPost model created earlier') + - ```posts = BlogPost``` (creating a 'posts' variable) + - ```posts.query.all()``` (displaying all the posts from the database. An empty list should be displayed) + - Adding records to the database: + - ```post1 = BlogPost(title='First Blog Post', content='First blog post content', author='Sammy Lee')``` (creating 'post1' variable with data to be added to the database) + - ```db.session.add(post1)``` (adding data to the database) + - ```db.session.commit()``` (commit all the changes to the database) + - Testing the database: + - ```posts.query.all()``` (displaying all posts) + - ```posts.query.all()[0].title``` (display the title of the first post) + - ```posts.query.get(1)``` (display the second post created by id) +- Additionally we can create a template to display the posts: + - ```from flask import render_template``` (importing render template module) + - ```touch templates/databases_index.html``` (creating a new html file in the templates directory) + - copy the data from the 'posts_index.html' to the new created file + - modify the index function in the 'databases.py' file: + ``` + @app.route('/') + def index(): + posts = BlogPost.query.all() + return render_template('databases_index.html', posts = posts) + ``` diff --git a/flask-minimal-app/posts.py b/flask-minimal-app/posts.py new file mode 100644 index 0000000..302791c --- /dev/null +++ b/flask-minimal-app/posts.py @@ -0,0 +1,42 @@ +# Posts Flask app + +from flask import Flask, render_template, request + +app = Flask(__name__) + + +# Dummy data for posts +posts = [ + { + 'title': 'First post on the blog', + 'content': 'This is the content of the first blog post', + 'author': 'John Doe', + 'date_posted': '2021-10-01' + }, + { + 'title': 'Second post on the blog', + 'content': 'This is the content of the second blog post', + 'author': 'John Doe', + 'date_posted': '2021-10-02' + }, + { + 'title': 'Third post on the blog', + 'content': 'This is the content of the third blog post', + 'date_posted': '2021-10-02' + } +] + + +@app.route('/') +def index(): + return render_template('posts_index.html', posts=posts) + + +@app.route('/posts/') +def post(post_id): + post = posts[post_id] + return render_template('posts_post.html', post=post) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/site_count.py b/flask-minimal-app/site_count.py new file mode 100644 index 0000000..076654e --- /dev/null +++ b/flask-minimal-app/site_count.py @@ -0,0 +1,28 @@ +# Page counting Flask app + +from flask import Flask, render_template, url_for + +app = Flask(__name__) + + +@app.route("/") +def home(): + + """Open the 'count.txt' file and read the stored count variable number""" + f = open('count.txt', 'r') + count = int(f.read()) + f.close() + + """Adding plus one count every time we visit the '/' page""" + count += 1 + + """Rewrite the count varible in the 'count.txt' file""" + f = open('count.txt', 'w') + f.write(str(count)) + f.close() + + return render_template('sc_index.html', count=count) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-minimal-app/static/main.css b/flask-minimal-app/static/main.css new file mode 100644 index 0000000..e21ac60 --- /dev/null +++ b/flask-minimal-app/static/main.css @@ -0,0 +1,48 @@ +body { + background-color: #f1f1f1; + +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Bebas Neue', cursive; +} + +p { + font-family: 'Roboto', sans-serif; +} + +.container { + margin: 40px; +} + +/* Site count app style */ +h1.sc , h3.sc { + text-align: center; +} + +h1.sc { + color: lightskyblue; + margin-top: 5em; +} + +/* Posts app style */ +h1.posts { + color: lightskyblue; + margin-top: 5em; +} + +a.posts { + color: black; + text-decoration: none; +} + +a.posts:hover { + color: lightskyblue; + text-decoration: underline; +} + +/* Add posts app style */ + +.addPosts { + margin-top: 20px; +} diff --git a/flask-minimal-app/templates/add-posts-all.html b/flask-minimal-app/templates/add-posts-all.html new file mode 100644 index 0000000..a65fdb3 --- /dev/null +++ b/flask-minimal-app/templates/add-posts-all.html @@ -0,0 +1,18 @@ +{% extends 'layout.html' %} + +{% block head %} + All posts app +{% endblock head %} +{% block body %} +
+

All Blog Posts Below

+

Want to add a new post?

+
+ {% for post in posts %} +
+ Posted on {{ post.date_posted }} by {{ post.author }} +

{{ post.title }}

+

{{ post.content }}

+
+ {% endfor %} +{% endblock body %} diff --git a/flask-minimal-app/templates/add-posts-posts.html b/flask-minimal-app/templates/add-posts-posts.html new file mode 100644 index 0000000..6a3845a --- /dev/null +++ b/flask-minimal-app/templates/add-posts-posts.html @@ -0,0 +1,23 @@ +{% extends 'layout.html' %} + +{% block head %} + Posts Page +{% endblock head %} +{% block body %} +
+ Posted on {{ post.date_posted }} by {{ post.author }} +

{{ post.title }}

+

{{ post.content }}

+
+
+
Back home
+
+
+
+ +
+
+ +
+
+{% endblock body %} diff --git a/flask-minimal-app/templates/add-posts-update.html b/flask-minimal-app/templates/add-posts-update.html new file mode 100644 index 0000000..cd38ab1 --- /dev/null +++ b/flask-minimal-app/templates/add-posts-update.html @@ -0,0 +1,20 @@ +{% extends 'layout.html' %} + +{% block head %} + Add Posts App +{% endblock head %} +{% block body %} +
+

Add a new post

+
+
+
+
+
+
+
+ +
+
+ +{% endblock body %} diff --git a/flask-minimal-app/templates/add-posts.html b/flask-minimal-app/templates/add-posts.html new file mode 100644 index 0000000..9235e66 --- /dev/null +++ b/flask-minimal-app/templates/add-posts.html @@ -0,0 +1,20 @@ +{% extends 'layout.html' %} + +{% block head %} + Add Posts App +{% endblock head %} +{% block body %} +
+

Add a new post

+
+
+
+
+
+
+
+ +
+
+ +{% endblock body %} diff --git a/flask-minimal-app/templates/databases_index.html b/flask-minimal-app/templates/databases_index.html new file mode 100644 index 0000000..a17a648 --- /dev/null +++ b/flask-minimal-app/templates/databases_index.html @@ -0,0 +1,14 @@ +{% extends 'layout.html' %} + +{% block head %} + Posts App +{% endblock head %} +{% block body %} + {% for post in posts[::-1] %} +
+ Posted on {{ post.date_posted }} by {{ post.author }} +

{{ post.title }}

+

{{ post.content }}

+
+ {% endfor %} +{% endblock body %} diff --git a/flask-minimal-app/templates/layout.html b/flask-minimal-app/templates/layout.html new file mode 100644 index 0000000..7dec41e --- /dev/null +++ b/flask-minimal-app/templates/layout.html @@ -0,0 +1,14 @@ + + + + {% block head %}{% endblock head %} + + + + + + + + {% block body %}{% endblock body %} + + diff --git a/flask-minimal-app/templates/posts_index.html b/flask-minimal-app/templates/posts_index.html new file mode 100644 index 0000000..074d2da --- /dev/null +++ b/flask-minimal-app/templates/posts_index.html @@ -0,0 +1,18 @@ +{% extends 'layout.html' %} + +{% block head %} + Posts App +{% endblock head %} +{% block body %} + {% for post in posts[::-1] %} +
+ {% if post.author %} + Posted on {{ post.date_posted }} by {{ post.author }} + {% else %} + Posted on {{ post.date_posted }} by Author Unknown + {% endif %} +

{{ post.title }}

+

{{ post.content }}

+
+ {% endfor %} +{% endblock body %} diff --git a/flask-minimal-app/templates/posts_post.html b/flask-minimal-app/templates/posts_post.html new file mode 100644 index 0000000..6f3cc38 --- /dev/null +++ b/flask-minimal-app/templates/posts_post.html @@ -0,0 +1,16 @@ +{% extends 'layout.html' %} + +{% block head %} + Posts Page +{% endblock head %} +{% block body %} +
+ {% if post.author %} + Posted on {{ post.date_posted }} by {{ post.author }} + {% else %} + Posted on {{ post.date_posted }} by Author Unknown + {% endif %} +

{{ post.title }}

+

{{ post.content }}

+
+{% endblock body %} diff --git a/flask-minimal-app/templates/sc_index.html b/flask-minimal-app/templates/sc_index.html new file mode 100644 index 0000000..ed3343e --- /dev/null +++ b/flask-minimal-app/templates/sc_index.html @@ -0,0 +1,9 @@ +{% extends 'layout.html' %} + +{% block head %} + Page counter +{% endblock head %} +{% block body %} +

Page Counter

+

This page was visited {{ count }} times

+{% endblock body %} diff --git a/flask-minimal-app/url.py b/flask-minimal-app/url.py new file mode 100644 index 0000000..382c74d --- /dev/null +++ b/flask-minimal-app/url.py @@ -0,0 +1,34 @@ +from flask import Flask, url_for + +app = Flask(__name__) + +@app.route("/") +@app.route("/home") +def home(): + return "

Home Page

    Other pages

  • /about
  • /login
  • /user/john
" + + +@app.route("/about") +def about(): + return "

About Page

" + + +@app.route("/login") +def login(): + return "

Login Page

" + + +@app.route("/user/") +def user(username): + return f"

{username.title()} Page

" + + +with app.test_request_context(): + print(url_for('home')) + print(url_for('about')) + print(url_for('login')) + print(url_for('user', username="john")) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask-simple-blog/.gitignore b/flask-simple-blog/.gitignore new file mode 100644 index 0000000..93526df --- /dev/null +++ b/flask-simple-blog/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ diff --git a/flask-simple-blog/README.md b/flask-simple-blog/README.md new file mode 100644 index 0000000..bb8d74f --- /dev/null +++ b/flask-simple-blog/README.md @@ -0,0 +1,22 @@ +# Basic demo blog created with Flask + +### Basic information: + +- Blog app created with Flask and Python +- Programming language: Python 3.6.9 +- Developer: Valeriy B. + +### Blog functionality: + +- Add a post +- Add a page +- Update a post +- Delete a post +- Update a page +- Delete a page + +### Libraries to install: +- ```pip install flask``` +- ```pip install flask-sqlalchemy``` + +This app is created for showcase purposes only diff --git a/flask-simple-blog/app.py b/flask-simple-blog/app.py new file mode 100644 index 0000000..78102f7 --- /dev/null +++ b/flask-simple-blog/app.py @@ -0,0 +1,182 @@ +# Basic blog app created with Flask +# Programming language: Python 3.6.9 +# Developer: Valeriy B. + +from flask import Flask, render_template, url_for, request, redirect +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# Creating 'db' database +db = SQLAlchemy(app) + +# Creating default variables +default_blog_title = "Simple Flask Blog" +default_blog_description = "Demo blog created with Flask where you can add posts and pages" +default_author_name = "Author Unknown" + + +# Models for adding posts to the database +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + author = db.Column(db.String(30), nullable=False) + content = db.Column(db.Text, nullable=False) + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + def __repr__(self): + return f"Post {self.id}" + + +# Models for adding new pages to the database +class Page(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + def __repr__(self): + return f"Page {self.id}" + + +# Quering all pages +def all_pages(): + return Page.query.all() + + +# Routing for adding pages to menu +def menu_pages(): + return render_template('base.html', pages=pages) + + +# Routing for adding blog description to sidebar +def about_sidebar(): + about = default_blog_description + return render_template('base-posts.html', about=about) + + +# Routing for 'Index' blog page (display all posts) +@app.route('/') +def index(): + posts = Post.query.all() + pages = all_pages() + default_variables = [default_blog_title, default_blog_description] + about = default_blog_description + return render_template('index.html', posts=posts, pages=pages, default_variables=default_variables, about=about) + + +# Routing for post display +@app.route('/post/') +def posts(id): + post = Post.query.get_or_404(id) + pages = all_pages() + return render_template('posts.html', post=post, pages=pages) + + +# Routing for adding a new post +@app.route('/add-post', methods=["POST", "GET"]) +def add_post(): + if request.method == "POST": + title = request.form['title'].title() + if request.form['author'] == "": + author = default_author_name + else: + author = request.form['author'].title() + content = request.form['content'] + post = Post(title=title, author=author, content=content) + db.session.add(post) + db.session.commit() + return redirect(url_for('index')) + else: + pages = all_pages() + about = default_blog_description + return render_template('add-posts.html', pages=pages, about=about) + + +# Routing for deleting posts +@app.route('/post//delete') +def delete_post(id): + post_to_delete = Post.query.get_or_404(id) + db.session.delete(post_to_delete) + db.session.commit() + return redirect(url_for('index')) + + +# Routing for updating posts +@app.route('/post//update', methods=["POST", "GET"]) +def update_post(id): + if request.method == "POST": + updated_post = Post.query.get_or_404(id) + updated_post.title = request.form['title'].title() + if request.form['author'] == "": + updated_post.author = default_author_name + else: + updated_post.author = request.form['author'].title() + updated_post.content = request.form['content'] + db.session.commit() + return redirect(url_for('index')) + else: + pages = all_pages() + post = Post.query.get_or_404(id) + return render_template('update-posts.html', post=post, pages=pages) + + +# Routing for 'About' page +@app.route('/about') +def about(): + pages = all_pages() + return render_template('about.html', pages=pages) + + +# Routing for adding pages +@app.route('/add-page', methods=["POST", "GET"]) +def add_page(): + if request.method == "POST": + title = request.form['title'].title() + content = request.form['content'] + post_to_add = Page(title=title, content=content) + db.session.add(post_to_add) + db.session.commit() + return redirect(url_for('index')) + else: + pages = all_pages() + about = default_blog_description + return render_template('add-pages.html', pages=pages, about=about, active_add_page="active") + + +# Routing for 'Page' display +@app.route('/page/') +def pages(id): + page = Page.query.get_or_404(id) + pages = all_pages() + return render_template('page.html', page=page, pages=pages) + + +# Routing for deleting pages +@app.route('/page//delete') +def delete_page(id): + page_to_delete = Page.query.get_or_404(id) + db.session.delete(page_to_delete) + db.session.commit() + return redirect(url_for('index')) + + +# Routing for updating pages +@app.route('/page//update', methods=["POST", "GET"]) +def update_page(id): + if request.method == "POST": + page_to_update = Page.query.get_or_404(id) + page_to_update.title = request.form['title'] + page_to_update.content = request.form['content'] + db.session.commit() + return redirect(url_for('index')) + page = Page.query.get_or_404(id) + pages = all_pages() + return render_template('update-pages.html', page=page, pages=pages) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/flask-simple-blog/blog.db b/flask-simple-blog/blog.db new file mode 100644 index 0000000..e5530b5 Binary files /dev/null and b/flask-simple-blog/blog.db differ diff --git a/flask-simple-blog/requirements.txt b/flask-simple-blog/requirements.txt new file mode 100644 index 0000000..d1c9e21 --- /dev/null +++ b/flask-simple-blog/requirements.txt @@ -0,0 +1,13 @@ +click==8.0.3 +dataclasses==0.8 +Flask==2.0.2 +Flask-SQLAlchemy==2.5.1 +importlib-metadata==4.8.1 +itsdangerous==2.0.1 +Jinja2==3.0.2 +MarkupSafe==2.0.1 +pkg-resources==0.0.0 +SQLAlchemy==1.4.25 +typing-extensions==3.10.0.2 +Werkzeug==2.0.2 +zipp==3.6.0 diff --git a/flask-simple-blog/static/blog.css b/flask-simple-blog/static/blog.css new file mode 100644 index 0000000..f9106eb --- /dev/null +++ b/flask-simple-blog/static/blog.css @@ -0,0 +1,166 @@ +/* + * Globals + */ + +body { + font-family: Georgia, "Times New Roman", Times, serif; + color: #555; +} + +h1, .h1, +h2, .h2, +h3, .h3, +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + color: #333; +} + + +/* + * Override Bootstrap's default container. + */ + +@media (min-width: 1200px) { + .container { + width: 970px; + } +} + + +/* + * Masthead for nav + */ + +.blog-masthead { + background-color: #428bca; + -webkit-box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); + box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); +} + +/* Nav links */ +.blog-nav-item { + position: relative; + display: inline-block; + padding: 10px; + font-weight: 500; + color: #cdddeb; +} +.blog-nav-item:hover, +.blog-nav-item:focus { + color: #fff; + text-decoration: none; +} + +/* Active state gets a caret at the bottom */ +.blog-nav .active { + color: #fff; +} +.blog-nav .active:after { + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + margin-left: -5px; + vertical-align: middle; + content: " "; + border-right: 5px solid transparent; + border-bottom: 5px solid; + border-left: 5px solid transparent; +} + + +/* + * Blog name and description + */ + +.blog-header { + padding-top: 20px; + padding-bottom: 20px; +} +.blog-title { + margin-top: 30px; + margin-bottom: 0; + font-size: 60px; + font-weight: normal; +} +.blog-description { + font-size: 20px; + color: #999; +} + + +/* + * Main column and sidebar layout + */ + +.blog-main { + font-size: 18px; + line-height: 1.5; +} + +/* Sidebar modules for boxing content */ +.sidebar-module { + padding: 15px; + margin: 0 -15px 15px; +} +.sidebar-module-inset { + padding: 15px; + background-color: #f5f5f5; + border-radius: 4px; +} +.sidebar-module-inset p:last-child, +.sidebar-module-inset ul:last-child, +.sidebar-module-inset ol:last-child { + margin-bottom: 0; +} + + +/* Pagination */ +.pager { + margin-bottom: 60px; + text-align: left; +} +.pager > li > a { + width: 140px; + padding: 10px 20px; + text-align: center; + border-radius: 30px; +} + + +/* + * Blog posts + */ + +.blog-post { + margin-bottom: 60px; +} +.blog-post-title { + margin-bottom: 5px; + font-size: 40px; +} +.blog-post-meta { + margin-bottom: 20px; + color: #999; +} + + +/* + * Footer + */ + +.blog-footer { + padding: 40px 0; + color: #999; + text-align: center; + background-color: #f9f9f9; + border-top: 1px solid #e5e5e5; +} +.blog-footer p:last-child { + margin-bottom: 0; +} diff --git a/flask-simple-blog/static/main.css b/flask-simple-blog/static/main.css new file mode 100644 index 0000000..e69de29 diff --git a/flask-simple-blog/templates/about.html b/flask-simple-blog/templates/about.html new file mode 100644 index 0000000..3503c3c --- /dev/null +++ b/flask-simple-blog/templates/about.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %}About page{% endblock title %} + +{% block body %} +
+

About

+
+
+
+

This app is created for showcase purposes only

+
+ +

Basic information:

+
    +
  • Blog app created with Flask and Python
  • +
  • Programming language: Python 3.6.9
  • +
  • Developer: Valeriy B.
  • +
+ +

Functionality:

+
    +
  • Add a post
  • +
  • Add a page
  • +
  • Update a post
  • +
  • Delete a post
  • +
  • Update a page
  • +
  • Delete a page
  • +
+ +

Libraries to install:

+
    +
  • pip install flask
  • +
  • pip install flask-sqlalchemy
  • +
+
+ +{% endblock body %} diff --git a/flask-simple-blog/templates/add-pages.html b/flask-simple-blog/templates/add-pages.html new file mode 100644 index 0000000..8ec9da4 --- /dev/null +++ b/flask-simple-blog/templates/add-pages.html @@ -0,0 +1,23 @@ +{% extends 'base-posts.html' %} + +{% set active_page = 'active_add_page' %} + +{% block blog_header %} +
+

Add a new page

+
+{% endblock blog_header %} + +{% block main %} +
+
+ + +
+
+ + +
+ +
+{% endblock main %} diff --git a/flask-simple-blog/templates/add-posts.html b/flask-simple-blog/templates/add-posts.html new file mode 100644 index 0000000..f88eacf --- /dev/null +++ b/flask-simple-blog/templates/add-posts.html @@ -0,0 +1,27 @@ +{% extends 'base-posts.html' %} + +{% set active_page = 'active_add_post' %} + +{% block blog_header %} +
+

Add a new post

+
+{% endblock blog_header %} + +{% block main %} +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock main %} diff --git a/flask-simple-blog/templates/base-posts.html b/flask-simple-blog/templates/base-posts.html new file mode 100644 index 0000000..d84b5ef --- /dev/null +++ b/flask-simple-blog/templates/base-posts.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} + + +{% block body %} + + {% block blog_header %}{% endblock blog_header %} + +
+ +
+ + {% block main %}{% endblock main %} + +
+ +
+ + + +
+ +
+{% endblock body %} diff --git a/flask-simple-blog/templates/base.html b/flask-simple-blog/templates/base.html new file mode 100644 index 0000000..9a1e18f --- /dev/null +++ b/flask-simple-blog/templates/base.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + {% block title %}Blog Template for Bootstrap{% endblock title %} + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+ + {% block body %}{% endblock body %} + +
+ + + + + + + + + + + + + diff --git a/flask-simple-blog/templates/index.html b/flask-simple-blog/templates/index.html new file mode 100644 index 0000000..e6c88aa --- /dev/null +++ b/flask-simple-blog/templates/index.html @@ -0,0 +1,33 @@ +{% extends 'base-posts.html' %} + +{% set active_page = 'active_index' %} + +{% block blog_header %} +
+

{{ default_variables[0] }}

+

{{ default_variables[1] }}

+
+{% endblock blog_header %} + +{% block main %} + {% if posts|length == 0 %} +
+

There are NO posts created yet!

+

Create your first post

+
+ {% else %} + {% for post in posts[::-1] %} +
+

{{ post.title }}

+ +

{{ post.content }}

+
+ {% endfor %} + + {% endif %} +{% endblock main %} diff --git a/flask-simple-blog/templates/page.html b/flask-simple-blog/templates/page.html new file mode 100644 index 0000000..c8d85b6 --- /dev/null +++ b/flask-simple-blog/templates/page.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% set active_page = 'page.id' %} + +{% block body %} +
+

{{ page.title }}

+
+
+

{{ page.content }}

+ +
+ Update + Delete +
+ +{% endblock body %} diff --git a/flask-simple-blog/templates/posts.html b/flask-simple-blog/templates/posts.html new file mode 100644 index 0000000..f0f7f52 --- /dev/null +++ b/flask-simple-blog/templates/posts.html @@ -0,0 +1,20 @@ +{% extends 'base-posts.html' %} + +{% block blog_header %} +
+

{{ post.title }}

+ +
+{% endblock blog_header %} + +{% block main %} + +
+

{{ post.content }}

+ +
+ Update + Delete +
+ +{% endblock main %} diff --git a/flask-simple-blog/templates/update-pages.html b/flask-simple-blog/templates/update-pages.html new file mode 100644 index 0000000..8189ee0 --- /dev/null +++ b/flask-simple-blog/templates/update-pages.html @@ -0,0 +1,21 @@ +{% extends 'base-posts.html' %} + +{% block blog_header %} +
+

Update page

+
+{% endblock blog_header %} + +{% block main %} +
+
+ + +
+
+ + +
+ +
+{% endblock main %} diff --git a/flask-simple-blog/templates/update-posts.html b/flask-simple-blog/templates/update-posts.html new file mode 100644 index 0000000..f95f61d --- /dev/null +++ b/flask-simple-blog/templates/update-posts.html @@ -0,0 +1,25 @@ +{% extends 'base-posts.html' %} + +{% block blog_header %} +
+

Update post

+
+{% endblock blog_header %} + +{% block main %} +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock main %} diff --git a/flask-to-do-app/.gitignore b/flask-to-do-app/.gitignore new file mode 100644 index 0000000..92afa22 --- /dev/null +++ b/flask-to-do-app/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +venv/ diff --git a/flask-to-do-app/README.md b/flask-to-do-app/README.md new file mode 100644 index 0000000..88e7cf2 --- /dev/null +++ b/flask-to-do-app/README.md @@ -0,0 +1,39 @@ +# Demo task manager built with Python and Flask + +### Features: +- Register a user to the database: + - Registration form fields: + - Email (Unique, Length: 4 - 50) + - Full Name (Length: 5 - 120) + - Password (Length: 8 - 20) + - Confirm password (EqualTo: password) +- Verification if email already exists in the database when registring a new user +- Login into the system for registered users: + - Login form fields: + - Email (Required) + - Password (Required) +- Successdul message display when users are registered ir logged into the system +- Logged user can create a task +- Logged user can complete a task marking it as 'Done' +- All completed tasks will be stored in 'Completed tasks' page +- User can 'Undone' a completed task. Task will appeat in the task list again +- Logged user can update created task +- Logged user can delete created task + +### Pre-requistites: +- Python should be installed: ```python3 --version```. If python is not installed: ```sudo apt install python3 python3-pip python3-venv``` + +### Install and run the application +- Clone the project from github repository: ```git svn clone https://github.com/valeriybercha/python-demos/trunk/flask-to-do-app``` +- Create project virtual environment: ```python -m venv venv``` +- Start the virtual environment server: ```source venv/bin/activate``` +- Install needed libraries: + - Flask: ```pip install flask``` + - Flask WTForms: ```pip install flask-wtf``` + - WTForms Email validation ```pip install wtforms[email]``` + - Flask SQLAlchemy: ```pip install flask-sqlalchemy``` + - Flask Login: ```pip install flask-login``` + OR, install libraries from ```requirements.txt``` file +- Enable the debug mode for development server: ```export FLASK_DEBUG=1``` +- Start flask project: ```export FLASK_APP=app.py``` and ```flask run``` +- Open the project in browser with address: ```localhost:5000``` diff --git a/flask-to-do-app/app.db b/flask-to-do-app/app.db new file mode 100644 index 0000000..7a8b6ae Binary files /dev/null and b/flask-to-do-app/app.db differ diff --git a/flask-to-do-app/app.py b/flask-to-do-app/app.py new file mode 100644 index 0000000..082bd13 --- /dev/null +++ b/flask-to-do-app/app.py @@ -0,0 +1,211 @@ +# Flask demo application for register and login users + +from flask import Flask, render_template, url_for, redirect, flash, request +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField +from wtforms.validators import InputRequired, Email, Length, EqualTo, ValidationError +from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required +from datetime import datetime + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your_secret_key' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) +login_manager = LoginManager(app) + + +# Load user object +@login_manager.user_loader +def load_user(user_id): + return User.query.get(user_id) + + +####### FORMS SECTION - forms.py ######## + +# User database model +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(50), unique=True, nullable=False) + name = db.Column(db.String(120), nullable=False) + password = db.Column(db.String(20), nullable=False) + tasks = db.relationship('Task', backref="user") + + def __repr__(self): + return f'User {self.id} - {self.name} - {self.email} - {self.task}' + + +# Task database model +class Task(db.Model): + id = db.Column(db.Integer, primary_key=True) + task = db.Column(db.String(250), nullable=False) + task_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + task_completed = db.Column(db.Boolean(), nullable=False, default=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + + def __repr__(self): + return f'Task {self.id} - {self.task} - {self.task_added} - {self.task_completed} - {self.user_id}' + + +# User registration form validation +class UserRegistration(FlaskForm): + email = StringField('Email', validators=[InputRequired(), Email(), Length(min=4, max=50)]) + name = StringField('Full Name', validators=[InputRequired(), Length(min=5, max=120)]) + password = PasswordField('Password', validators=[InputRequired(), Length(min=8, max=20)]) + confirm_password = PasswordField('Confirm Password', validators=[InputRequired(), EqualTo('password')]) + submit = SubmitField('Sign Up') + + def validate_email(self, email): + user = User.query.filter_by(email=email.data).first() + if user: + raise ValidationError('This email already exists in the database. Try another one') + + +# User login form validation +class UserLogin(FlaskForm): + email = StringField('Email', validators=[InputRequired(), Email()]) + password = PasswordField('Password', validators=[InputRequired()]) + submit = SubmitField('Sign In') + + +# Task form validation +class TaskAdd(FlaskForm): + task = StringField('Add a task', validators=[InputRequired(), Length(max=250)]) + submit = SubmitField('Add task') + + +######## ROUTING SECTION - routes.py ######## + +# Routing for 'home/add a task' function +@app.route('/', methods=["GET", "POST"]) +def index(): + form = TaskAdd() + if form.validate_on_submit(): + task_to_add = Task(task=form.task.data, user=current_user) + db.session.add(task_to_add) + db.session.commit() + return redirect(url_for('index')) + else: + curr_user_id = current_user.get_id() + tasks = Task.query.filter(Task.user_id==curr_user_id).filter_by(task_completed=True).all() + return render_template('index.html', form=form, tasks=tasks) + + +# Routing for 'register' function +@app.route('/register', methods=["GET", "POST"]) +def register(): + form = UserRegistration() + if form.validate_on_submit(): + user_to_register = User(email=form.email.data.lower(), name=form.name.data.title(), password=generate_password_hash(form.password.data)) + db.session.add(user_to_register) + db.session.commit() + return redirect(url_for('login')) + return render_template('register.html', form=form) + + +# Routing for 'login' function +@app.route('/login', methods=["GET", "POST"]) +def login(): + form = UserLogin() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data).first() + if user and check_password_hash(user.password, form.password.data): + login_user(user) + return redirect('success') + else: + flash('Incorrect email or password. Please try again') + return render_template('login.html', form=form) + + +# Routing for 'logout' function +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('index')) + + +# Routing for 'about' function +@app.route('/about') +def about(): + return render_template('about.html') + + +# Routing for task 'done' function +@app.route('/done/') +def task_done(task_id): + done_task = Task.query.get_or_404(task_id) + done_task.task_completed = False + db.session.commit() + return redirect(url_for('index')) + + +# Routing for task 'complete/done' function +@app.route('/completed') +@login_required +def completed_tasks(): + curr_user_id = current_user.get_id() + tasks = Task.query.filter(Task.user_id==curr_user_id).filter_by(task_completed=False).all() + return render_template('completed.html', tasks=tasks) + + +# Routing for task 'update' function +@app.route('/update/', methods=["GET", "POST"]) +@login_required +def task_update(task_id): + task_to_update = Task.query.get_or_404(task_id) + if request.method == "POST": + task_to_update.task = request.form['task'] + db.session.commit() + return redirect(url_for('index')) + return render_template('update-task.html', task_to_update=task_to_update) + + +# Rounting for task 'delete' function from home page +@app.route('/delete/') +@login_required +def task_delete(task_id): + task_to_delete = Task.query.get_or_404(task_id) + db.session.delete(task_to_delete) + db.session.commit() + return redirect(url_for('index')) + + +# Routing for task 'delete' function from completed tasks page +@app.route('/deletec/') +@login_required +def task_delete_complete(task_id): + task_to_delete = Task.query.get_or_404(task_id) + db.session.delete(task_to_delete) + db.session.commit() + return redirect(url_for('completed_tasks')) + + +# Routing for task 'undone' function from completed tasks page +@app.route('/undone/') +@login_required +def task_undone(task_id): + undone_task = Task.query.get_or_404(task_id) + undone_task.task_completed = True + db.session.commit() + return redirect(url_for('completed_tasks')) + + +# TEST Routing for 'success' function +@app.route('/success') +def success(): + return render_template('test-success.html') + + +# TEST Rouiting for 'users' function +@app.route('/users') +@login_required +def users(): + users = User.query.all() + return render_template('test-users.html', users=users) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/flask-to-do-app/requirements.txt b/flask-to-do-app/requirements.txt new file mode 100644 index 0000000..0fa0279 --- /dev/null +++ b/flask-to-do-app/requirements.txt @@ -0,0 +1,20 @@ +click==8.0.3 +dataclasses==0.8 +dnspython==2.1.0 +email-validator==1.1.3 +Flask==2.0.2 +Flask-Login==0.5.0 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==1.0.0 +greenlet==1.1.2 +idna==3.3 +importlib-metadata==4.8.2 +itsdangerous==2.0.1 +Jinja2==3.0.3 +MarkupSafe==2.0.1 +pkg-resources==0.0.0 +SQLAlchemy==1.4.27 +typing-extensions==4.0.0 +Werkzeug==2.0.2 +WTForms==3.0.0 +zipp==3.6.0 diff --git a/flask-to-do-app/templates/about.html b/flask-to-do-app/templates/about.html new file mode 100644 index 0000000..6c81455 --- /dev/null +++ b/flask-to-do-app/templates/about.html @@ -0,0 +1,7 @@ + +{% extends 'layout.html' %} +{% block title %}About{% endblock title %} +{% block header %}About page{% endblock header %} +{% block main %} +

This app is created using Flask, Flask-WTForms, Flask-SQLAlchemy, Flask-Login

+{% endblock main %} diff --git a/flask-to-do-app/templates/completed.html b/flask-to-do-app/templates/completed.html new file mode 100644 index 0000000..537277f --- /dev/null +++ b/flask-to-do-app/templates/completed.html @@ -0,0 +1,25 @@ + +{% extends 'layout.html' %} +{% block title %}Completed{% endblock title %} +{% block header %}Completed tasks{% endblock header %} +{% block main %} +

There are currently {{ tasks|length }} completed tasks on the list

+ {% if tasks %} + + + + + + + + {% for task in tasks %} + + + + + + + {% endfor %} +
Nr.TASKADDED DATAACTION
{{ tasks.index(task) + 1 }}{{ task.task }}{{ task.task_added.strftime('%Y-%m-%d, %H:%M') }}Undone | Delete
+ {% endif %} +{% endblock main %} diff --git a/flask-to-do-app/templates/index.html b/flask-to-do-app/templates/index.html new file mode 100644 index 0000000..5c8c191 --- /dev/null +++ b/flask-to-do-app/templates/index.html @@ -0,0 +1,45 @@ + +{% extends 'layout.html' %} +{% block title %}Home{% endblock title %} +{% block header %}'To Do' application{% endblock header %} +{% block main %} + {% if current_user.is_authenticated %} +
+ {{ form.csrf_token }} + {{ form.task.label }}
+ {{ form.task() }}
+

+ {{ form.submit() }} +
+ {% if form.task.errors %} +
    + {% for error in form.task.errors %} +
  • {{ error }}
  • + {% endfor %} + {% endif %} +

    Tasks to do

    +

    There are currently {{ tasks|length }} tasks on the list

    + {% if tasks %} + + + + + + + + {% for task in tasks %} + + + + + + + {% endfor %} +
    Nr.TASKADDED DATAACTION
    {{ tasks.index(task) + 1 }}{{ task.task }}{{ task.task_added.strftime('%Y-%m-%d, %H:%M') }}Done | Update | Delete
    + {% endif %} + {% else %} +

    Welcome!

    +

    Login into the system for using the app

    +

    Don't have an account yet? Create an account

    + {% endif %} +{% endblock main %} diff --git a/flask-to-do-app/templates/layout.html b/flask-to-do-app/templates/layout.html new file mode 100644 index 0000000..738a5e4 --- /dev/null +++ b/flask-to-do-app/templates/layout.html @@ -0,0 +1,25 @@ + + + + + {% block title %}{% endblock title %} - Registration and login app + + +

    {% block header %}{% endblock header %}

    +

    + Home | + {% if current_user.is_authenticated %} + Users | + Completed | + Logout | + {% else %} + Login | + Register | + {% endif %} + About +

    + {% block main %}{% endblock main %} +
    +

    This app is created using Flask, Flask-WTForms, Flask-SQLAlchemy, Flask-Login

    + + diff --git a/flask-to-do-app/templates/login.html b/flask-to-do-app/templates/login.html new file mode 100644 index 0000000..8a5bf5b --- /dev/null +++ b/flask-to-do-app/templates/login.html @@ -0,0 +1,36 @@ + +{% extends 'layout.html' %} +{% block title %}Login{% endblock title %} +{% block header %}Login Page{% endblock header %} +{% block main %} +
    + {{ form.csrf_token }} + {{ form.email.label }}
    + {{ form.email() }}
    + {{ form.password.label }}
    + {{ form.password() }}
    +

    + + {{ form.submit() }} +
    + {% if form.email.errors %} +
      + {% for error in form.email.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% elif form.password.errors %} +
      + {% for error in form.password.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

    {{ message }}

    + {% endfor %} + {% endif %} + {% endwith %} +{% endblock main %} diff --git a/flask-to-do-app/templates/register.html b/flask-to-do-app/templates/register.html new file mode 100644 index 0000000..1de3390 --- /dev/null +++ b/flask-to-do-app/templates/register.html @@ -0,0 +1,52 @@ + +{% extends 'layout.html' %} +{% block title %}Home{% endblock title %} +{% block header %}User register and login{% endblock header %} +{% block main %} +
    + {{ form.csrf_token }} + {{ form.email.label }}
    + {{ form.email() }}
    + {{ form.name.label }}
    + {{ form.name() }}
    + {{ form.password.label }}
    + {{ form.password() }}
    + {{ form.confirm_password.label }}
    + {{ form.confirm_password() }}
    +

    + + {{ form.submit() }} +
    + {% if form.email.errors %} +
      + {% for error in form.email.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% elif form.name.errors %} +
      + {% for error in form.name.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% elif form.password.errors %} +
      + {% for error in form.password.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% elif form.confirm_password.errors %} +
      + {% for error in form.confirm_password.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +

    {{ message }}

    + {% endfor %} + {% endif %} + {% endwith %} +{% endblock main %} diff --git a/flask-to-do-app/templates/test-success.html b/flask-to-do-app/templates/test-success.html new file mode 100644 index 0000000..1c91866 --- /dev/null +++ b/flask-to-do-app/templates/test-success.html @@ -0,0 +1,7 @@ + +{% extends 'layout.html' %} +{% block title %}Success{% endblock title %} +{% block header %}TEST Success Page{% endblock header %} +{% block main %} +

    You logined or registered successfully!

    +{% endblock main %} diff --git a/flask-to-do-app/templates/test-users.html b/flask-to-do-app/templates/test-users.html new file mode 100644 index 0000000..ece57ce --- /dev/null +++ b/flask-to-do-app/templates/test-users.html @@ -0,0 +1,18 @@ + +{% extends 'layout.html' %} +{% block title %}Users{% endblock title %} +{% block header %}TEST Users Page{% endblock header %} +{% block main %} + + + + + + {% for user in users[::-1] %} + + + + + {% endfor %} +
    NAMEEMAIL
    {{ user.name }}{{ user.email }}
    +{% endblock main %} diff --git a/flask-to-do-app/templates/update-task.html b/flask-to-do-app/templates/update-task.html new file mode 100644 index 0000000..d817d0b --- /dev/null +++ b/flask-to-do-app/templates/update-task.html @@ -0,0 +1,11 @@ + +{% extends 'layout.html' %} +{% block title %}Update{% endblock title %} +{% block header %}Task update{% endblock header %} +{% block main %} +
    + + + +
    +{% endblock main %} diff --git a/gui-calculator/.gitignore b/gui-calculator/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/gui-calculator/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/gui-calculator/README.md b/gui-calculator/README.md new file mode 100644 index 0000000..0e79bd8 --- /dev/null +++ b/gui-calculator/README.md @@ -0,0 +1,5 @@ +# GUI Calculator with Tkinter module + +Screenshot: + +![gui calculator with tkinter](https://github.com/valeriybercha/python-demos/blob/master/gui-calculator/screen.png) diff --git a/gui-calculator/calculator.py b/gui-calculator/calculator.py new file mode 100644 index 0000000..ba1bc92 --- /dev/null +++ b/gui-calculator/calculator.py @@ -0,0 +1,76 @@ +# GUI Calculator on Python with Tkinter +# Author: Valeriy B. +# Language: Python 3.8.5 + + +from tkinter import * + +# creating the main window +root = Tk() +root.title("ValiCalc") + +# setting window size +root.geometry('450x390') + +# defining a global variable +num = '' + +input_text = StringVar() + +def btn_equal(): + global num + if num[-1] == "0" and num[-2] == "/": + result = "Division by 0 is not possible" + else: + result = str(eval(num)) + input_text.set(result) + num = "" + +def btn_click(arg): + global num + num = num + str(arg) + input_text.set(num) + +def btn_clear(): + global num + num = "" + input_text.set("") + +input_frame = Frame(root, width = 312, height = 50, bd = 0, highlightbackground = "black", highlightcolor = "black", highlightthickness = 1) +input_frame.pack(side = TOP) + +input_field = Entry(input_frame, font = ('arial', 18, 'bold'), textvariable = input_text, width = 50, bg = "#eee", bd = 0, justify = RIGHT) +input_field.grid(row = 0, column = 0) +input_field.pack(ipady = 10) + +btn_frame = Frame(root, width = 312, height = 272.5, bg = "grey") +btn_frame.pack() + +# 1st row buttons +b_clear = Button(btn_frame, text = "Clear", fg = "black", width = 37, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_clear()).grid(row = 0, column = 0, columnspan = 3, padx = 1, pady = 1) +b_divide = Button(btn_frame, text = "/", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_click("/")).grid(row = 0, column = 3, padx = 1, pady = 1) + +# 2nd row buttons +b_seven = Button(btn_frame, text = "7", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(7)).grid(row = 1, column = 0, padx = 1, pady = 1) +b_eight = Button(btn_frame, text = "8", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(8)).grid(row = 1, column = 1, padx = 1, pady = 1) +b_nine = Button(btn_frame, text = "9", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(9)).grid(row = 1, column = 2, padx = 1, pady = 1) +b_multiply = Button(btn_frame, text = "*", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_click("*")).grid(row = 1, column = 3, padx = 1, pady = 1) + +# 3rd row buttons +b_four = Button(btn_frame, text = "4", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(4)).grid(row = 2, column = 0, padx = 1, pady = 1) +b_five = Button(btn_frame, text = "5", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(5)).grid(row = 2, column = 1, padx = 1, pady = 1) +b_six = Button(btn_frame, text = "6", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(6)).grid(row = 2, column = 2, padx = 1, pady = 1) +b_minus = Button(btn_frame, text = "-", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_click("-")).grid(row = 2, column = 3, padx = 1, pady = 1) + +# 4th row buttons +b_one = Button(btn_frame, text = "1", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(1)).grid(row = 3, column = 0, padx = 1, pady = 1) +b_two = Button(btn_frame, text = "2", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(2)).grid(row = 3, column = 1, padx = 1, pady = 1) +b_three = Button(btn_frame, text = "3", fg = "black", width = 10, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(3)).grid(row = 3, column = 2, padx = 1, pady = 1) +b_plus = Button(btn_frame, text = "+", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_click("+")).grid(row = 3, column = 3, padx = 1, pady = 1) + +# 5th row buttons +b_zero = Button(btn_frame, text = "0", fg = "black", width = 23, height = 3, bd = 0, bg = "#fff", cursor = "hand2", command = lambda: btn_click(0)).grid(row = 4, column = 0, columnspan = 2, padx = 1, pady = 1) +b_point = Button(btn_frame, text = ".", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_click(".")).grid(row = 4, column = 2, padx = 1, pady = 1) +b_equals = Button(btn_frame, text = "=", fg = "black", width = 10, height = 3, bd = 0, bg = "#eee", cursor = "hand2", command = lambda: btn_equal()).grid(row = 4, column = 3, padx = 1, pady = 1) + +root.mainloop() \ No newline at end of file diff --git a/gui-calculator/screen.png b/gui-calculator/screen.png new file mode 100644 index 0000000..b3ca6db Binary files /dev/null and b/gui-calculator/screen.png differ diff --git a/password-generator/README.md b/password-generator/README.md new file mode 100644 index 0000000..8c199b2 --- /dev/null +++ b/password-generator/README.md @@ -0,0 +1,7 @@ +# Password Generator on Python + +User input password generator with the possibility to add symbols, numbers or uppercase letters. + +Screenshot: + +![Password Generator Screen](https://github.com/valeriybercha/python-demos/blob/master/password-generator/screen.png) \ No newline at end of file diff --git a/password-generator/generator.py b/password-generator/generator.py new file mode 100644 index 0000000..6b33e98 --- /dev/null +++ b/password-generator/generator.py @@ -0,0 +1,53 @@ +# STRONG PASSWORD GENERATOR +# Author: Valeriy B. +# Language: Python 3.8.5 + +import random + +# password generator function logic +def generator(arr): + + # initiating a count variable + count = arr[0] - sum([1 for i in arr[1:] if i.lower() == "y"]) + + # creating symbols list + symbols = [chr(i) for i in range(33, 48)] + symbols_l = random.sample(symbols, 1) + + # creating numbers list + numbers = [chr(i) for i in range(48, 58)] + numbers_l = random.sample(numbers, 1) + + # crating lowercase letters list + lowers = [chr(i) for i in range(97, 123)] + lowers_l = random.sample(lowers, count) + + # creating uppercase letters + uppers = [chr(i) for i in range(65, 91)] + uppers_l = random.sample(uppers, 1) + + # creating generated password list + password_generator = lowers_l + if arr[1].lower() == "y": + password_generator += symbols_l + if arr[2].lower() == "y": + password_generator += numbers_l + if arr[3].lower() == "y": + password_generator += uppers_l + + # arrenging list randomly + random.shuffle(password_generator) + + return "Generated password: " + "".join(password_generator) + +print("Password generator") + +# user inputs +pswd_length = int(input("Password length: ")) +pswd_symbols = input("Include symbols (Y, N) (e.g. @#$%): ") +pswd_numbers = input("Include numbers (Y, N) (e.g. 123456): ") +pswd_uppercase = input("Include uppercase letters (Y, N) (e.g. ABCDEF): ") + +pswd_list = [pswd_length, pswd_symbols, pswd_numbers, pswd_uppercase] + +print(generator(pswd_list)) \ No newline at end of file diff --git a/password-generator/screen.png b/password-generator/screen.png new file mode 100644 index 0000000..b88b554 Binary files /dev/null and b/password-generator/screen.png differ diff --git a/text-pro-app/README.md b/text-pro-app/README.md new file mode 100644 index 0000000..4af6d6d --- /dev/null +++ b/text-pro-app/README.md @@ -0,0 +1,2 @@ +# Text Pro Demo App +Description: very basic demo app covering the Python syntax: functions, loops, statements \ No newline at end of file diff --git a/text-pro-app/textpro.py b/text-pro-app/textpro.py new file mode 100644 index 0000000..14a6387 --- /dev/null +++ b/text-pro-app/textpro.py @@ -0,0 +1,42 @@ +# Text Pro Demo App +# Description: very basic demo app covering the Python syntax: functions, loops, statements + +# Author: Valeriy B. +# Language: Python + + +# Question words +def question_words(msg): + questions_list = ["what", "where", "when", "which", "who", "whom", "whose", "why", "how", "is", "are", "will"] + hello_list = ["hello", "hi", "howdy"] + if msg == "hello": + return hello_list + elif msg == "questions": + return questions_list + + +# App logic +def textpro(msg): + + # Result list + result_list = [] + + # Looping while message not equals '!end' + while msg != ("!end"): + + # Typing messages + msg = input("Type something: ") + + # Verifying if first typed word is not in question words list + if msg.split()[0] in question_words("questions"): + result_list.append(msg.capitalize() + "?") + elif msg.split()[0] in question_words("hello"): + result_list.append(msg.capitalize() + "!") + else: + result_list.append(msg.capitalize() + ".") + + # Printing the the list converted into string without the last element + print(" ".join(result_list[:-1])) + + +textpro("") \ No newline at end of file diff --git a/tkinter/README.md b/tkinter/README.md new file mode 100644 index 0000000..09c2054 --- /dev/null +++ b/tkinter/README.md @@ -0,0 +1,45 @@ +# Creating GUI applications with Python module tkinter + +Content: + + +#### 1. 'Hello World' application with Tkinter module - [first-program.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/first-program.py) + +Topics covered: +* module import +* creating the main window +* setting window size +* creating a button +* mainloop + + +#### 2. 'Feedback Form' application with Tkinter module - [feedback-form.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/feedback-form.py) + +Topics covered: +* creating labels +* creating input fields + + +#### 3. 'Radio buttons and checkboxes' application with Tkinter module - [radio-buttons-checkboxes.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/radio-buttons-checkboxes.py) + +Topics covered: +* creating radioboxes +* creating checkboxes + + +#### 4. 'Scale' application with Tkinter module - [scale.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/scale.py) + +Topics covered: +* Creating scale in tkinter + + +#### 5. 'Scrollbar' application with Tkinter module - [scrollbar.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/scrollbar.py) + +Topics covered: +* Creating a scrollbar in tkinter + + +#### 6. 'Toplevel' app with Tkinter module - [toplevel.py](https://github.com/valeriybercha/python-demos/blob/master/tkinter/toplevel.py) + +Topics covered: +* Creating toplevel windows with Tkinter diff --git a/tkinter/feedback-form.py b/tkinter/feedback-form.py new file mode 100644 index 0000000..74afdcd --- /dev/null +++ b/tkinter/feedback-form.py @@ -0,0 +1,54 @@ +# Feedback form with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + +# importing the module +from tkinter import * + + +# creating 'print' information function +def print_info(): + user_email = email_input_field.get() + user_message = message_text_field.get('1.0', END) # for text fields you have to specify the begin and the end of a text to print + print(f"{user_email}: {user_message}") + + +# creating the main window +window = Tk() +window.title("Feedback Form") + + +# setting window size +window.geometry('400x400') + + +# creating a label for 'email' input field +email_label = Label(window, text="Your email:", bg='lemon chiffon') + + +# entry input field settings +email_input_field = Entry(window, width=20, bd=3, bg='lemon chiffon') + + +# 'comments' label settings +message_label = Label(window, text='Your message:') + + +# 'input field' settings +message_text_field = Text(window, width=25, height='5', wrap=WORD) + + +# button settings +submit = Button(window, text='Send', width=20, bg='sky blue', command=print_info) + + +# declaring widgets position +email_label.pack() +email_input_field.pack() +message_label.pack() +message_text_field.pack() +submit.pack() + + +# mainloop +window.mainloop() \ No newline at end of file diff --git a/tkinter/first-program.py b/tkinter/first-program.py new file mode 100644 index 0000000..2867595 --- /dev/null +++ b/tkinter/first-program.py @@ -0,0 +1,31 @@ +# 'Hello, World' application with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + + +# importing the module +from tkinter import * + + +# creating 'hello world' function +def hello_world(event): + print("Hello, World! First app with Tkinter") + + +# creating the main window +window = Tk() + + +# setting window size +window.geometry('150x150') + + +# Button settings +but = Button(window) +but['text'] = "Print" +but.bind("", hello_world) +but.pack() + + +# mainloop +window.mainloop() diff --git a/tkinter/radio-buttons-checkboxes.py b/tkinter/radio-buttons-checkboxes.py new file mode 100644 index 0000000..5ce0b27 --- /dev/null +++ b/tkinter/radio-buttons-checkboxes.py @@ -0,0 +1,78 @@ +# Feedback form with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + + +# importing the module +from tkinter import * + + +# logic for radiobuttons +def radiobuttons(): + if var.get() == 0: + return "0-10" + elif var.get() == 1: + return "11-20" + elif var.get() == 2: + return "21-30" + elif var.get() == 3: + return "31-40" + + +# logic for checkboxes +def checkboxes(): + check_list = [] + if c1.get() == 1: + check_list.append("RED") + if c2.get() == 1: + check_list.append("BLUE") + if c3.get() == 1: + check_list.append("GREEN") + if c4.get() == 1: + check_list.append("YELLOW") + return " ".join(check_list) + + +# printing the result +def print_result(): + r = radiobuttons() + c = checkboxes() + print("Result: Pieces - " + r + " , Colors - " + c) + + +# creating the main window +window = Tk() +window.title("Feedback Form") + + +# setting window size +window.geometry('400x400') + + +# creating radio buttons section +radio_label = Label(window, text='How many pieces?').pack() +var = IntVar() +var.set(1) +rad0 = Radiobutton(window, text='0-10', variable = var, value=0).pack() +rad1 = Radiobutton(window, text='11-20', variable = var, value=1).pack() +rad2 = Radiobutton(window, text='21-30', variable = var, value=2).pack() +rad3 = Radiobutton(window, text='31-40', variable = var, value=3).pack() + + +# creating checkboxes section +checkbox_label = Label(window, text='Pick a color').pack() +c1 = IntVar() +c2 = IntVar() +c3 = IntVar() +c4 = IntVar() +che1 = Checkbutton(window, text='RED', bg='red', variable=c1, onvalue=1, offvalue=0).pack() +che2 = Checkbutton(window, text='BLUE', bg='blue', variable=c2, onvalue=1, offvalue=0).pack() +che3 = Checkbutton(window, text='GREEN', bg='green', variable=c3, onvalue=1, offvalue=0).pack() +che4 = Checkbutton(window, text='YELLOW', bg='yellow', variable=c4, onvalue=1, offvalue=0).pack() + + +# submit button +submit = Button(window, text='Send', width=20, bg='sky blue', command=print_result).pack() + + +window.mainloop() \ No newline at end of file diff --git a/tkinter/scale.py b/tkinter/scale.py new file mode 100644 index 0000000..43ba397 --- /dev/null +++ b/tkinter/scale.py @@ -0,0 +1,31 @@ +# 'Scale' with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + + +# importing the module +from tkinter import * + + +# creating the main window +root = Tk() +root.title("Scale") + + +# creating frameworks +fra1 = Frame(root,width=700,height=200,bg="lightgreen",bd=20) +fra2 = Frame(root,width=300,height=50,bg="green",bd=20) +fra3 = Frame(root) + + +# creating a scale +sca1 = Scale(fra1,orient=HORIZONTAL,length=300,from_=0,to=100,tickinterval=10,resolution=5).pack() +ent1 = Entry(fra2,width=20).pack() + + +# packing the frames widgets +fra1.pack() +fra2.pack() + + +root.mainloop() \ No newline at end of file diff --git a/tkinter/scrollbar.py b/tkinter/scrollbar.py new file mode 100644 index 0000000..a33f9d8 --- /dev/null +++ b/tkinter/scrollbar.py @@ -0,0 +1,28 @@ +# 'Scrollbar' with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + + +# importing the module +from tkinter import * + + +# creating the main window +root = Tk() +root.title("Scrollbar") + + +# creating a scrollbar +scb = Scrollbar(root) +scb.pack(side = RIGHT, fill = Y) + + +# creating a text box +txt = Text(root,width=40,height=3,font='12', yscrollcommand = scb.set) + + +txt.pack() +scb.config(command = txt.yview) + + +root.mainloop() \ No newline at end of file diff --git a/tkinter/toplevel.py b/tkinter/toplevel.py new file mode 100644 index 0000000..4e35cb1 --- /dev/null +++ b/tkinter/toplevel.py @@ -0,0 +1,27 @@ +# 'Toplevel' window with Tkinter module +# Author: Valeriy B. +# Language: Python 3.8.5 + + +# importing the module +from tkinter import * + + +# creating the main window +root = Tk() +root.title("Toplevel") + + +# creating the first toplevel window +win1 = Toplevel(root, relief = SUNKEN, bd = 10, bg = "lightblue") +win1.title("First toplevel window") +win1.minsize(width=400, height=200) + + +# creating the second toplevel window +win1 = Toplevel(root, relief = SUNKEN, bd = 10, bg = "lightyellow") +win1.title("Second toplevel window") +win1.minsize(width=600, height=400) + + +root.mainloop() \ No newline at end of file