diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96b94f4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*~
+node_modules
+
diff --git a/3-deploy_web_static.py b/3-deploy_web_static.py
new file mode 100755
index 0000000..097f319
--- /dev/null
+++ b/3-deploy_web_static.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python3
+"""Fab file to archive web_static content"""
+from os import remove
+from os.path import isfile
+from fabric.api import *
+from datetime import datetime
+
+
+env.hosts = ['35.245.121.190']
+
+
+def do_pack():
+ """Pack web_static files into archive"""
+ try:
+ dt = datetime.now().strftime("%Y%m%d%H%M%S")
+ local("mkdir -p versions/")
+ path = local("tar -cvzf versions/tript_{}.tgz tript"
+ .format(dt))
+ return "versions/tript_{}.tgz".format(dt)
+ except:
+ return None
+
+
+def do_deploy(archive_path):
+ """Deploy function for archive to get deployed to servers"""
+ if not isfile(archive_path):
+ return False
+ fileNameExt = archive_path.split('/')[-1]
+ fileName = fileNameExt.split(".")[0]
+ result = put(archive_path, '/tmp/{}'.format(fileNameExt))
+ if result.failed:
+ return False
+ result = sudo("rm -rf /data/tript/releases/{}/".format(fileName))
+ if result.failed:
+ return False
+ result = sudo("mkdir -p /data/tript/releases/{}/".format(fileName))
+ if result.failed:
+ return False
+ result = sudo("tar -xzf /tmp/{} -C /data/tript/releases/{}/"
+ .format(fileNameExt, fileName))
+ if result.failed:
+ return False
+ result = sudo("rm /tmp/{}".format(fileNameExt))
+ if result.failed:
+ return False
+ input = "mv /data/tript/releases/{}/tript/*\
+ /data/tript/releases/{}/".format(fileName, fileName)
+ result = sudo(input)
+ if result.failed:
+ return False
+ result = sudo("rm -rf /data/tript/releases/{}/tript"
+ .format(fileName))
+ if result.failed:
+ return False
+ result = sudo("rm -rf /data/tript/current")
+ if result.failed:
+ return False
+ result = sudo("ln -s /data/tript/releases/{}/ /data/tript/current"
+ .format(fileName))
+ if result.failed:
+ return False
+ print("New version deployed!")
+ return True
+
+
+def deploy():
+ """Call pack and deploy"""
+ path = do_pack()
+ if path is None:
+ return False
+ do_deploy(path)
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 0000000..8dd0f4e
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python3
+"""Start Flask web app for our TripleTail
+website"""
+
+from flask import Flask, jsonify, render_template, request
+from flask_cors import CORS, cross_origin
+from user import User
+
+import requests
+
+# Flask setup
+app = Flask(__name__)
+app.url_map.strict_slashes = False
+host = '0.0.0.0'
+host = '127.0.0.1'
+# Cross-Origin Resource sharing
+cors = CORS(app, resouces={r"/*": {"origins": "*"}})
+
+authorization_base_url = 'https://github.com/login/oauth/authorize'
+token_url = 'https://github.com/login/oauth/access_token'
+request_url = 'https://api.github.com'
+
+
+def page_not_found(e):
+ """404 error page for nonexistent routes"""
+ return jsonify({'error': "Not found"}), 404
+
+
+@app.route('/', methods=['GET', 'POST'])
+def homepage():
+ """
+ Landing page
+ """
+ return render_template('index.html')
+
+
+@app.route('/testing')
+def test():
+ userinfo = {'tier': 'tier1', 'username': 'hello', 'name': 'Random Name'}
+ return render_template('ranking.html', userinfo=userinfo)
+
+
+@app.route('/tier', methods=['GET', 'POST'])
+def handle_callback():
+ """
+ This function helps exchange temporary 'code' value with a permanent
+ access_token.
+ """
+
+ if 'code' in request.args:
+ payload = {
+ 'client_id': '926a308eec433f17e3ff',
+ 'client_secret': '8b3e897ad2ac2e85f558fd96571f6af5eca26511',
+ 'code': request.args['code']
+ }
+ headers = {'Accept': 'application/json'}
+ req = requests.post(token_url, params=payload, headers=headers)
+ resp = req.json()
+ if 'access_token' in resp:
+ user_info = User(resp['access_token'])
+ print(user_info.__dict__)
+ return render_template('ranking.html', userinfo=user_info.__dict__)
+ else:
+ return render_template('index.html')
+ else:
+ return render_template('index.html')
+
+if __name__ == "__main__":
+ app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
+ app.register_error_handler(404, page_not_found)
+ app.run(host=host)
diff --git a/backend/static/images/clogo.png b/backend/static/images/clogo.png
new file mode 100644
index 0000000..02afa53
Binary files /dev/null and b/backend/static/images/clogo.png differ
diff --git a/backend/static/images/elephant.png b/backend/static/images/elephant.png
new file mode 100644
index 0000000..199c6fb
Binary files /dev/null and b/backend/static/images/elephant.png differ
diff --git a/backend/static/images/image.png b/backend/static/images/image.png
new file mode 100644
index 0000000..40e22cf
Binary files /dev/null and b/backend/static/images/image.png differ
diff --git a/backend/static/images/kangaroo.png b/backend/static/images/kangaroo.png
new file mode 100644
index 0000000..36a251e
Binary files /dev/null and b/backend/static/images/kangaroo.png differ
diff --git a/backend/static/images/lion.png b/backend/static/images/lion.png
new file mode 100644
index 0000000..6ca24f3
Binary files /dev/null and b/backend/static/images/lion.png differ
diff --git a/backend/static/images/logo.png b/backend/static/images/logo.png
new file mode 100644
index 0000000..82e0a26
Binary files /dev/null and b/backend/static/images/logo.png differ
diff --git a/backend/static/images/pig.png b/backend/static/images/pig.png
new file mode 100644
index 0000000..03d96de
Binary files /dev/null and b/backend/static/images/pig.png differ
diff --git a/backend/static/images/sloth.png b/backend/static/images/sloth.png
new file mode 100644
index 0000000..9480179
Binary files /dev/null and b/backend/static/images/sloth.png differ
diff --git a/backend/static/scripts/auth.js b/backend/static/scripts/auth.js
new file mode 100644
index 0000000..eb93639
--- /dev/null
+++ b/backend/static/scripts/auth.js
@@ -0,0 +1,14 @@
+const ready = () => {
+ login()
+}
+
+const login = () => {
+ $('.signin .github-signin').click(github_oauth());
+ alert("hi")
+}
+const github_oauth = ()=> {
+ // Requests an api, which retrieves the auth token
+ window.location.href = "https://github.com/login/oauth/authorize?client_id=926a308eec433f17e3ff"
+}
+
+document.addEventListener('DOMContentLoaded', ready);
diff --git a/backend/static/scripts/design.js b/backend/static/scripts/design.js
new file mode 100644
index 0000000..024e56d
--- /dev/null
+++ b/backend/static/scripts/design.js
@@ -0,0 +1,13 @@
+$('.btn-clear').on('click', () => {
+ if ($('body').hasClass('black'))
+ {
+ $('body').removeClass('black');
+ $('body').addClass('blue');
+ }
+ else
+ {
+ $('body').removeClass('blue');
+ $('body').addClass('black');
+ }
+});
+
diff --git a/backend/static/scripts/index.js b/backend/static/scripts/index.js
new file mode 100644
index 0000000..085f441
--- /dev/null
+++ b/backend/static/scripts/index.js
@@ -0,0 +1,114 @@
+import '@babel/polyfill';
+
+const toParams = query => {
+ const q = query.replace(/^\??\//, '');
+
+ return q.split('&').reduce((values, param) => {
+ const [key, value] = param.split('=');
+
+ values[key] = value;
+
+ return values;
+ }, {});
+};
+
+const toQuery = (params, delimiter = '&') => {
+ const keys = Object.keys(params);
+
+ return keys.reduce((str, key, index) => {
+ let query = `${str}${key}=${params[key]}`;
+
+ if (index < keys.length - 1) {
+ query += delimiter;
+ }
+
+ return query;
+ }, '');
+};
+
+class PopupWindow {
+ constructor(
+ params,
+ options = { height: 1000, width: 600 },
+ url = `https://github.com/login/oauth/authorize`,
+ id = 'github-oauth-authorize'
+ ) {
+ this.id = id;
+ this.url = url + '?' + toQuery(params);
+ this.options = options;
+ }
+
+ open() {
+ const { url, id, options } = this;
+
+ this.window = window.open(url, id, toQuery(options, ','));
+ }
+
+ close() {
+ this.cancel();
+ this.window.close();
+ }
+
+ poll() {
+ this.promise = new Promise((resolve, reject) => {
+ this._iid = window.setInterval(() => {
+ try {
+ const popup = this.window;
+
+ if (!popup || popup.closed !== false) {
+ this.close();
+
+ reject(new Error('The popup was closed'));
+
+ return;
+ }
+
+ if (
+ popup.location.href === this.url ||
+ popup.location.pathname === 'blank'
+ ) {
+ return;
+ }
+
+ const params = toParams(popup.location.search.replace(/^\?/, ''));
+
+ resolve(params);
+
+ this.close();
+ } catch (error) {}
+ }, 500);
+ });
+ }
+
+ cancel() {
+ if (this._iid) {
+ window.clearInterval(this._iid);
+ this._iid = null;
+ }
+ }
+
+ then(...args) {
+ return this.promise.then(...args);
+ }
+
+ catch(...args) {
+ return this.promise.then(...args);
+ }
+
+ static open(...args) {
+ const popup = new this(...args);
+
+ popup.open();
+ popup.poll();
+
+ return popup;
+ }
+}
+
+export const loginWithGithub = (params, options, url, id) => {
+ return new Promise(async (resolve, reject) => {
+ const popup = PopupWindow.open(params, options, url, id);
+
+ popup.then(resolve, reject);
+ });
+};
diff --git a/backend/static/scripts/ranking.js b/backend/static/scripts/ranking.js
new file mode 100644
index 0000000..ce163d8
--- /dev/null
+++ b/backend/static/scripts/ranking.js
@@ -0,0 +1,11 @@
+$('.back').css('visibility', 'hidden');
+
+$('#rotatefront').click(() => {
+ $('.front').css('visibility', 'hidden');
+ $('.back').css('visibility', 'visible');
+});
+
+$('#rotateback').click(() => {
+ $('.back').css('visibility', 'hidden');
+ $('.front').css('visibility', 'visible');
+});
diff --git a/backend/static/styles/index.css b/backend/static/styles/index.css
new file mode 100644
index 0000000..18643da
--- /dev/null
+++ b/backend/static/styles/index.css
@@ -0,0 +1,8 @@
+h1#logo a {
+ max-width: 80%;
+ height: 80%;
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%;
+}
diff --git a/backend/static/styles/layout.css b/backend/static/styles/layout.css
new file mode 100644
index 0000000..c533e5f
--- /dev/null
+++ b/backend/static/styles/layout.css
@@ -0,0 +1,45 @@
+html {
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.header {
+ margin: 1rem;
+ box-sizing: border-box;
+ font-family: Avenir;
+ color: #6ec4db;
+ position: relative;
+ display: flex;
+ flex-wrap: nowrap;
+}
+
+body.black {
+ background-color: black;
+}
+
+body.blue {
+}
+
+.btn-clear {
+ border: none;
+ background: transparent;
+}
+
+.content {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 85%;
+}
+
+.facebook-button, .twitter-button {
+ display: inline-block;
+}
diff --git a/backend/static/styles/ranking.css b/backend/static/styles/ranking.css
new file mode 100644
index 0000000..00cd8f3
--- /dev/null
+++ b/backend/static/styles/ranking.css
@@ -0,0 +1,75 @@
+.my-card {
+ background-color: white;
+ max-width: 80%;
+ height: 80%;
+ width: auto;
+ padding: 1.5rem;
+}
+
+.front-side {
+ display: flex;
+ flex-direction: row;
+ padding: 1rem;
+}
+
+.front-side-body {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+}
+
+.face {
+ position: relative;
+ height: 100%;
+}
+
+.user-image {
+ width: 40%;
+ height: 90%;
+ background-size: contain;
+ padding-right: 2rem;
+}
+
+.front {
+ position: relative;
+}
+
+.back {
+ position: absolute;
+ left: 0;
+ top: 0%;
+ width: 100%;
+ height: 100%;
+}
+
+.card {
+ width: 75%;
+ border: 5px solid rgba(110, 196, 219, 1);
+ padding: 1rem;
+}
+
+.card-body {
+ padding: 1rem;
+ width: 100%;
+ height: 100%;
+}
+
+.rotate-button {
+ position: absolute;
+ bottom: 2%;
+ transform: translate(-50%, -50%);
+ left: 50%;
+}
+
+.avatar-git {
+ height: 40%;
+}
+
+.avatar-animal {
+ height: 60%;
+}
+
+.img {
+ max-height: 100%;
+ max-width: 100%;
+}
diff --git a/backend/templates/base.html b/backend/templates/base.html
new file mode 100644
index 0000000..14015f7
--- /dev/null
+++ b/backend/templates/base.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+ {% block css %}{% endblock %}
+ {% block title %}{% endblock %} - Triple Tail
+
+
+
+
+
+ {% block header %}{% endblock %}
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+ {% block script %}{% endblock %}
+
+
diff --git a/backend/templates/index.html b/backend/templates/index.html
new file mode 100644
index 0000000..e7ba45e
--- /dev/null
+++ b/backend/templates/index.html
@@ -0,0 +1,29 @@
+{% extends 'base.html' %}
+{% block css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+
+{% block script %}
+
+
+{% endblock %}
diff --git a/backend/templates/ranking.html b/backend/templates/ranking.html
new file mode 100644
index 0000000..7aa2e8c
--- /dev/null
+++ b/backend/templates/ranking.html
@@ -0,0 +1,76 @@
+{% extends 'base.html' %}
+{% block css %}
+
+{% endblock %}
+
+{% block log_out %}
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+

+
+
+
+
+
{{ userinfo.name }}
+
{{ userinfo.username }}
+
Public Repos: {{ userinfo.public_repos }}
+
Followers: {{ userinfo.followers }}
+
Hireable: {{ userinfo.hireable }}
+
+
+
+
+
+
+
+
+
+ {% if userinfo.tier == 'O(n!)' %}
+
+
About This Rank
+
+
Rank
+
O(n!)
+
This mighty sloth is on its way to greatness
+
+ {% elif userinfo.tier == 'O(n)' %}
+
+
Rank
+
+
Time Complexity
+
O(n)
+
Mediocrity is the elephant in the room
+
+ {% else %}
+
+
Rank
+
+
Time Complexity
+
O(1)
+
Long live the king!
+ {% endif %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block script %}
+
+{% endblock %}
diff --git a/backend/user.py b/backend/user.py
new file mode 100755
index 0000000..3a28687
--- /dev/null
+++ b/backend/user.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+"""
+this module contains a User class
+"""
+
+
+from datetime import datetime, timedelta
+import re
+import requests
+
+
+class User:
+ """a User class"""
+ url = 'https://api.github.com'
+ tiers = {'tier1': 'O(n!)', 'tier2': 'O(n)', 'tier3': 'O(1)'}
+
+ def __init__(self, token):
+ """instantiation of User class object"""
+ self.token = token
+ self.user_dict = self.user_dict()
+ self.username = self.username()
+ self.public_repos = self.public_repos()
+ self.followers = self.followers()
+ self.account_age = self.account_age()
+ self.readme_pct = self.readme_pct()
+ self.tier = self.tier()
+ self.avatar_url = self.avatar_url()
+ self.following = self.following()
+ self.bio = self.bio()
+ self.hireable = self.hireable()
+ self.name = self.name()
+ self.email = self.email()
+
+ def user_dict(self):
+ """creates dictionary of user's info"""
+ header = {'Authorization': 'Bearer {}'.format(self.token)}
+ r = requests.get('https://api.github.com/user', headers=header)
+ allowed = ['public_repos', 'followers']
+ return r.json()
+
+ def public_repos(self):
+ """returns number of public repos of a user"""
+ return self.user_dict['public_repos']
+
+ def followers(self):
+ """returns number of followers of a user"""
+ return self.user_dict['followers']
+
+ def account_age(self):
+ """calculates and returns age of the account"""
+ self.now = datetime.now()
+ self.created_at = datetime.strptime(self.user_dict['created_at'],
+ "%Y-%m-%dT%H:%M:%SZ")
+ age = self.now - self.created_at
+ age_str = str(age)
+ age_str_split = re.split('[, :\.]*', age_str)
+ if len(age_str_split) > 5:
+ age_dhms = age_str_split[0] \
+ + ' ' + age_str_split[1] \
+ + ' ' + age_str_split[2] \
+ + ' hours ' + age_str_split[3] \
+ + ' minutes ' + age_str_split[4] \
+ + ' seconds'
+ else:
+ age_dhms = '0 days ' \
+ + age_str_split[0] \
+ + ' hours ' + age_str_split[1] \
+ + ' minutes ' + age_str_split[2] \
+ + ' seconds'
+ return age_dhms
+
+ def readme_pct(self):
+ """Returns the percentage of repos that have a README"""
+ header = {'Authorization': 'Bearer {}'.format(self.token)}
+ repo_list = requests.get('{}/users/{}/repos'
+ .format(self.url, self.username),
+ headers=header).json()
+ readme_count = 0
+ if self.user_dict.get('public_repos') is 0:
+ return 0
+ for repo in repo_list:
+ response = requests.get('{}/repos/{}/{}/readme'
+ .format(self.url,
+ self.username,
+ repo.get('name')),
+ headers=header)
+ if response.status_code == 404:
+ pass
+ else:
+ readme_count += 1
+ return '{:.1%}'.format(readme_count/self.user_dict.get('public_repos'))
+
+ def tier(self):
+ """returns the tier of the user"""
+ acc_age_str_split = self.account_age.split()
+ days_old = acc_age_str_split[0]
+ if self.followers > 4 and \
+ self.public_repos > 10:
+ return User.tiers['tier3']
+ elif self.followers > 0 and \
+ self.public_repos > 4:
+ return User.tiers['tier2']
+ else:
+ return User.tiers['tier1']
+
+ def avatar_url(self):
+ """returns url of user's avatar"""
+ return self.user_dict['avatar_url']
+
+ def username(self):
+ """returns username"""
+ return self.user_dict['login']
+
+ def following(self):
+ """returns number of users the user is following"""
+ return self.user_dict['following']
+
+ def bio(self):
+ """get users bio"""
+ return self.user_dict['bio']
+
+ def hireable(self):
+ """status on whether user is hireable"""
+ return self.user_dict['hireable']
+
+ def name(self):
+ """users name on github"""
+ return self.user_dict['name']
+
+ def email(self):
+ """get users email"""
+ return self.user_dict['email']
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4668a55
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "tripletail",
+ "version": "1.0.0",
+ "description": "Derrick's README",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/stak21/TripleTail.git"
+ },
+ "author": "Shoji Takashima, Sammie Azad, Susan Su, Phu Truong, Stef Silverio, Derrick Gee, Wendy Leung",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/stak21/TripleTail/issues"
+ },
+ "homepage": "https://github.com/stak21/TripleTail#readme",
+ "dependencies": {
+ "simple-crypto-js": "^2.0.2"
+ }
+}
diff --git a/requirements.txt b/requirements.txt
index bc972d3..6463ec1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
Fabric3==1.14.post1
Python3==3.4.3
Flask==1.0.2
+beautifulsoup4==4.7.1
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..324e089
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+ Triple Tail
+
+
+
+
+
+
+
+
+
+