From 94db289717617a62df9bc40ba9f6b1ba8fb40e75 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 15 Oct 2021 15:55:50 -0400 Subject: [PATCH 1/8] initial commits --- qbay/__init__.py | 12 +++ qbay/__main__.py | 14 +++- qbay/controllers.py | 124 ++++++++++++++++++++++++++++++ qbay/templates/base.html | 145 +++++++++++++++++++++++++++++++++++ qbay/templates/index.html | 22 ++++++ qbay/templates/login.html | 16 ++++ qbay/templates/register.html | 23 ++++++ 7 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 qbay/controllers.py create mode 100644 qbay/templates/base.html create mode 100644 qbay/templates/index.html create mode 100644 qbay/templates/login.html create mode 100644 qbay/templates/register.html diff --git a/qbay/__init__.py b/qbay/__init__.py index d2a1cb4..f93dab3 100644 --- a/qbay/__init__.py +++ b/qbay/__init__.py @@ -2,6 +2,18 @@ an init file is required for this folder to be considered as a module ''' from flask import Flask +import os + + +package_dir = os.path.dirname( + os.path.abspath(__file__) +) + +templates = os.path.join( + package_dir, "templates" +) + app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../db.sqlite' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = '69cae04b04756f65eabcd2c5a11c8c24' \ No newline at end of file diff --git a/qbay/__main__.py b/qbay/__main__.py index f08984c..6ab8546 100644 --- a/qbay/__main__.py +++ b/qbay/__main__.py @@ -1,2 +1,12 @@ -from qbay import * -from qbay.models import * \ No newline at end of file +from qbay import app +from qbay.models import * +from qbay.controllers import * + +""" +This file runs the server at a given port +""" + +FLASK_PORT = 8081 + +if __name__ == "__main__": + app.run(debug=True, port=FLASK_PORT) \ No newline at end of file diff --git a/qbay/controllers.py b/qbay/controllers.py new file mode 100644 index 0000000..c4695a7 --- /dev/null +++ b/qbay/controllers.py @@ -0,0 +1,124 @@ +from flask import render_template, request, session, redirect +from qbay.models import login, User + + +from qbay import app + + +def authenticate(inner_function): + """ + :param inner_function: any python function that accepts a user object + Wrap any python function and check the current session to see if + the user has logged in. If login, it will call the inner_function + with the logged in user object. + To wrap a function, we can put a decoration on that function. + Example: + @authenticate + def home_page(user): + pass + """ + + def wrapped_inner(): + + # check did we store the key in the session + if 'logged_in' in session: + email = session['logged_in'] + try: + user = User.query.filter_by(email=email).one_or_none() + if user: + # if the user exists, call the inner_function + # with user as parameter + return inner_function(user) + except: + pass + else: + # else, redirect to the login page + return redirect('/login') + + # return the wrapped version of the inner_function: + return wrapped_inner + + +@app.route('/login', methods=['GET']) +def login_get(): + return render_template('login.html', message='Please login') + + +@app.route('/login', methods=['POST']) +def login_post(): + email = request.form.get('email') + password = request.form.get('password') + user = login(email, password) + if user: + session['logged_in'] = user.email + """ + Session is an object that contains sharing information + between a user's browser and the end server. + Typically it is packed and stored in the browser cookies. + They will be past along between every request the browser made + to this services. Here we store the user object into the + session, so we can tell if the client has already login + in the following sessions. + """ + # success! go back to the home page + # code 303 is to force a 'GET' request + return redirect('/', code=303) + else: + return render_template('login.html', message='login failed') + + +@app.route('/') +@authenticate +def home(user): + # authentication is done in the wrapper function + # see above. + # by using @authenticate, we don't need to re-write + # the login checking code all the time for other + # front-end portals + + # some fake product data + products = [ + {'name': 'prodcut 1', 'price': 10}, + {'name': 'prodcut 2', 'price': 20} + ] + return render_template('index.html', user=user, products=products) + + +@app.route('/register', methods=['GET']) +def register_get(): + # templates are stored in the templates folder + return render_template('register.html', message='') + + +@app.route('/register', methods=['POST']) +def register_post(): + email = request.form.get('email') + name = request.form.get('name') + password = request.form.get('password') + password2 = request.form.get('password2') + error_message = None + + if password != password2: + error_message = "The passwords do not match" + + elif len(email) < 1: + error_message = "Email format error" + + elif len(password) < 1: + error_message = "Password not strong enough" + else: + # use backend api to register the user + pass + # if there is any error messages when registering new user + # at the backend, go back to the register page. + if error_message: + return render_template('register.html', message=error_message) + else: + return redirect('/login') + + +@app.route('/logout') +def logout(): + if 'logged_in' in session: + session.pop('logged_in', None) + return redirect('/') diff --git a/qbay/templates/base.html b/qbay/templates/base.html new file mode 100644 index 0000000..4318d26 --- /dev/null +++ b/qbay/templates/base.html @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + {% block title %}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ {% block content %}{% endblock %} +
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/qbay/templates/index.html b/qbay/templates/index.html new file mode 100644 index 0000000..3db017f --- /dev/null +++ b/qbay/templates/index.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Profile{% endblock %}

+{% endblock %} + +{% block content %} +

Welcome {{ user.name }} !

+ + +

Here are all available products

+ +
+ {% for product in products %} +
+

name: {{ product.name }} price: {{ product.price }} update

+
+ {% endfor %} +
+ +logout +{% endblock %} \ No newline at end of file diff --git a/qbay/templates/login.html b/qbay/templates/login.html new file mode 100644 index 0000000..58a6648 --- /dev/null +++ b/qbay/templates/login.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +

{% block title %}Log In{% endblock %}

+

{{message}}

+
+
+ + + + + +
+
+Register +{% endblock %} \ No newline at end of file diff --git a/qbay/templates/register.html b/qbay/templates/register.html new file mode 100644 index 0000000..cf0529e --- /dev/null +++ b/qbay/templates/register.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +

{{message}}

+
+
+ + + + + + + + + + Login +
+
+{% endblock %} \ No newline at end of file From 7776817dbee85adbb9a1463a1e9e20f63beb60f5 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 15 Oct 2021 16:01:58 -0400 Subject: [PATCH 2/8] style error --- qbay/controllers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qbay/controllers.py b/qbay/controllers.py index c4695a7..4381353 100644 --- a/qbay/controllers.py +++ b/qbay/controllers.py @@ -29,7 +29,7 @@ def wrapped_inner(): # if the user exists, call the inner_function # with user as parameter return inner_function(user) - except: + except Exception: pass else: # else, redirect to the login page @@ -80,7 +80,7 @@ def home(user): products = [ {'name': 'prodcut 1', 'price': 10}, {'name': 'prodcut 2', 'price': 20} - ] + ] return render_template('index.html', user=user, products=products) From 681e73124be904ee04dcfa863b7f343e3a78c5ff Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 15 Oct 2021 16:18:27 -0400 Subject: [PATCH 3/8] updated controller --- qbay/controllers.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/qbay/controllers.py b/qbay/controllers.py index 4381353..01db69b 100644 --- a/qbay/controllers.py +++ b/qbay/controllers.py @@ -1,5 +1,5 @@ from flask import render_template, request, session, redirect -from qbay.models import login, User +from qbay.models import login, User, register from qbay import app @@ -100,15 +100,11 @@ def register_post(): if password != password2: error_message = "The passwords do not match" - - elif len(email) < 1: - error_message = "Email format error" - - elif len(password) < 1: - error_message = "Password not strong enough" else: # use backend api to register the user - pass + success = register(name, email, password) + if not success: + error_message = "Registration failed." # if there is any error messages when registering new user # at the backend, go back to the register page. if error_message: From 6a8e7a1a1b10116a6cf65f9b935a7d7fffe2a1e9 Mon Sep 17 00:00:00 2001 From: Steven Ding Date: Sat, 16 Oct 2021 00:17:29 -0400 Subject: [PATCH 4/8] Update README.md --- README.md | 134 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ded74a4..320f8d9 100644 --- a/README.md +++ b/README.md @@ -3,74 +3,106 @@ [![Pytest-All](https://github.com/CISC-CMPE-327/Python-CI-2021/actions/workflows/pytest.yml/badge.svg)](https://github.com/CISC-CMPE-327/Python-CI-2021/actions/workflows/pytest.yml) [![Python PEP8](https://github.com/CISC-CMPE-327/Python-CI-2021/actions/workflows/style_check.yml/badge.svg)](https://github.com/CISC-CMPE-327/Python-CI-2021/actions/workflows/style_check.yml) -This folder contains the template for A2 (backend dev). Folder structure: +### Frontend -``` -├── LICENSE -├── README.md -├── .github -│ └── workflows -│ ├── pytest.yml ======> CI settings for running test automatically (trigger test for commits/pull-requests) -│ └── style_check.yml ======> CI settings for checking PEP8 automatically (trigger test for commits/pull-requests) -├── qbay ======> Application source code -│ ├── __init__.py ======> Required for a python module (can be empty) -│ ├── __main__.py ======> Program entry point -│ └── models.py ======> Data models -├── qbay_test ======> Testing code -│ ├── __init__.py ======> Required for a python module (can be empty) -│ ├── conftest.py ======> Code to run before/after all the testing -│ └── test_models.py ======> Testing code for models.py -└── requirements.txt ======> Dependencies -``` - -To run the application module (make sure you have a python environment of 3.5+) +In order to understand every single bit of this template, first please try cloning the repository, running it, registering a user, logging in, and logging out to develop a general sense of what is going on: ``` -$ pip install -r requirements.txt -$ python -m qbay +python -m qbay ``` -Currently it shows nothing since it is empty in the `__main__.py` file. -Database and the tables will be automatically created into a `db.sqlite` file if non-existed. +Next, try to read the python code from the entry point, starting from `qbay/__main__.py` file. It imports a pre-configured flask application instance from `qbay/__init__.py`. In the init file, `SECRET_KEY` is used to encrypt the session data stored in the client's browser, so one cannot just tell by intercepting your traffice. Usually this shouldn't be hardcoded and read from environment variable during deployment. For the seak of convinience, we hard-code the secret key here as a demo. -To run testing: +When the user type the link `localhost:8081` in the browser, the browser will send a request to the server. The client can type different routes such as `localhost:8081\login` or `localhost:8081\register` with different request methods such as `GET` or `POST`. These different routes will be handled by different python code fragments. And those code fragments are all defined in the `qbay/controllers.py` file. For example: +```python +@app.route('/register', methods=['GET']) +def register_get(): + # templates are stored in the templates folder + return render_template('register.html', message='') ``` -# style check (only show errors) -flake8 --select=E . - -# run all testing code -pytest -s qbay_test +The first line here defines that if a client request `localhost:8081\register` with the 'GET' method, this fragment of code should handle that request and return the corresponding HTML code to be rendered at the client side. For example, if the user type `localhost:8081\register` on his/her browswer and hit enter, then the browser will send a GET request. The above fragment of code recieve the request, and the last line looks up for a HTMP template named `register.html` in the `qa327.templates` folder. +```html +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +

{{message}}

+
+
+ + + + + + + + + + Login +
+
+{% endblock %} ``` -## GitHub Actions (for Continuous Integration): :ok_hand: -With GitHub Actions, you will be able to automatically run all your test cases directly on the cloud, whenever you make changes to your codebase. GitHub actions are available by default. You will be able to use GitHub Actions for your course project. You will have 2,000 minutes test runtime per month for your project. You will find that at your repository homepage there is a tab ‘Actions’. You will be able to find all the logs of all the test runs, and where it broke (using this repo as an example): - -

- -

-Setting up GitHub Actions workflow for your project is simple. Workflows are defined in yml files. The xxx.yml file in the folder `.github/workflows` define the workflow of your CI process. +Let's break this down. This is the [Jinja Templating format (full synatx documentation here)]{https://jinja.palletsprojects.com/en/2.11.x/templates/}. In contrast to React, Vue or other frameworks, it is a server-side rendering framework. It means that the job of filling the template with the required information is done on the server, and the final html will be sent to the client's browswer. Client side rendering is also becoming very popular, but it is very important to understand how different things work. -

- -

+The firstline calls a base template, if you open it, you will find a large chunk of html code. That is the base template for all webpages so we can share common HTML/CSS/JS code for all templates. In line ~127 of the base.html template, you can find something like: -As an example, this is a yml file we use here to automatically check the code's style compliance to PEP8. +```html +
+ {% block content %}{% endblock %} +
+``` -- `name`: the name of your CI process. Can be anything. You name it. -- `on`: the event for which will trigger your CI process. Here we add push. Means that everytime you push your code to the repository, it will trigger the script to run! -- `runs-on`: which platform you would like the test case to run on. -- `steps`: steps to carry out in sequence. -- `uses`: leverage existing operations defined in github actions. In this example, `actions/checkout@v1` means downloading the code. -- `name`: give a name to a step. Usually under the name item you will find a `run` item. -- `run`: the script/command to execute. (in this case, flake8) +It defines a block named `content`. This block can be replaced by any block definitions in other templates that use the base template. So in this example, `register.html` defines a block also named `content`, this block will replace the `content` block in the base template. So everything in the content block of `register.html` will be inserted into `base.html`. -These templates provide you a starting point to setup your repository and understand how the workflow works for GitHub Actions (well the other CI platforms all follow a similar idea, under the hood GitHub Actions uses M:heavy_dollar_sign: Azure pipelines). +On `register.html` there is also a line: -The `passing` badge on the homepage (in the README file) still points to the original template. So make sure that you update the link accordingly pointing to your repository. You can find the link at the Action tab, click on one of the workflow, on the upper right corner, there is a `...` button that shows a `create status badge` menu where it will give you the full markdown link. +```html +

{{message}}

+``` +This will be replaced by the same named parameter, in this case `message`, in the params of `render_template` function call. If we go back `frontend.py` python code ealier, we see: -![image](https://user-images.githubusercontent.com/8474647/135193609-eb84b6f7-e825-4555-b096-69c353d4d71b.png) +```python +@app.route('/register', methods=['GET']) +def register_get(): + # templates are stored in the templates folder + return render_template('register.html', message='') +``` +Here the message param is an empty string. So when rendering the template, `{{message}}` will be replaced by an empty string. +Then completed the whole registration page will be returned to the browser. That will be the page you saw on the register URL. + +Once the client got to the register page, he/she can submit the form with input information. The form by default, after the user clicked the submit button, will be `POST`ed to the same URL, so in this case, 'localhost:8081/register'. Now the server recieves the browswer's request, and need to find the corresponding code fragment to handle the request of route `/register` and method `POST`. It looks up the defined routes, and we have the following match in `frontend.py`: + +```python +@app.route('/register', methods=['POST']) +def register_post(): + email = request.form.get('email') + name = request.form.get('name') + password = request.form.get('password') + password2 = request.form.get('password2') + error_message = None + + if password != password2: + error_message = "The passwords do not match" + else: + # use backend api to register the user + success = register(name, email, password) + if not success: + error_message = "Registration failed." + # if there is any error messages when registering new user + # at the backend, go back to the register page. + if error_message: + return render_template('register.html', message=error_message) + else: + return redirect('/login') +``` +So this fragment of code will read the data from the form, as you can tell from the first 4 lines of function `register_post`. Then, it calls a backend function to register the user. If there is any error, the backend will return an error message, describing what is the problem. If there is any error message, we will return the original `register.html` template to the client with the error message replaced the `{{message}}` snippet in the template. From b7f60590ed40947b62926a30272bece4657471fb Mon Sep 17 00:00:00 2001 From: Steven Ding Date: Sat, 16 Oct 2021 00:39:33 -0400 Subject: [PATCH 5/8] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 320f8d9..e5fb144 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,11 @@ The first line here defines that if a client request `localhost:8081\register` w {% endblock %} ``` -Let's break this down. This is the [Jinja Templating format (full synatx documentation here)]{https://jinja.palletsprojects.com/en/2.11.x/templates/}. In contrast to React, Vue or other frameworks, it is a server-side rendering framework. It means that the job of filling the template with the required information is done on the server, and the final html will be sent to the client's browswer. Client side rendering is also becoming very popular, but it is very important to understand how different things work. +Let's break this down. This is the Jinja Templating format (full synatx documentation here): + +https://jinja.palletsprojects.com/en/2.11.x/templates/ + +In contrast to React, Vue or other frameworks, it is a server-side rendering framework. It means that the job of filling the template with the required information is done on the server, and the final html will be sent to the client's browswer. Client side rendering is also becoming very popular, but it is very important to understand how different things work. The firstline calls a base template, if you open it, you will find a large chunk of html code. That is the base template for all webpages so we can share common HTML/CSS/JS code for all templates. In line ~127 of the base.html template, you can find something like: From 564c18abb9a15873c2ef6e3e6799b5b7925bdce1 Mon Sep 17 00:00:00 2001 From: Steven Ding Date: Sat, 16 Oct 2021 15:05:54 -0400 Subject: [PATCH 6/8] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5fb144..43baa4b 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ On `register.html` there is also a line: ```html

{{message}}

``` -This will be replaced by the same named parameter, in this case `message`, in the params of `render_template` function call. If we go back `frontend.py` python code ealier, we see: +This will be replaced by the same named parameter, in this case `message`, in the params of `render_template` function call. If we go back `controllers.py` python code ealier, we see: ```python @app.route('/register', methods=['GET']) @@ -82,7 +82,7 @@ def register_get(): Here the message param is an empty string. So when rendering the template, `{{message}}` will be replaced by an empty string. Then completed the whole registration page will be returned to the browser. That will be the page you saw on the register URL. -Once the client got to the register page, he/she can submit the form with input information. The form by default, after the user clicked the submit button, will be `POST`ed to the same URL, so in this case, 'localhost:8081/register'. Now the server recieves the browswer's request, and need to find the corresponding code fragment to handle the request of route `/register` and method `POST`. It looks up the defined routes, and we have the following match in `frontend.py`: +Once the client got to the register page, he/she can submit the form with input information. The form by default, after the user clicked the submit button, will be `POST`ed to the same URL, so in this case, 'localhost:8081/register'. Now the server recieves the browswer's request, and need to find the corresponding code fragment to handle the request of route `/register` and method `POST`. It looks up the defined routes, and we have the following match in `controllers.py`: ```python @app.route('/register', methods=['POST']) From c57967744329be5672939a901118b9509af1f9e5 Mon Sep 17 00:00:00 2001 From: Shenghao Jin <55732335+Altered-Si@users.noreply.github.com> Date: Sun, 17 Oct 2021 18:18:22 -0400 Subject: [PATCH 7/8] Fix a spelling error at line 73 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43baa4b..7071d65 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ On `register.html` there is also a line: ```html

{{message}}

``` -This will be replaced by the same named parameter, in this case `message`, in the params of `render_template` function call. If we go back `controllers.py` python code ealier, we see: +This will be replaced by the same named parameter, in this case `message`, in the params of `render_template` function call. If we go back `controllers.py` python code earlier, we see: ```python @app.route('/register', methods=['GET']) From b1df1b4fa24936156b55620e0cc9b5bbe9192d54 Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 6 Oct 2022 02:53:27 -0400 Subject: [PATCH 8/8] fix context issue again --- qbay/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qbay/__init__.py b/qbay/__init__.py index f93dab3..6979655 100644 --- a/qbay/__init__.py +++ b/qbay/__init__.py @@ -16,4 +16,6 @@ app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../db.sqlite' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['SECRET_KEY'] = '69cae04b04756f65eabcd2c5a11c8c24' \ No newline at end of file +app.config['SECRET_KEY'] = '69cae04b04756f65eabcd2c5a11c8c24' +app.app_context().push() +