diff --git a/README.md b/README.md index 2149594..7071d65 100644 --- a/README.md +++ b/README.md @@ -3,78 +3,110 @@ [![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 %} ``` -For the ORM API of flask+sqlalchemy, more exampales can be found here: -https://flask-sqlalchemy.palletsprojects.com/en/latest/queries/#insert-update-delete - -## 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): +Let's break this down. This is the Jinja Templating format (full synatx documentation here): -

- -

+https://jinja.palletsprojects.com/en/2.11.x/templates/ -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. +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 `controllers.py` python code earlier, 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 `controllers.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. diff --git a/qbay/__init__.py b/qbay/__init__.py index d2a1cb4..6979655 100644 --- a/qbay/__init__.py +++ b/qbay/__init__.py @@ -2,6 +2,20 @@ 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' +app.app_context().push() + 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..01db69b --- /dev/null +++ b/qbay/controllers.py @@ -0,0 +1,120 @@ +from flask import render_template, request, session, redirect +from qbay.models import login, User, register + + +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 Exception: + 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" + 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') + + +@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