diff --git a/project.py b/project.py deleted file mode 100644 index c8f8a0c..0000000 --- a/project.py +++ /dev/null @@ -1,541 +0,0 @@ -''' -Project 4 for Udacity Fullstack Nanodegree -Author: Aleksandr Zonis -''' - -# Modules from Flask Library -from flask import Flask, render_template, request, flash -from flask import session as login_session -from flask import make_response, redirect, jsonify, url_for -# SQLAlchemy library to access database -from sqlalchemy import create_engine, asc -from sqlalchemy.orm import sessionmaker -from database_setup import Base, Category, Product, User - -import random -import string -import httplib2 -import json -import requests - -# Authentification modules -from oauth2client.client import flow_from_clientsecrets -from oauth2client.client import FlowExchangeError - - -app = Flask(__name__) - - -CLIENT_ID = json.loads( - open('client_secrets.json', 'r').read())['web']['client_id'] -APPLICATION_NAME = "Catalog App" - - -# Connect to Database and create database session -engine = create_engine('sqlite:///furniturecatalog.db', - connect_args={'check_same_thread': False}) -# Bind the engine to the metadata of the Base class so that the -# declaratives can be accessed through a DBSession instance -Base.metadata.bind = engine - -DBSession = sessionmaker(bind=engine) -# A DBSession() instance establishes all conversations with the database -# and represents a "staging zone" for all the objects loaded into the -# database session object. Any change made against the objects in the -# session won't be persisted into the database until you call -# session.commit(). If you're not happy about the changes, you can -# revert all of them back to the last commit by calling -# session.rollback() -session = DBSession() - - -@app.route('/login') -def showLogin(): - """App route function to display login page.""" - state = ''.join(random.choice(string.ascii_uppercase + string.digits) - for x in range(32)) - login_session['state'] = state - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # return "The current session state is %s" % login_session['state'] - return render_template('login.html', STATE=state, categories=categories, - CLIENT_ID=CLIENT_ID) - - -@app.route('/') -@app.route('/categories/') -def showCategories(): - """App route function for main page to show all categories.""" - # Check if user is logged in - isLogin = 'email' in login_session - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # Select recently added products - latestProducts = (session.query(Product) - .order_by(Product.created_date.desc()).limit(8)) - return render_template('index.html', categories=categories, - isLogin=isLogin, latestProducts=latestProducts) - - -@app.route('/categories//') -def showCategoryProducts(cat_id): - """App route function to show Products of the selected Category.""" - # Check if user is logged in - isLogin = 'email' in login_session - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # Get category that is selected to be shown - category = session.query(Category).filter_by(id=cat_id).one() - # Check if user is Creator of Category - isCreator = False - if isLogin: - isCreator = login_session['email'] == category.user.email - # Count how many products selected Category have - countProducts = (session.query(Product) - .filter_by(category_id=cat_id).count()) - # Get all products from selected Category - products = session.query(Product).filter_by(category_id=category.id).all() - return render_template('category.html', products=products, - categories=categories, category=category, - isLogin=isLogin, countProducts=countProducts, - isCreator=isCreator) - - -@app.route('/categories///') -def showProduct(cat_id, prod_id): - """App route function to show selected product.""" - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # Get category that is selected to be shown - category = session.query(Category).filter_by(id=cat_id).one() - product = session.query(Product).filter_by(id=prod_id).one() - # Check if user is Creator of Category or Product - isCreator = False - if isLogin: - isCreator = (login_session['email'] == category.user.email or - login_session['email'] == product.user.email) - # Get the selected product - product = session.query(Product).filter_by(id=prod_id).one() - return render_template('product.html', categories=categories, - category=category, product=product, - isLogin=isLogin, isCreator=isCreator) - - -@app.route('/addcategory/', methods=['GET', 'POST']) -def addCategory(): - """App route function to add new Category to the Category Table.""" - if 'email' not in login_session: - return redirect('/login') - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - if request.method == 'POST': - if request.form['name'] == '': - flash("Please, enter Category Name") - return render_template('addcategory.html', - categories=categories, - isLogin=isLogin) - newCategory = Category(name=request.form['name'], - user_id=login_session['user_id']) - session.add(newCategory) - session.commit() - flash('New Category {} Successfully Created'.format(newCategory.name)) - return redirect(url_for('showCategories')) - else: - return render_template('addcategory.html', - categories=categories, - isLogin=isLogin) - - -@app.route('/addproduct/', methods=['GET', 'POST']) -def addProduct(): - """App route function to add new Product to the Product list.""" - # If user is not logged in, inform him about it and redirect - if 'email' not in login_session: - return redirect('/login') - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - if request.method == 'POST': - # Check if all the fields are filled out - if request.form['name'] == '': - flash("Please, enter product name. \ - Make sure you fill out all the fields") - return render_template('addproduct.html', - categories=categories, - isLogin=isLogin) - if request.form['image_url'] == '': - flash("Please, enter product image(link of the image). \ - Make sure you fill out all the fields") - return render_template('addproduct.html', - categories=categories, - isLogin=isLogin) - if request.form['product_url'] == '': - flash("Please, enter product URL. \ - Make sure you fill out all the fields") - return render_template('addproduct.html', - categories=categories, - isLogin=isLogin) - if request.form['description'] == '': - flash('Please, enter product description. \ - Make sure you fill out all the fields') - return render_template('addproduct.html', - categories=categories, - isLogin=isLogin) - - category = (session.query(Category) - .filter_by(name=request.form['category_name']).one()) - newProduct = Product(name=request.form['name'], - description=request.form['description'], - image_url=request.form['image_url'], - product_url=request.form['product_url'], - category_id=category.id, - user_id=login_session['user_id']) - session.add(newProduct) - session.commit() - flash('New Product {} Successfully Created'.format(newProduct.name)) - return redirect(url_for('showCategoryProducts', - cat_id=newProduct.category_id)) - else: - return render_template('addproduct.html', - categories=categories, isLogin=isLogin) - - -@app.route('/editcategory//', methods=['GET', 'POST']) -def editCategory(cat_id): - """App route function to edit existing Category.""" - # If user is not logged in, inform him about it and redirect - if 'email' not in login_session: - flash("You need to Log In if you want to edit") - return redirect('/categories') - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # Get category that is selected to be edited - categoryToEdit = session.query(Category).filter_by(id=cat_id).one() - # Check if user is Creator, if not inform that he cannot - # do changes - if not login_session['email'] == categoryToEdit.user.email: - flash("You need to be Creator of the category to be able to edit") - return redirect('/categories') - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - if request.method == 'POST': - if request.form['name']: - categoryToEdit.name = request.form['name'] - else: - flash('Please, enter category name.') - return render_template('editcategory.html', - categories=categories, - isLogin=isLogin, - categoryToEdit=categoryToEdit) - session.add(categoryToEdit) - session.commit() - flash('You successfully \ - updated category to {}'.format(categoryToEdit.name)) - return redirect(url_for('showCategoryProducts', cat_id=cat_id)) - else: - return render_template('editcategory.html', categories=categories, - isLogin=isLogin, categoryToEdit=categoryToEdit) - - -@app.route('/deletecategory//', methods=['GET', 'POST']) -def deleteCategory(cat_id): - """App route function to delete existing Category""" - # If user is not logged in, inform him about it and redirect - if 'email' not in login_session: - flash("You need to Log In if you want to delete the category.") - return redirect('/categories') - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # Get category that is selected to be edited - categoryToDelete = session.query(Category).filter_by(id=cat_id).one() - # Check if user is Creator, if not inform that he cannot - # do changes - if not login_session['email'] == categoryToDelete.user.email: - flash("You need to be a Creator of the category\ - to be able to delete it") - return redirect('/categories') - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # Check for method and do appropriate. If 'POST' -> delete Category, - # if 'GET' -> render 'deletecategory.html' - if request.method == 'POST': - session.delete(categoryToDelete) - session.commit() - flash('You successfully deleted \ - category "{}"'.format(categoryToDelete.name)) - return redirect(url_for('showCategories')) - else: - return render_template('deletecategory.html', - categories=categories, - isLogin=isLogin, - categoryToDelete=categoryToDelete) - - -@app.route('/editproduct///', methods=['GET', 'POST']) -def editProduct(cat_id, prod_id): - """App route function to edit existing Product.""" - # If user is not logged in, inform him about it and redirect - if 'email' not in login_session: - flash("You need to Log In if you want to edit") - return redirect('/login') - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - # Get Product that user wants to edit - productToEdit = session.query(Product).filter_by(id=prod_id).one() - # Check if user is Creator, if not inform that he cannot - # do changes - if not login_session['email'] == productToEdit.user.email: - flash("You need to be a Creator of the Product to edit it") - return redirect(url_for('showProduct', cat_id=cat_id, prod_id=prod_id)) - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - # Get category that is selected to be shown - category = session.query(Category).filter_by(id=cat_id).one() - if request.method == 'POST': - if request.form['name']: - productToEdit.name = request.form['name'] - if request.form['image_url']: - productToEdit.image_url = request.form['image_url'] - if request.form['product_url']: - productToEdit.product_url = request.form['product_url'] - if request.form['description']: - productToEdit.description = request.form['description'] - session.add(productToEdit) - session.commit() - flash('You successfully edited product') - return redirect(url_for('showProduct', cat_id=cat_id, prod_id=prod_id)) - else: - return render_template('editproduct.html', categories=categories, - isLogin=isLogin, category=category, - productToEdit=productToEdit) - - -@app.route('/deleteproduct///', - methods=['GET', 'POST']) -def deleteProduct(cat_id, prod_id): - """App route function to delete existing product.""" - # If user is not logged in, inform him about it and redirect - if 'email' not in login_session: - flash("You need to Log In if you want to delete product") - return redirect('/login') - - # Check if user is logged in and save the result in isLogin - isLogin = 'email' in login_session - - # Get the Product that User wants to delete - productToDelete = session.query(Product).filter_by(id=prod_id).one() - - # Check if user is Creator, if not inform that he cannot - # do changes - if not login_session['email'] == productToDelete.user.email: - flash("You need to be a Creator of the Product to delete it") - return redirect(url_for('showProduct', cat_id=cat_id, prod_id=prod_id)) - - # All categories ordered by name - categories = session.query(Category).order_by(Category.name).all() - - # Get category that is selected to be shown - category = session.query(Category).filter_by(id=cat_id).one() - if request.method == 'POST': - session.delete(productToDelete) - session.commit() - flash('You successfully deleted \ - product "{}"'.format(productToDelete.name)) - return redirect(url_for('showCategoryProducts', cat_id=cat_id)) - else: - return render_template('deleteproduct.html', categories=categories, - isLogin=isLogin, category=category, - productToDelete=productToDelete) - - -''' -JSON endpoints -''' - - -@app.route('/catalog/json/') -def showAllJSON(): - """App route function to show Catalog Data in JSON.""" - # Obtain all categoris - categories = session.query(Category).all() - # Obtain all products - products = session.query(Product).all() - return jsonify(categories=[cat.serialize for cat in categories], - products=[prod.serialize for prod in products]) - - -@app.route('/categories/json/') -def showCategoriesJSON(): - """App route function to show Categories Data in JSON.""" - # Obtain all categoris - categories = session.query(Category).all() - return jsonify(categories=[cat.serialize for cat in categories]) - - -@app.route('/products/json/') -def showProductsJSON(): - """App route function to show Products Data in JSON.""" - # Obtain all products - products = session.query(Product).all() - return jsonify(products=[prod.serialize for prod in products]) - - -@app.route('/category//json/') -def showCategoryJSON(cat_id): - """App route function to show selected Category JSON.""" - # Obtain Category - category = session.query(Category).filter_by(id=cat_id).one() - return jsonify(category=[category.serialize]) - - -@app.route('/product//json/') -def showProductJSON(prod_id): - """App route function to show selected Product JSON.""" - # Obtain product - product = session.query(Product).filter_by(id=prod_id).one() - return jsonify(product=[product.serialize]) - - -@app.route('/googleConnect', methods=['POST']) -def googleConnect(): - """Connect via Google Account and fetch User info.""" - # Validate state token - if request.args.get('state') != login_session['state']: - response = make_response(json.dumps('Invalid state parameter.'), 401) - response.headers['Content-Type'] = 'application/json' - return response - # Obtain authorization code - code = request.data - - try: - # Upgrade the authorization code into a credentials object - oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='') - oauth_flow.redirect_uri = 'postmessage' - credentials = oauth_flow.step2_exchange(code) - except FlowExchangeError: - response = make_response( - json.dumps('Failed to upgrade the authorization code.'), 401) - response.headers['Content-Type'] = 'application/json' - return response - - # Check that the access token is valid. - access_token = credentials.access_token - url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s' - % access_token) - h = requests.get(url=url) - result = json.loads(h.text) - # If there was an error in the access token info, abort. - if result.get('error') is not None: - response = make_response(json.dumps(result.get('error')), 500) - response.headers['Content-Type'] = 'application/json' - return response - - # Verify that the access token is used for the intended user. - gplus_id = credentials.id_token['sub'] - if result['user_id'] != gplus_id: - response = make_response( - json.dumps("Token's user ID doesn't match given user ID."), 401) - response.headers['Content-Type'] = 'application/json' - return response - - # Verify that the access token is valid for this app. - if result['issued_to'] != CLIENT_ID: - response = make_response( - json.dumps("Token's client ID does not match app's."), 401) - response.headers['Content-Type'] = 'application/json' - return response - - stored_access_token = login_session.get('access_token') - stored_gplus_id = login_session.get('gplus_id') - if stored_access_token is not None and gplus_id == stored_gplus_id: - response = make_response(json.dumps('Current user \ - is already connected.'), 200) - response.headers['Content-Type'] = 'application/json' - return response - - # Store the access token in the session for later use. - login_session['access_token'] = credentials.access_token - login_session['gplus_id'] = gplus_id - - # Get user info - userinfo_url = "https://www.googleapis.com/oauth2/v1/userinfo" - params = {'access_token': credentials.access_token, 'alt': 'json'} - answer = requests.get(userinfo_url, params=params) - - data = answer.json() - - login_session['username'] = data['name'] - login_session['email'] = data['email'] - # ADD PROVIDER TO LOGIN SESSION - login_session['provider'] = 'google' - - # see if user exists, if it doesn't make a new one - user_id = getUserID(data["email"]) - if not user_id: - user_id = createUser(login_session) - login_session['user_id'] = user_id - result = "User is authorized!" - flash("You are now logged in as %s" % login_session['username']) - return result - - -# User Helper Functions -def createUser(login_session): - """Creates new login_session user, returns user.id.""" - newUser = User(name=login_session['username'], email=login_session[ - 'email']) - session.add(newUser) - session.commit() - user = session.query(User).filter_by(email=login_session['email']).one() - return user.id - - -def getUserInfo(user_id): - """Returns User object""" - user = session.query(User).filter_by(id=user_id).one() - return user - - -def getUserID(email): - """Returns user.id if exists, otherwise returns none.""" - try: - user = session.query(User).filter_by(email=email).one() - return user.id - except: - return None - - -@app.route('/googleDisconnect') -def googleDisconnect(): - """Disconnect user""" - # Check if user is connected, - # only disconnect a connected user. - access_token = login_session.get('access_token') - if access_token is None: - response = make_response( - json.dumps('Current user not connected.'), 401) - response.headers['Content-Type'] = 'application/json' - flash("Current user not connected") - return redirect(url_for('showCategories')) - response = make_response(json.dumps('Successfully disconnected.'), 200) - response.headers['Content-Type'] = 'application/json' - # Delete all login_session info - del login_session['gplus_id'] - del login_session['access_token'] - del login_session['username'] - del login_session['email'] - del login_session['user_id'] - del login_session['provider'] - flash("You have successfully been logged out.") - return redirect(url_for('showCategories')) - - -if __name__ == '__main__': - app.secret_key = 'super_secret_key' - app.debug = True - app.run(host='0.0.0.0', port=8000) diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..eaf6360 --- /dev/null +++ b/project/__init__.py @@ -0,0 +1,25 @@ +''' +Project 4 for Udacity Fullstack Nanodegree +Author: Aleksandr Zonis +''' + +# Modules from Flask Library +from flask import Flask +import config + +# Route modules +from categories.routes import categories +from products.routes import products +from jsonAPI.routes import jsonAPI +from auth.routes import auth + +app = Flask(__name__) + +# Configure the Application +app.config.from_object(config.DevelopmentConfig) + +# Register Routes +app.register_blueprint(categories) +app.register_blueprint(products) +app.register_blueprint(jsonAPI) +app.register_blueprint(auth) diff --git a/project/auth/__init__.py b/project/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/auth/routes.py b/project/auth/routes.py new file mode 100644 index 0000000..cd4a217 --- /dev/null +++ b/project/auth/routes.py @@ -0,0 +1,179 @@ +# Modules from Flask Library +from flask import Flask, render_template, request, flash, Blueprint +from flask import session as login_session +from flask import make_response, redirect, jsonify, url_for +# Access database +from project.models import Base, Category, Product, User +from project.db import session +# Services +from project.services.categories import CategoryService +from project.services.config import ConfigService +from project.services.user import UserService + +import random +import string +import httplib2 +import json +import requests + +# Authentification modules +from oauth2client.client import flow_from_clientsecrets +from oauth2client.client import FlowExchangeError + + +auth = Blueprint('auth', __name__) + + +# Instantiate services +category_service = CategoryService() +config_service = ConfigService() +user_service = UserService() + +# Get Client ID +CLIENT_ID = config_service.get_setting('client_id') + +# Status Code Constansts +HTTP_STATUS_CODE_OK = 200 +HTTP_STATUS_CODE_UNAUTHORIZED = 401 +HTTP_STATUS_CODE_ERROR = 500 + +@auth.route('/login') +def showLogin(): + """App route function to display login page.""" + state = ''.join(random.choice(string.ascii_uppercase + string.digits) + for x in range(32)) + login_session['state'] = state + return render_template('login.html', + STATE=state, + categories=category_service.get_all_categories(), + CLIENT_ID=CLIENT_ID) + + +@auth.route('/googleConnect', methods=['POST']) +def googleConnect(): + """Connect via Google Account and fetch User info.""" + # Validate state token + if request.args.get('state') != login_session['state']: + make_json_response('Invalid state parameter.', + HTTP_STATUS_CODE_UNAUTHORIZED) + # Obtain authorization code + code = request.data + + try: + # Upgrade the authorization code into a credentials object + oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='') + oauth_flow.redirect_uri = 'postmessage' + credentials = oauth_flow.step2_exchange(code) + except FlowExchangeError: + make_json_response('Invalid state parameter.', + HTTP_STATUS_CODE_UNAUTHORIZED) + + # Check that the access token is valid. + access_token = credentials.access_token + url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}' + .format(access_token)) + h = requests.get(url=url) + result = json.loads(h.text) + # If there was an error in the access token info, abort. + if result.get('error') is not None: + make_json_response('error', HTTP_STATUS_CODE_ERROR) + + # Verify that the access token is used for the intended user. + gplus_id = credentials.id_token['sub'] + if result['user_id'] != gplus_id: + make_json_response('Invalid state parameter.', + HTTP_STATUS_CODE_UNAUTHORIZED) + + # Verify that the access token is valid for this app. + if result['issued_to'] != CLIENT_ID: + make_json_response('Invalid state parameter.', + HTTP_STATUS_CODE_UNAUTHORIZED) + + stored_access_token = login_session.get('access_token') + stored_gplus_id = login_session.get('gplus_id') + if stored_access_token is not None and gplus_id == stored_gplus_id: + make_json_response('Current user is already connected.', + HTTP_STATUS_CODE_OK) + + # Store the access token in the session for later use. + login_session['access_token'] = credentials.access_token + login_session['gplus_id'] = gplus_id + + # Get user info + userinfo_url = "https://www.googleapis.com/oauth2/v1/userinfo" + params = {'access_token': credentials.access_token, 'alt': 'json'} + answer = requests.get(userinfo_url, params=params) + + data = answer.json() + + login_session['username'] = data['email'] + login_session['email'] = data['email'] + # ADD PROVIDER TO LOGIN SESSION + login_session['provider'] = 'google' + + # see if user exists, if it doesn't make a new one + user_id = getUserID(data["email"]) + if not user_id: + user_id = createUser(login_session) + login_session['user_id'] = user_id + result = "User is authorized!" + flash("You are now logged in as {}".format(login_session['username'])) + return result + + +@auth.route('/googleDisconnect') +def googleDisconnect(): + """Disconnect user""" + # Check if user is connected, + # only disconnect a connected user. + access_token = login_session.get('access_token') + if access_token is None: + response = make_response( + json.dumps('Current user not connected.'), 401) + response.headers['Content-Type'] = 'application/json' + flash("Current user not connected") + return redirect(url_for('showCategories')) + response = make_response(json.dumps('Successfully disconnected.'), 200) + response.headers['Content-Type'] = 'application/json' + # Delete all login_session info + del login_session['gplus_id'] + del login_session['access_token'] + del login_session['username'] + del login_session['email'] + del login_session['user_id'] + del login_session['provider'] + flash("You have successfully been logged out.") + return redirect(url_for('categories.showCategories')) + + +# User Helper Functions +def make_json_response(data, status): + """Makes http response, accepts message and status code""" + response = make_response(data, status) + response.headers['Content-Type'] = 'application/json' + return response + + +def createUser(login_session): + """Creates new login_session user, returns user.id.""" + newUser = User(name=login_session['username'], email=login_session[ + 'email']) + session.add(newUser) + session.commit() + user = user_service.get_user_by_email(login_session['email']) + return user.id + + +def getUserInfo(user_id): + """Returns User object""" + user = user_service.get_user_by_id(user_id) + return user + + +def getUserID(email): + """Returns user.id if exists, otherwise returns none.""" + try: + user = user_service.get_user_by_email(email) + return user.id + except: + return None diff --git a/project/categories/__init__.py b/project/categories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/categories/routes.py b/project/categories/routes.py new file mode 100644 index 0000000..3bac001 --- /dev/null +++ b/project/categories/routes.py @@ -0,0 +1,155 @@ +# Modules from Flask Library +from flask import Flask, render_template, request, flash, Blueprint +from flask import session as login_session +from flask import make_response, redirect, jsonify, url_for +# Access database +from project.models import Category, Product, User +from project.db import session +# Services +from project.services.categories import CategoryService +from project.services.auth import AuthService +from project.services.products import ProductService + +import random +import string +import httplib2 +import json +import requests + + +# Create Blueprint 'categories' +categories = Blueprint('categories', __name__) + + +# Instantiate services +category_service = CategoryService() +product_service = ProductService() +auth_service = AuthService() + +DEFAULT_CATEGORY_PRODUCT_NUMBER = 8 + +@categories.route('/') +@categories.route('/categories/') +def showCategories(): + """App route function for main page to show all categories.""" + return render_template('index.html', + categories=category_service.get_all_categories(), + isLogin=auth_service.is_user_authorized(), + latestProducts=(product_service + .get_latest_products(DEFAULT_CATEGORY_PRODUCT_NUMBER))) + + +@categories.route('/categories//') +def showCategoryProducts(category_id): + """App route function to show Products of the selected Category.""" + isLogin = auth_service.is_user_authorized() + # Pick category selected by user + category = category_service.get_category_by_id(category_id) + # Check if user is Creator of Category + isCreator = False + if isLogin: + isCreator = login_session['email'] == category.user.email + return render_template('category.html', + products=(product_service + .get_products_by_category_id(category_id)), + categories=category_service.get_all_categories(), + category=category, + isLogin=isLogin, + countProducts=(product_service + .products_count(category_id)), + isCreator=isCreator) + + +@categories.route('/addcategory/', methods=['GET', 'POST']) +def addCategory(): + """App route function to add new Category to the Category Table.""" + if 'email' not in login_session: + return redirect('/login') + if request.method == 'POST': + if request.form['name'] == '': + flash("Please, enter Category Name") + return render_template('addcategory.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + newCategory = Category(name=request.form['name'], + user_id=login_session['user_id']) + session.add(newCategory) + session.commit() + flash('New Category {} Successfully Created'.format(newCategory.name)) + return redirect(url_for('categories.showCategories')) + else: + return render_template('addcategory.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + + +@categories.route('/editcategory//', + methods=['GET', 'POST']) +def editCategory(category_id): + """App route function to edit existing Category.""" + # If user is not logged in, inform him about it and redirect + if 'email' not in login_session: + flash("You need to Log In if you want to edit") + return redirect('/categories') + # Get category that is selected to be edited + categoryToEdit = category_service.get_category_by_id(category_id) + # Check if user is Creator, if not inform that he cannot + # do changes + if not login_session['email'] == categoryToEdit.user.email: + flash("You need to be Creator of the category to be able to edit") + return redirect('/categories') + if request.method == 'POST': + if request.form['name']: + categoryToEdit.name = request.form['name'] + else: + flash('Please, enter category name.') + return render_template('editcategory.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized(), + categoryToEdit=categoryToEdit) + session.add(categoryToEdit) + session.commit() + flash('You successfully \ + updated category to {}'.format(categoryToEdit.name)) + return redirect(url_for('categories.showCategoryProducts', + category_id=category_id)) + else: + return render_template('editcategory.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized(), + categoryToEdit=categoryToEdit) + + +@categories.route('/deletecategory//', methods=['GET', 'POST']) +def deleteCategory(category_id): + """App route function to delete existing Category""" + # If user is not logged in, inform him about it and redirect + if 'email' not in login_session: + flash("You need to Log In if you want to delete the category.") + return redirect('/categories') + # Get category that is selected to be edited + categoryToDelete = category_service.get_category_by_id(category_id) + # Check if user is Creator, if not inform that he cannot + # do changes + if not login_session['email'] == categoryToDelete.user.email: + flash("You need to be a Creator of the category\ + to be able to delete it") + return redirect('/categories') + # Check for method and do appropriate. If 'POST' -> delete Category, + # if 'GET' -> render 'deletecategory.html' + if request.method == 'POST': + session.delete(categoryToDelete) + session.commit() + flash('You successfully deleted \ + category "{}"'.format(categoryToDelete.name)) + return redirect(url_for('categories.showCategories')) + else: + return render_template('deletecategory.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized(), + categoryToDelete=categoryToDelete) diff --git a/project/config.py b/project/config.py new file mode 100644 index 0000000..672f67d --- /dev/null +++ b/project/config.py @@ -0,0 +1,10 @@ +class Config(): + SECRET_KEY = 'super_secret_key' + DEBUG = False + + +class DevelopmentConfig(Config): + DEBUG = True + +class ProductionConfig(Config): + DEBUG = False diff --git a/project/db.py b/project/db.py new file mode 100644 index 0000000..78589d8 --- /dev/null +++ b/project/db.py @@ -0,0 +1,20 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from models import Base + + +class DBConnector(): + + engine = create_engine('sqlite:///furniturecatalog.db', + connect_args={'check_same_thread': False}) + Base.metadata.bind = engine + + def __init__(self): + self.DBSession = sessionmaker(bind=self.engine) + + def get_session(self): + return self.DBSession() + +# Connect to the database and create session +session = DBConnector().get_session() \ No newline at end of file diff --git a/project/jsonAPI/__init__.py b/project/jsonAPI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/jsonAPI/routes.py b/project/jsonAPI/routes.py new file mode 100644 index 0000000..4c17518 --- /dev/null +++ b/project/jsonAPI/routes.py @@ -0,0 +1,73 @@ +# Modules from Flask Library +from flask import Flask, render_template, request, flash, Blueprint +from flask import session as login_session +from flask import make_response, redirect, jsonify, url_for +# Access database +from project.models import Category, Product, User +from project.db import DBConnector +# Services +from project.services.categories import CategoryService +from project.services.products import ProductService + +import random +import string +import httplib2 +import json +import requests + + +jsonAPI = Blueprint('jsonAPI', __name__) + +# Connect to the database, create session +session = DBConnector().get_session() + +# Instantiate services +category_service = CategoryService() +product_service = ProductService() + +''' +JSON endpoints +''' + + +@jsonAPI.route('/catalog/json/') +def showAllJSON(): + """App route function to show Catalog Data in JSON.""" + # Obtain all categoris + categories = category_service.get_all_categories() + # Obtain all products + products = product_service.get_all_products() + return jsonify(categories=[cat.serialize for cat in categories], + products=[prod.serialize for prod in products]) + + +@jsonAPI.route('/categories/json/') +def showCategoriesJSON(): + """App route function to show Categories Data in JSON.""" + # Obtain all categoris + categories = category_service.get_all_categories() + return jsonify(categories=[cat.serialize for cat in categories]) + + +@jsonAPI.route('/products/json/') +def showProductsJSON(): + """App route function to show Products Data in JSON.""" + # Obtain all products + products = product_service.get_all_products() + return jsonify(products=[prod.serialize for prod in products]) + + +@jsonAPI.route('/category//json/') +def showCategoryJSON(cat_id): + """App route function to show selected Category JSON.""" + # Obtain Category + category = category_service.get_category_by_id(cat_id) + return jsonify(category=[category.serialize]) + + +@jsonAPI.route('/product//json/') +def showProductJSON(prod_id): + """App route function to show selected Product JSON.""" + # Obtain product + product = get_product_by_id(prod_id) + return jsonify(product=[product.serialize]) diff --git a/addproducts.py b/project/mock_catalog_data.py similarity index 91% rename from addproducts.py rename to project/mock_catalog_data.py index 0775158..2db3bee 100644 --- a/addproducts.py +++ b/project/mock_catalog_data.py @@ -1,27 +1,8 @@ ''' Insert data to the "furniturecatalog.db" database ''' - - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from database_setup import Base, Category, Product, User - -engine = create_engine('sqlite:///furniturecatalog.db') -# Bind the engine to the metadata of the Base class so that the -# declaratives can be accessed through a DBSession instance -Base.metadata.bind = engine - -DBSession = sessionmaker(bind=engine) -# A DBSession() instance establishes all conversations with the database -# and represents a "staging zone" for all the objects loaded into the -# database session object. Any change made against the objects in the -# session won't be persisted into the database until you call -# session.commit(). If you're not happy about the changes, you can -# revert all of them back to the last commit by calling -# session.rollback() -session = DBSession() +from models import Category, Product, User +from db import session user1 = User(name='Aleksandr Zonis', email='zonis7@gmail.com') @@ -217,3 +198,5 @@ category_id=category3.id, user_id=user2.id) session.add(product12) session.commit() + +print('All products are added') diff --git a/database_setup.py b/project/models.py similarity index 100% rename from database_setup.py rename to project/models.py diff --git a/project/products/__init__.py b/project/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/products/routes.py b/project/products/routes.py new file mode 100644 index 0000000..a11f158 --- /dev/null +++ b/project/products/routes.py @@ -0,0 +1,174 @@ +# Modules from Flask Library +from flask import Flask, render_template, request, flash, Blueprint +from flask import session as login_session +from flask import make_response, redirect, jsonify, url_for +# Access database +from project.models import Category, Product, User +from project.db import session + +# Services +from project.services.categories import CategoryService +from project.services.auth import AuthService +from project.services.products import ProductService + +import random +import string +import httplib2 +import json +import requests + +products = Blueprint('products', __name__) + +# Instantiate services +category_service = CategoryService() +product_service = ProductService() +auth_service = AuthService() + + +@products.route('/categories///') +def showProduct(category_id, product_id): + """App route function to show selected product.""" + # Check if user is logged in and save the result in isLogin + isLogin = auth_service.is_user_authorized() + # Get category that is selected to be shown + category = category_service.get_category_by_id(category_id) + product = product_service.get_product_by_id(product_id) + # Check if user is Creator of Category or Product + isCreator = False + if isLogin: + isCreator = (login_session['email'] == category.user.email or + login_session['email'] == product.user.email) + return render_template('product.html', + categories=category_service.get_all_categories(), + category=category, product=product, + isLogin=isLogin, isCreator=isCreator) + + +@products.route('/addproduct/', methods=['GET', 'POST']) +def addProduct(): + """App route function to add new Product to the Product list.""" + # If user is not logged in, inform him about it and redirect + if 'email' not in login_session: + return redirect('/login') + if request.method == 'POST': + # Check if all the fields are filled out + if request.form['name'] == '': + flash("Please, enter product name. \ + Make sure you fill out all the fields") + return render_template('addproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + if request.form['image_url'] == '': + flash("Please, enter product image(link of the image). \ + Make sure you fill out all the fields") + return render_template('addproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + if request.form['product_url'] == '': + flash("Please, enter product URL. \ + Make sure you fill out all the fields") + return render_template('addproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + if request.form['description'] == '': + flash('Please, enter product description. \ + Make sure you fill out all the fields') + return render_template('addproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + + category = (session.query(Category) + .filter_by(name=request.form['category_name']).one()) + newProduct = Product(name=request.form['name'], + description=request.form['description'], + image_url=request.form['image_url'], + product_url=request.form['product_url'], + category_id=category.id, + user_id=login_session['user_id']) + session.add(newProduct) + session.commit() + flash('New Product {} Successfully Created'.format(newProduct.name)) + return redirect(url_for('categories.showCategoryProducts', + category_id=newProduct.category_id)) + else: + return render_template('addproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized()) + + +@products.route('/editproduct///', + methods=['GET', 'POST']) +def editProduct(category_id, product_id): + """App route function to edit existing Product.""" + # If user is not logged in, inform him about it and redirect + if 'email' not in login_session: + flash("You need to Log In if you want to edit") + return redirect('/login') + # Get Product that user wants to edit + productToEdit = product_service.get_product_by_id(product_id) + # Check if user is Creator, if not inform that he cannot + # do changes + if not login_session['email'] == productToEdit.user.email: + flash("You need to be a Creator of the Product to edit it") + return redirect(url_for('products.showProduct', + category_id=category_id, product_id=product_id)) + if request.method == 'POST': + if request.form['name']: + productToEdit.name = request.form['name'] + if request.form['image_url']: + productToEdit.image_url = request.form['image_url'] + if request.form['product_url']: + productToEdit.product_url = request.form['product_url'] + if request.form['description']: + productToEdit.description = request.form['description'] + session.add(productToEdit) + session.commit() + flash('You successfully edited product') + return redirect(url_for('products.showProduct', + category_id=category_id, product_id=product_id)) + else: + return render_template('editproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized(), + category=(category_service + .get_category_by_id(category_id)), + productToEdit=productToEdit) + + +@products.route('/deleteproduct///', + methods=['GET', 'POST']) +def deleteProduct(category_id, product_id): + """App route function to delete existing product.""" + # If user is not logged in, inform him about it and redirect + if 'email' not in login_session: + flash("You need to Log In if you want to delete product") + return redirect('/login') + # Get the Product that User wants to delete + productToDelete = product_service.get_product_by_id(product_id) + # Check if user is Creator, if not inform that he cannot + # do changes + if not login_session['email'] == productToDelete.user.email: + flash("You need to be a Creator of the Product to delete it") + return redirect(url_for('products.showProduct', + category_id=category_id, product_id=product_id)) + if request.method == 'POST': + session.delete(productToDelete) + session.commit() + flash('You successfully deleted \ + product "{}"'.format(productToDelete.name)) + return redirect(url_for('categories.showCategoryProducts', + category_id=category_id)) + else: + return render_template('deleteproduct.html', + categories=(category_service + .get_all_categories()), + isLogin=auth_service.is_user_authorized(), + category=(category_service + .get_category_by_id(category_id)), + productToDelete=productToDelete) diff --git a/project/services/__init__.py b/project/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/services/auth.py b/project/services/auth.py new file mode 100644 index 0000000..b0a8ffe --- /dev/null +++ b/project/services/auth.py @@ -0,0 +1,11 @@ +from flask import session as login_session + + +class AuthService(): + + def __init__(self): + self.login_session = login_session + + def is_user_authorized(self): + """ returns if user is logged in """ + return 'email' in self.login_session diff --git a/project/services/categories.py b/project/services/categories.py new file mode 100644 index 0000000..1094d2c --- /dev/null +++ b/project/services/categories.py @@ -0,0 +1,16 @@ +from project.models import Category, Product, User +from project.db import session + + +class CategoryService(): + + def __init__(self): + self.session = session + + def get_all_categories(self): + """ returns all categories ordered by name""" + return self.session.query(Category).order_by(Category.name).all() + + def get_category_by_id(self, category_id): + """ returns a category that is selected to be it's id """ + return self.session.query(Category).filter_by(id=category_id).one() diff --git a/project/services/config.py b/project/services/config.py new file mode 100644 index 0000000..1efd17a --- /dev/null +++ b/project/services/config.py @@ -0,0 +1,11 @@ +import json + + +class ConfigService(): + + def __init__(self): + self.client_secrets = json.loads(open('client_secrets.json', 'r') + .read())['web'] + + def get_setting(self, setting): + return self.client_secrets[setting] diff --git a/project/services/products.py b/project/services/products.py new file mode 100644 index 0000000..9c0890d --- /dev/null +++ b/project/services/products.py @@ -0,0 +1,31 @@ +from project.models import Category, Product, User +from project.db import session + + +class ProductService(): + + def __init__(self): + self.session = session + + def get_product_by_id(self, product_id): + """ return selected by its id product """ + return self.session.query(Product).filter_by(id=product_id).one() + + def get_latest_products(self, limit): + """ returns the most recently added products""" + return (self.session.query(Product) + .order_by(Product.created_date.desc()).limit(limit)) + + def get_all_products(self): + """ return all products from catalog """ + return self.session.query(Product).all() + + def get_products_by_category_id(self, category_id): + """ returns all products from selected category """ + return (self.session.query(Product) + .filter_by(category_id=category_id).all()) + + def products_count(self, category_id): + """ returns count of how many products are in the category """ + return (self.session.query(Product) + .filter_by(category_id=category_id).count()) diff --git a/project/services/user.py b/project/services/user.py new file mode 100644 index 0000000..1e0de50 --- /dev/null +++ b/project/services/user.py @@ -0,0 +1,15 @@ +from project.models import Category, Product, User +from project.db import session + +class UserService(): + + def __init__(self): + self.session = session + + def get_user_by_email(self, email): + """Returns User object by email""" + return self.session.query(User).filter_by(email=email).one() + + def get_user_by_id(self, user_id): + """Returns User object by id""" + return self.session.query(User).filter_by(id=user_id).one() \ No newline at end of file diff --git a/static/images/favicon.png b/project/static/images/favicon.png similarity index 100% rename from static/images/favicon.png rename to project/static/images/favicon.png diff --git a/static/styles.css b/project/static/styles.css similarity index 100% rename from static/styles.css rename to project/static/styles.css diff --git a/templates/addcategory.html b/project/templates/addcategory.html similarity index 100% rename from templates/addcategory.html rename to project/templates/addcategory.html diff --git a/templates/addproduct.html b/project/templates/addproduct.html similarity index 100% rename from templates/addproduct.html rename to project/templates/addproduct.html diff --git a/templates/category.html b/project/templates/category.html similarity index 72% rename from templates/category.html rename to project/templates/category.html index d10c649..44b7a4b 100644 --- a/templates/category.html +++ b/project/templates/category.html @@ -10,7 +10,7 @@

{% endif %} {% if isCreator %} - edit edit delete {% endif %}

@@ -24,7 +24,7 @@

{{ product.name }}

- Explore + Explore
diff --git a/templates/deletecategory.html b/project/templates/deletecategory.html similarity index 79% rename from templates/deletecategory.html rename to project/templates/deletecategory.html index f4c7c6f..6c720f2 100644 --- a/templates/deletecategory.html +++ b/project/templates/deletecategory.html @@ -8,7 +8,7 @@

Are you sure you want to delete "{{categoryToDelete.name}}" category?

- Cancel + Cancel diff --git a/templates/deleteproduct.html b/project/templates/deleteproduct.html similarity index 82% rename from templates/deleteproduct.html rename to project/templates/deleteproduct.html index 9bf7540..772bb59 100644 --- a/templates/deleteproduct.html +++ b/project/templates/deleteproduct.html @@ -8,7 +8,7 @@

Are you sure you want to delete "{{productToDelete.name}}" product?

- Cancel diff --git a/templates/editcategory.html b/project/templates/editcategory.html similarity index 88% rename from templates/editcategory.html rename to project/templates/editcategory.html index 1e16901..14af1b7 100644 --- a/templates/editcategory.html +++ b/project/templates/editcategory.html @@ -14,7 +14,7 @@

Edit Category "{{categoryToEdit.name}}"

- Cancel + Cancel diff --git a/templates/editproduct.html b/project/templates/editproduct.html similarity index 90% rename from templates/editproduct.html rename to project/templates/editproduct.html index dda5ef0..a3eb881 100644 --- a/templates/editproduct.html +++ b/project/templates/editproduct.html @@ -16,7 +16,7 @@

Edit Product "{{ productToEdit.name }}"

- Cancel diff --git a/templates/index.html b/project/templates/index.html similarity index 83% rename from templates/index.html rename to project/templates/index.html index 57ae5d6..fb66522 100644 --- a/templates/index.html +++ b/project/templates/index.html @@ -19,7 +19,7 @@

Recently added products

{{ product.category.name }} {{ product.user.name }} - Explore + Explore {% endfor %} diff --git a/templates/login.html b/project/templates/login.html similarity index 75% rename from templates/login.html rename to project/templates/login.html index 6ebd246..e228131 100644 --- a/templates/login.html +++ b/project/templates/login.html @@ -1,27 +1,14 @@ {% extends 'main.html' %} {% block content %} - +

Please Sign In

- + data-approvalprompt="force">Google "G" LogoLogin with Google +

You will be permitted to create, edit or delete categories and products.

diff --git a/templates/main.html b/project/templates/main.html similarity index 97% rename from templates/main.html rename to project/templates/main.html index 9d0b5c8..ba72bad 100644 --- a/templates/main.html +++ b/project/templates/main.html @@ -72,7 +72,7 @@

Categories

diff --git a/templates/product.html b/project/templates/product.html similarity index 67% rename from templates/product.html rename to project/templates/product.html index cb9e9b3..e258096 100644 --- a/templates/product.html +++ b/project/templates/product.html @@ -5,8 +5,8 @@
{% if isCreator %} {% else %}
@@ -18,7 +18,7 @@

{{ product.name }}

{{ product.description }}

Learn More - Other + Other {{ category.name }} Products
diff --git a/run.py b/run.py new file mode 100644 index 0000000..2271e5d --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from project import app + +if __name__ == '__main__': + app.run('0.0.0.0', port=8000) \ No newline at end of file