Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .flaskenv
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
FLASK_ENV=development
FLASK_APP=watchlist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Build and deploy Python app to Azure Web App - helloflask-watchlist
on:
push:
branches:
- master
- main

jobs:
build-and-deploy:
Expand All @@ -18,13 +18,13 @@ jobs:
- name: Set up Python version
uses: actions/setup-python@v1
with:
python-version: '3.6'
python-version: '3.13'

- name: Build using AppService-Build
uses: azure/appservice-build@v1
with:
platform: python
platform-version: '3.6'
platform-version: '3.13'

- name: 'Deploy to Azure Web App'
uses: azure/webapps-deploy@v1
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Tests
on:
push:
branches:
- main
paths-ignore:
- '*.md'
pull_request:
branches:
- main
paths-ignore:
- '*.md'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Test with pytest
run: |
python test_watchlist.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ __pycache__
*.db
htmlcov/
.coverage
.venv
venv
env
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
# Watchlist

Example application for flask tutorial "[Flask 入门教程](https://helloflask.com/book/3)".
Example application for flask tutorial "[Flask 入门教程 / Flask for Beginners](https://helloflask.com/book/3)".

Demo: http://watchlist.helloflask.com

![Screenshot](https://helloflask.com/screenshots/watchlist.png)
![Screenshot](demo.png)


## Installation

clone:
Clone the repository:

```
$ git clone https://github.com/helloflask/watchlist.git
$ cd watchlist
```
create & active virtual enviroment then install dependencies:

Create & active virtual enviroment and install dependencies:

```
$ python3 -m venv env # use `python ...` on Windows
$ source env/bin/activate # use `env\Scripts\activate` on Windows
(env) $ pip install -r requirements.txt
$ python3 -m venv .venv # use `python ...` on Windows
$ source .venv/bin/activate # use `.venv\Scripts\activate` on Windows
(.venv) $ pip install -r requirements.txt
```

generate fake data then run:
Generate fake data then run the app:

```
(env) $ flask forge
(env) $ flask run
(.venv) $ flask forge
(.venv) $ flask run
* Running on http://127.0.0.1:5000/
```

Test account:

- username: `admin`
- password: `helloflask`


## License

Expand Down
Binary file added demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 23 additions & 16 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile requirements.in
#
click==8.1.3
blinker==1.9.0
# via flask
coverage==6.4.2
click==8.1.8
# via flask
coverage==7.10.3
# via -r requirements.in
flask==2.1.3
flask==3.1.1
# via
# -r requirements.in
# flask-login
# flask-sqlalchemy
flask-login==0.6.1
flask-login==0.6.3
# via -r requirements.in
flask-sqlalchemy==2.5.1
flask-sqlalchemy==3.1.1
# via -r requirements.in
importlib-metadata==4.12.0
importlib-metadata==8.7.0
# via flask
itsdangerous==2.1.2
itsdangerous==2.2.0
# via flask
jinja2==3.1.2
jinja2==3.1.6
# via flask
markupsafe==2.1.1
# via jinja2
python-dotenv==0.20.0
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
python-dotenv==1.1.1
# via -r requirements.in
sqlalchemy==1.4.39
sqlalchemy==2.0.43
# via flask-sqlalchemy
werkzeug==2.1.2
typing-extensions==4.14.1
# via sqlalchemy
werkzeug==3.1.3
# via
# flask
# flask-login
zipp==3.8.1
zipp==3.23.0
# via importlib-metadata
26 changes: 13 additions & 13 deletions test_watchlist.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import unittest

from watchlist import app, db
from watchlist import create_app
from watchlist.extensions import db
from watchlist.models import Movie, User
from watchlist.commands import forge, initdb


class WatchlistTestCase(unittest.TestCase):

def setUp(self):
app.config.update(
TESTING=True,
SQLALCHEMY_DATABASE_URI='sqlite:///:memory:'
)
db.create_all()
self.app = create_app('testing')
self.context = self.app.app_context()
self.context.push()

db.create_all()
user = User(name='Test', username='test')
user.set_password('123')
movie = Movie(title='Test Movie Title', year='2019')
db.session.add_all([user, movie])
db.session.commit()

self.client = app.test_client()
self.runner = app.test_cli_runner()
self.client = self.app.test_client()
self.runner = self.app.test_cli_runner()

def tearDown(self):
db.session.remove()
db.drop_all()
self.context.pop()

def login(self):
self.client.post('/login', data=dict(
Expand All @@ -34,10 +34,10 @@ def login(self):
), follow_redirects=True)

def test_app_exist(self):
self.assertIsNotNone(app)
self.assertIsNotNone(self.app)

def test_app_is_testing(self):
self.assertTrue(app.config['TESTING'])
self.assertTrue(self.app.config['TESTING'])

def test_404_page(self):
response = self.client.get('/nothing')
Expand Down Expand Up @@ -211,12 +211,12 @@ def test_delete_item(self):
self.assertNotIn('Test Movie Title', data)

def test_forge_command(self):
result = self.runner.invoke(forge)
result = self.runner.invoke(args=['forge'])
self.assertIn('Done.', result.output)
self.assertNotEqual(Movie.query.count(), 0)

def test_initdb_command(self):
result = self.runner.invoke(initdb)
result = self.runner.invoke(args=['init-db'])
self.assertIn('Initialized database.', result.output)

def test_admin_command(self):
Expand Down
55 changes: 22 additions & 33 deletions watchlist/__init__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,31 @@
import os
import sys

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

# SQLite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
prefix = 'sqlite:///'
else:
prefix = 'sqlite:////'

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev')
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(os.path.dirname(app.root_path), os.getenv('DATABASE_FILE', 'data.db'))
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
login_manager = LoginManager(app)
from sqlalchemy import select

from watchlist.settings import config
from watchlist.blueprints.main import main_bp
from watchlist.blueprints.auth import auth_bp
from watchlist.models import User
from watchlist.extensions import db, login_manager
from watchlist.errors import register_errors
from watchlist.commands import register_commands

@login_manager.user_loader
def load_user(user_id):
from watchlist.models import User
user = User.query.get(int(user_id))
return user

def create_app(config_name='development'):
app = Flask(__name__)
app.config.from_object(config[config_name])

login_manager.login_view = 'login'
# login_manager.login_message = 'Your custom message'
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)

db.init_app(app)
login_manager.init_app(app)

@app.context_processor
def inject_user():
from watchlist.models import User
user = User.query.first()
return dict(user=user)
register_errors(app)
register_commands(app)

@app.context_processor
def inject_user():
user = db.session.execute(select(User)).scalar()
return dict(user=user)

from watchlist import views, errors, commands
return app
Empty file.
39 changes: 39 additions & 0 deletions watchlist/blueprints/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, login_required, logout_user
from sqlalchemy import select

from watchlist.models import User
from watchlist.extensions import db

auth_bp = Blueprint('auth', __name__)


@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

if not username or not password:
flash('Invalid input.')
return redirect(url_for('auth.login'))

user = db.session.execute(select(User).filter_by(username=username)).scalar()

if user is not None and user.validate_password(password):
login_user(user)
flash('Login success.')
return redirect(url_for('main.index'))

flash('Invalid username or password.')
return redirect(url_for('auth.login'))

return render_template('login.html')


@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
flash('Goodbye.')
return redirect(url_for('main.index'))
Loading