⭐ Star me on GitHub — just for fun and motivation 😀
HSE SMASH is my final project for Harvard CS50 online course. It is a web app that allows you to compare girls by there profile photos & find the hottest one 🔥 🔥 🔥 🔥 😏 😏 😏 😏
The idea of making such an app came to me after watching "The Social Network" movie. Where Mark Zuckerberg gets pissed off by his ex girlfriend and, being a little bit drunk, creates FaceSmash, a web-app that allows to compare Harvard girls between each other, two at a time.
I was not pissed by my ex, nor was I drunk, but decided to reimplement this app using Python as a backend language for my Flask Server and JS/HTML/CSS for my pages. Being a HSE student I have decided to use VK society for getting the profile pages of the girls from my university.
Below I give a detailed description of all the project, so feel free to use it as a template for making such a prank in your own university 😉
├── phpLiteAdmin
│ └── HSE.db
├── VK
│ └── vk_export.py
├── application.py
├── member.py
├── templates
│ └── credits.html
│ └── hottest.html
│ └── index.html
│ └── layout.html
│ └── personal.html
├── static
│ ├── fonts
│ │ └── SexyShoutFreeFont.ttf
│ │ └── SexyShoutFreeFont.otf
│ └── Jobs.jpg
│ └── Jobs2.jpg
│ └── Slushi.jpg
│ └── scripts.js
│ └── styles.css
├── LICENSE
├── README.md
Before writing my web app I need a database with my girls. For that I need to decide what tools to use and what fields to create for each member.
I have decided to use SQLite for this project and phpLiteAdmin for local management and testing. The latter was utilized for initial cretion of the db, table and fields, though it could be done programmatically in Python. Here is the link for how to start a local phpLiteAdmin server "https://bitbucket.org/phpliteadmin/public/wiki/NoWebserver".
The snipet of resulting "members" table:
tab_id | vk_id | first_name | last_name | sex | photo_link | rating |
---|---|---|---|---|---|---|
2 | 270 | Irina | Rybakova | 1 | https://pp.userapi.com/c629116/v629116270/14349/NivavpUia9k.jpg?ava=1 | 2.0 |
5 | 509 | Alyona | Vershinina | 1 | https://pp.userapi.com/c852128/v852128160/2b6c0/pnVQlSodoEE.jpg?ava=1 | 0.0 |
7 | 605 | Lena | Udodova | 1 | https://sun1-11.userapi.com/kDDxts6O4jouBIScMt4iH7nRVT_JKxzKkv9gaw/_IWoF3dFzlA.jpg?ava=1 | 0.0 |
8 | 680 | Katya | Semenko | 1 | https://pp.userapi.com/c627316/v627316680/43095/ZVEhuFxe59Y.jpg?ava=1 | 0.0 |
9 | 692 | Liza | Kulik | 1 | https://pp.userapi.com/c630416/v630416692/7446/Rbatb84-q9k.jpg?ava=1 | 0.0 |
13 | 796 | Alina | Sazonova | 1 | https://pp.userapi.com/c638923/v638923796/4ebfc/Pa1bYmNPZgE.jpg?ava=1 | 0.0 |
16 | 896 | Olga | Borodulina | 1 | https://pp.userapi.com/c313/u00896/a_2f90c1d9.jpg?ava=1 | 0.0 |
17 | 905 | Alina | Yashina | 1 | https://pp.userapi.com/c824504/v824504324/1a43e4/x0DNcr_UhG0.jpg?ava=1 | 0.0 |
Now let's look at vk_export.py which populates my database with girls.
I use vk-requests package for making VK API calls and sqlite3 for making queries.
import vk_requests
import sqlite3
from time import sleep
Then I create a connect to the database and create a cursor for making queries.
conn = sqlite3.connect("../phpLiteAdmin/HSE.db")
cur = conn.cursor()
I the main
function I get access to the api calls by using the token
for my application which I got from registering the application with VK API. In create_api
I also specify the scope
(with I would like to work in my app) and the api_version
.
Getting the all the users from the HSE Official Group posed a little change, because I could only get 1000 users per one request. That is why I have decided to get the whole number of comminuty members first and then, using the while loop, make requests with an offset
.
In the while loop I use getMembers
method, where I specify group_id
, sort
order, offset
, fields
that I need for the db and count
- max number of users I can get per one request.
Next I use db_upload
function for populating my local db and print into console for error checking. I also had an issue with "making too many requests" bugs, so I used sleep()
function for a little pause in execution. After that I increment i
, which is my offset variable.
In the end of the main
function I make one last API call for the rest of the users (I use 1000 as the increment, but the number of users is not a round one). I also use a cleanup
function for beautifying the database.
def main():
token = ...
api = vk_requests.create_api(service_token=token, scope=['friends', 'photos', 'groups', 'offline'], api_version='5.92')
count = api.groups.getMembers(group_id='hseofficial')['count']
i = 0
while i < count:
g_users = api.groups.getMembers(group_id='hseofficial', sort='id_asc', offset=i, fields=['sex', 'photo_max_orig', 'bday'],
count=1000)
db_upload(g_users)
print(g_users)
sleep(0.1)
i += 1000
db_upload(api.groups.getMembers(group_id='hseofficial', sort='id_asc', offset=(i-1000), fields=['sex', 'photo_max_orig'],
count=(count - i + 1000)))
cleanup()
db_upload
function gets a response from the API request and for every user in my 1000 returned ones executes an insertion into my "members" table. For this I utilize the cursor
which I have defined earlier and using an execute
method I write a simple query. Also never forget to do conn.commit()
for saving the changes (reminds me of a git commit
command).
def db_upload(resp):
for i in resp["items"]:
cur.execute("INSERT INTO members (vk_id, first_name, last_name, sex, photo_link) VALUES (:id, :f_n, :l_n, :s, :p_l)",
{"id": i.get('id'), "f_n": i.get('first_name'), "l_n": i.get('last_name'), "s": i.get("sex"), "p_l": i.get("photo_max_orig")})
conn.commit()
The cleanup
function makes some changes to the table through a number of SQL queries:
- Deletes all DELETED accounts
- Deletes all males (
sex
parameter equals 2) - Deletes all users with no picture
- Deletes all users with deactivated profile picture
def cleanup():
cur.execute("DELETE FROM members WHERE first_name = 'DELETED'")
cur.execute("DELETE FROM members WHERE sex = 2")
cur.execute("DELETE FROM members WHERE photo_link = 'https://vk.com/images/camera_400.png?ava=1'")
cur.execute("DELETE FROM members WHERE photo_link = 'https://vk.com/images/deactivated_400.png'")
conn.commit()
This file is the route of our backend Flask server. It contains commands for every page the user visits and the method the user uses to visit them (POST or GET).
So at first I need to import flask package for our server, flask_sqlalchemy for working with the database and also json
and Member
class from helping member.py.
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, redirect, render_template, request
import json
from member import Member
Then I make some default initial configs:
- Configure the application
- Ensure the templates are reloaded
- Disable commit tracking (Turn off Flask-SQLAlchemy event system for saving resources)
- Adding path to the database (Local or Remote, Ex. Heroku DB)
- Initiating DB object
- Creating a table class in order to read/write into the database
app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./phpLiteAdmin/HSE.db'
db = SQLAlchemy(app)
class Members(db.Model):
__tablename__ = 'members'
tab_id = db.Column('tab_id', db.Integer, primary_key=True, nullable=False)
vk_id = db.Column('vk_id', db.Integer, nullable=False)
first_name = db.Column('first_name', db.Text, nullable=False)
last_name = db.Column('last_name', db.Text, nullable=False)
sex = db.Column('sex', db.Integer, nullable=False)
photo_link = db.Column('photo_link', db.Text, nullable=False)
rating = db.Column('rating', db.Float, nullable=False)
def __init__(self, tab_id, vk_id, first_name, last_name, sex, photo_link, rating):
self.tab_id = tab_id
self.vk_id = vk_id
self.first_name = first_name
self.last_name = last_name
self.sex = sex
self.photo_link = photo_link
self.rating = rating
Now let's look at the rest of the code with the app
decorators, but before doing that I make sure that no responses are cached and make global vars for two compared girls using Member
class.
@app.after_request
def after_request(response):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Expires"] = 0
response.headers["Pragma"] = "no-cache"
return response
mem1 = Member(0, 0, "")
mem2 = Member(0, 0, "")
link_list = [mem1, mem2]
In addition I'll also discuss the Member
class in order to avoid futher confusion. It has only three parameters:
- un_id -- unique ID in "members" table
- rating -- a double number
- link -- a url of the profile picture
class Member:
def __init__(self, un_id, rating, link):
self.un_id = un_id
self.rating = rating
self.link = link
Let's return back to application.py.
If the user visits the main (index) page via GET, he/she gets presented with two random profile picture of girls, which the app gets by making a query to the "members" table. It assigns the corresponding values to the variables in link_list
and, in the end renders a template with the right links and ids.
@app.route("/", methods=["GET"])
def index_get():
global link_list
i = 0
girls = Members.query.order_by(func.random()).limit(2)
for g in girls:
link_list[i].un_id = g.tab_id
link_list[i].link = g.photo_link
link_list[i].rating = g.rating
i += 1
return render_template("index.html", link1=mem1.link, link2=mem2.link, id1=mem1.un_id, id2=mem2.un_id)
If the user visits the main (index) page via POST, so as by clicking on a button under the picture of the girl he/she likes more, my app needs to deal with how to give the girl the rating points and also reload the page to output the next random pair.
Using JSON we get data from POST request, which has information about which picture the user has choosen. This data contains s1
and s2
coefficients, which correspond to the 1st and the 2nd profile picture and can be either 1 or 0.
Using the received coefficients and the current girls' ratings I calculate their new ratings using the Elo Rating Algorithm and after that write the new numbers into the local database. In the end I dump a json that everything went without mistakes.
@app.route("/", methods=["POST"])
def index_post():
data = request.get_json()
ratings = elo_rate(mem1.rating, mem2.rating, data["s1"], data["s2"])
j = 0
for i in ratings:
girl = Members.query.filter_by(tab_id=link_list[j].un_id).first()
girl.rating = i
db.session.commit()
j += 1
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
Basically it means that if you have two girls, where one has a high rating and another one has a low rating and, supposedly, the girl with highest rating wins, she gets less points in comparison with the situation, when the girl with the low rating wins. It is a coommon rating system in strategic games and it is best explained in this video: https://www.youtube.com/watch?v=GTaAWtuLHuo&index=4&list=LLwfqVIYgpcUBxvjAdSkO05w
def elo_rate(rate1, rate2, s1, s2):
E1 = 1/(1 + pow(10, ((rate1 - rate2)/400)))
E2 = 1/(1 + pow(10, ((rate2 - rate1)/400)))
rate1_new = rate1 + 32*(s1 - E1)
rate2_new = rate2 + 32 * (s2 - E2)
return [rate1_new, rate2_new]
The last routes in my app to be discussed are pretty straightforward.
In the hottest I render a photo of a girl with the highest rating, which a get by making an ordering query to the table.
In the personal I have hardcoded my personal choice, github
just redirects to this page and, finally in the credits I have a thanking message to the CS50 staff.
@app.route("/hottest", methods=["GET"])
def hot():
girl = Members.query.order_by(desc(Members.rating)).first()
return render_template("hottest.html", link=girl.photo_link)
@app.route("/personal", methods=["GET"])
def personal():
link = "https://pp.userapi.com/c847020/v847020538/1427a1/lVggmbf3o-U.jpg"
return render_template("personal.html", link=link)
@app.route("/github", methods=["GET"])
def github():
return redirect("https://github.com/Snowfighter/CS50-Final-Project")
@app.route("/credits", methods=["GET"])
def credit():
return render_template("credits.html")
In scripts.js I have a function that figures out what to do when the left or right button of choice is clicked in index.html.
- I use
$(document).ready()
to make sure that the page is safely loaded. $("button").click()
checks if any button is pressed and executes a functionevent.preventDefault()
prevents from reloading the page
The rest of the code checks if right
or left
button was clicked on and assigns the right numbers to s1
and s2
. Then it makes an asynchronous POST request using ajax sending information to the main route, which the app processes in @app.route("/", methods=["POST"])
$(document).ready(function() {
$("button").click(function(event) {
event.preventDefault();
var s1 = 0;
var s2 = 0;
var value = $(this).val();
if (value == "left") {
s1 = 1;
s2 = 0
} else {
s1 = 0;
s2 = 1;
};
var postData = {
s1: s1,
s2: s2
}
$.ajax({
url: '/',
type: 'POST',
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(postData),
success: function(response) {
alert(value);
//$('#left_pic').attr('src', '/static/Jobs2.jpg');
//$('#right_pic').attr('src', '/static/Jobs.jpg');
window.location.href = "/";
},
error: function(response) {
alert("error");
}
});
});
});
In templates I have all html pages made in Bootstrap 4. If any given one needs to be passed a variable from application.py I use Jinja conventions for that.
In Heroku_db I have inserted code for making and populating a remote database on HEROKU. The files do not really differ a lot from vk_export.py, except for the remote urls. You also need to create not an SQLite db but HEROKU PostgreSQL, which is done in heroku_declaritive_db.py.
For more information about uploading your personal web app on HEROKU warch this video: https://www.youtube.com/watch?v=4_RYQJfiuVU&list=PLVB8q5l3DXHczVis7sU4ziKTX3pAqe8c9&index=3