diff --git a/README.md b/README.md deleted file mode 100644 index 1fd837b..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# cmpe281-starks diff --git a/Recommendation System/Functional Flow b/Recommendation System/Functional Flow new file mode 100644 index 0000000..d9b46cc --- /dev/null +++ b/Recommendation System/Functional Flow @@ -0,0 +1 @@ +RE diff --git a/Recommendation System/app.py b/Recommendation System/app.py new file mode 100644 index 0000000..7e89198 --- /dev/null +++ b/Recommendation System/app.py @@ -0,0 +1,7 @@ +from re import app +import os + +app.secret_key = os.urandom(24) +port = int(os.environ.get('PORT', 5000)) +app.run(host='0.0.0.0', port=port) + diff --git a/Recommendation System/re/__init__.py b/Recommendation System/re/__init__.py new file mode 100644 index 0000000..11e246c --- /dev/null +++ b/Recommendation System/re/__init__.py @@ -0,0 +1,2 @@ +from .views import app +from .models import graph diff --git a/Recommendation System/re/models.py b/Recommendation System/re/models.py new file mode 100644 index 0000000..1f7700a --- /dev/null +++ b/Recommendation System/re/models.py @@ -0,0 +1,125 @@ +from py2neo import Graph, Node, Relationship +from passlib.hash import bcrypt +from datetime import datetime +import os +import uuid + +url = os.environ.get('GRAPHENEDB_URL', 'http://localhost:7474') +username = os.environ.get('NEO4J_USERNAME') +password = os.environ.get('NEO4J_PASSWORD') + +graph = Graph(url + '/db/data/', username=username, password=password) + +class User: + def __init__(self, username): + self.username = username + + def find(self): + user = graph.find_one('User', 'username', self.username) + return user + + def register(self, password): + if not self.find(): + user = Node('User', username=self.username, password=bcrypt.encrypt(password)) + graph.create(user) + return True + else: + return False + + def verify_password(self, password): + user = self.find() + if user: + return bcrypt.verify(password, user['password']) + else: + return False + + def add_post(self, title, tags, text): + user = self.find() + post = Node( + 'Post', + id=str(uuid.uuid4()), + title=title, + text=text, + timestamp=timestamp(), + date=date() + ) + rel = Relationship(user, 'PUBLISHED', post) + graph.create(rel) + + tags = [x.strip() for x in tags.lower().split(',')] + for name in set(tags): + tag = Node('Tag', name=name) + graph.merge(tag) + + rel = Relationship(tag, 'TAGGED', post) + graph.create(rel) + + def like_post(self, post_id): + user = self.find() + post = graph.find_one('Post', 'id', post_id) + graph.merge(Relationship(user, 'LIKED', post)) + + def get_recent_posts(self): + query = ''' + MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag) + WHERE user.username = {username} + RETURN post, COLLECT(tag.name) AS tags + ORDER BY post.timestamp DESC LIMIT 5 + ''' + + return graph.run(query, username=self.username) + + def get_products(self): + query = ''' + MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag) + WHERE user.username = {username} + RETURN post, COLLECT(tag.name) AS tags + ORDER BY post.timestamp DESC LIMIT 5 + ''' + + return graph.run(query, username=self.username) + + def get_similar_users(self): + + query = ''' + MATCH (you:User)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag:Tag), + (they:User)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag) + WHERE you.username = {username} AND you <> they + WITH they, COLLECT(DISTINCT tag.name) AS tags + ORDER BY SIZE(tags) DESC LIMIT 3 + RETURN they.username AS similar_user, tags + ''' + + return graph.run(query, username=self.username) + + def get_commonality_of_user(self, other): + + query = ''' + MATCH (they:User {username: {they} }) + MATCH (you:User {username: {you} }) + OPTIONAL MATCH (they)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag:Tag), + (you)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag) + RETURN SIZE((they)-[:LIKED]->(:Post)<-[:PUBLISHED]-(you)) AS likes, + COLLECT(DISTINCT tag.name) AS tags + ''' + + return graph.run(query, they=other.username, you=self.username).next + +def get_todays_recent_posts(): + query = ''' + MATCH (user:User)-[:PUBLISHED]->(post:Post)<-[:TAGGED]-(tag:Tag) + WHERE post.date = {today} + RETURN user.username AS username, post, COLLECT(tag.name) AS tags + ORDER BY post.timestamp DESC LIMIT 5 + ''' + + return graph.run(query, today=date()) + +def timestamp(): + epoch = datetime.utcfromtimestamp(0) + now = datetime.now() + delta = now - epoch + return delta.total_seconds() + +def date(): + return datetime.now().strftime('%Y-%m-%d') diff --git a/Recommendation System/re/static/style.css b/Recommendation System/re/static/style.css new file mode 100644 index 0000000..c6b030f --- /dev/null +++ b/Recommendation System/re/static/style.css @@ -0,0 +1,15 @@ +body { font-family: sans-serif; background: #eee; } +a, h1, h3 { color: #377ba8; } +h1, h2, h3 { font-family: 'Georgia', serif; margin: 0; } +h1, h2 { border-bottom: 2px solid #eee; padding: 3px; } +h3 { padding: 3px; } +dd { display: block; margin-left: 0px; } +dl { font-weight: bold; } +a:visited { color: #800080; } +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } +.posts { list-style: none; margin: 0; padding: 0; } +.posts li { margin: 0.8em 1.2em; } +.posts li h2 { margin-left: -1em; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } +.flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; } +.error { background: #f0d6d6; padding: 0.5em; } diff --git a/Recommendation System/re/templates/display_host b/Recommendation System/re/templates/display_host new file mode 100644 index 0000000..c51578f --- /dev/null +++ b/Recommendation System/re/templates/display_host @@ -0,0 +1,15 @@ + diff --git a/Recommendation System/re/templates/index.html b/Recommendation System/re/templates/index.html new file mode 100644 index 0000000..9e0b361 --- /dev/null +++ b/Recommendation System/re/templates/index.html @@ -0,0 +1,25 @@ +{% extends "layout.html" %} +{% block body %} + +

Home

+ {% if session.username %} +

Purchase New Items

+
+
+
Product Name:
+
+
Categories (separated by commas):
+
+
Description:
+
+
+ +
+ {% endif %} + +
+ +

Today's Recent Purchases

+{% include "display_posts.html" %} + +{% endblock %} diff --git a/Recommendation System/re/templates/layout.html b/Recommendation System/re/templates/layout.html new file mode 100644 index 0000000..d13041b --- /dev/null +++ b/Recommendation System/re/templates/layout.html @@ -0,0 +1,23 @@ + +My Recommendation Engine + +
+

My Recommendation Site

+
+ {% if session.username %} + Logged in as {{ session.username }} + {% endif %} + Home + {% if not session.username %} + Register + Login + {% else %} + Profile + Logout + {% endif %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block body %}{% endblock %} +
diff --git a/Recommendation System/re/templates/profile.html b/Recommendation System/re/templates/profile.html new file mode 100644 index 0000000..968d1c0 --- /dev/null +++ b/Recommendation System/re/templates/profile.html @@ -0,0 +1,51 @@ +{% extends "layout.html" %} +{% block body %} + +

{{ username }}'s profile

+ +{% if session.username %} + {% if session.username == username %} +

Users who brought items similar to you:

+ + {% for user in similar %} +

+ {{ user.similar_user }} + also purchased {{ ", ".join(user.tags) }} {{ ", ".join(user.posts) }} +

+ {% else %} +

There aren't any users who've purchased products the same category as you!

+ {% endfor %} + +

Recommendations for you

+ Based on your previous purchases + {% for product in products %} +

+ {{ product.post.title }} +

+ + + {% else %} +

There aren't any users who've purchased products the same category as you!

+ {% endfor %} + +

Your recent purchases:

+ + + {% else %} + +

{{ username }} has liked {{ common.likes }} of your purchases and + {% if common.tags %} + also purchased {{ ", ".join(common.tags) }} + {% else %} + hasn't purchased any of the same category + {% endif %} +

+ +

{{ username }}'s recent purchases:

+ + {% endif %} +{% endif %} + +{% include "display_posts.html" %} + +{% endblock %} diff --git a/Recommendation System/re/views.py b/Recommendation System/re/views.py new file mode 100644 index 0000000..dee8ddd --- /dev/null +++ b/Recommendation System/re/views.py @@ -0,0 +1,109 @@ +from .models import User, get_todays_recent_posts +from flask import Flask, request, session, redirect, url_for, render_template, flash + +app = Flask(__name__) + +@app.route('/') +def index(): + posts = get_todays_recent_posts() + return render_template('index.html', posts=posts) + +@app.route('/register', methods=['GET','POST']) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + if len(username) < 1: + flash('Your username must be at least one character.') + elif len(password) < 5: + flash('Your password must be at least 5 characters.') + elif not User(username).register(password): + flash('A user with that username already exists.') + else: + session['username'] = username + flash('Logged in.') + return redirect(url_for('index')) + + return render_template('register.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + if not User(username).verify_password(password): + flash('Invalid login.') + else: + session['username'] = username + flash('Logged in.') + return redirect(url_for('index')) + + return render_template('login.html') + +@app.route('/logout') +def logout(): + session.pop('username', None) + flash('Logged out.') + return redirect(url_for('index')) + +@app.route('/add_product', methods=['POST']) +def add_product(): + title = request.form['title'] + tags = request.form['tags'] + text = request.form['text'] + + if not title: + flash('You must give your post a title.') + elif not tags: + flash('You must give your post at least one tag.') + elif not text: + flash('You must give your post a text body.') + else: + User(session['username']).add_post(title, tags, text) + + return redirect(url_for('index')) + +@app.route('/like_post/') +def like_post(post_id): + username = session.get('username') + + if not username: + flash('You must be logged in to like a post.') + return redirect(url_for('login')) + + User(username).like_post(post_id) + + flash('Liked post.') + return redirect(request.referrer) + +@app.route('/profile/') +def profile(username): + logged_in_username = session.get('username') + user_being_viewed_username = username + + user_being_viewed = User(user_being_viewed_username) + posts = user_being_viewed.get_recent_posts() + + similar = [] + common = [] + products = [] + + if logged_in_username: + logged_in_user = User(logged_in_username) + + if logged_in_user.username == user_being_viewed.username: + similar = logged_in_user.get_similar_users() + products = logged_in_user.get_products() + else: + common = logged_in_user.get_commonality_of_user(user_being_viewed) + + return render_template( + 'profile.html', + username=username, + posts=posts, + similar=similar, + common=common, + products=products + ) diff --git a/Recommendation System/requirements.txt b/Recommendation System/requirements.txt new file mode 100644 index 0000000..797d5e6 --- /dev/null +++ b/Recommendation System/requirements.txt @@ -0,0 +1,13 @@ +bcrypt==3.1.0 +cffi==1.7.0 +click==6.6 +Flask==0.11.1 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 +passlib==1.6.5 +py2neo==3.1.1 +pycparser==2.14 +six==1.10.0 +Werkzeug==0.11.10 + diff --git a/Recommendation_Engine_Architecture_Diagram.png b/Recommendation_Engine_Architecture_Diagram.png new file mode 100644 index 0000000..c3ceea8 Binary files /dev/null and b/Recommendation_Engine_Architecture_Diagram.png differ