How to Build a Python Web Application Using Flask and Neo4j

@_nicolemargaret

About Me

Context

Flaskr

SQLite

Flaskr

SQL to Neo4j

SQL

SELECT u.username, p.text, GROUP_CONCAT(t.name)
FROM Users u
JOIN Posts p
ON u.id = p.user_id
JOIN PostsTags pt
ON p.id = pt.post_id
JOIN Tags t
ON pt.tag_id = t.id
GROUP BY u.username, p.text

Neo4j

Neo4j

MATCH (u:User)-[:PUBLISHED]->(p:Post)<-[:TAGGED]-(t:Tag)
RETURN u.username, p.text, COLLECT(t.name)
pip install py2neo
from py2neo import Graph, Node, Relationship

graph = Graph()

user = Node("User", username="nicole")
post = Node("Post", text="hello pycon")

graph.create(Relationship(user, "PUBLISHED", post))

Application Features

neo4j-flask-blog.herokuapp.com

Register A User

views.py
@app.route('/register', methods=['GET','POST'])
def register():
    error = None

    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if len(username) < 1:
            error = 'Your username must be at least one character.'
        elif len(password) < 5:
            error = 'Your password must be at least 5 characters.'
        elif not User(username).register(password):
            error = 'A user with that username already exists.'
        else:
            session['username'] = username
            flash('Logged in.')
            return redirect(url_for('index'))

    return render_template('register.html', error=error)
models.py
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

Add A Post

@app.route('/add_post', methods=['POST'])
def add_post():
    title = request.form['title']
    tags = request.form['tags']
    text = request.form['text']

    if not title:
        abort(400, 'You must give your post a title.')
    if not tags:
        abort(400, 'You must give your post at least one tag.')
    if not text:
        abort(400, 'You must give your post a text body.')

    User(session['username']).add_post(title, tags, text)

    return redirect(url_for('index'))
views.py
class User:

    ...

    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 t in set(tags):
            tag = graph.merge_one("Tag", "name", t)
            rel = Relationship(tag, "TAGGED", post)
            graph.create(rel)
models.py

Like A Post

@app.route('/like_post/<post_id>')
def like_post(post_id):
    username = session.get('username')

    if not username:
        abort(400, 'You must be logged in to like a post.')

    User(username).like_post(post_id)

    flash('Liked post.')
    return redirect(request.referrer)
views.py
class User:

    ...

    def like_post(self, post_id):
        user = self.find()
        post = graph.find_one("Post", "id", post_id)
        graph.create_unique(Relationship(user, "LIKED", post))
models.py

Profile Pages

@app.route('/profile/<username>')
def profile(username):
    posts = get_users_recent_posts(username)

    similar = []
    common = []

    viewer_username = session.get('username')

    if viewer_username:
        viewer = User(viewer_username)

        if viewer.username == username:
            similar = viewer.get_similar_users()
        else:
            common = viewer.get_commonality_of_user(username)

    return render_template(
        'profile.html',
        username=username,
        posts=posts,
        similar=similar,
        common=common
    )
views.py

A User's Recent Posts

def get_users_recent_posts(username):
    query = """
    MATCH (user:User)-[:PUBLISHED]->(post:Post),
          (tag:Tag)-[:TAGGED]->(post)
    WHERE user.username = {username}
    RETURN post.id AS id,
           post.date AS date,
           post.timestamp AS timestamp,
           post.title AS title,
           post.text AS text,
           COLLECT(tag.name) AS tags
    ORDER BY timestamp DESC
    LIMIT 5
    """

    return graph.cypher.execute(query, username=username)
models.py

Visiting Your Own Profile

class User:

    ...

    def get_similar_users(self):
        # Find three users who are most similar to the logged-in user
        # based on tags they've both blogged about.
        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, COUNT(DISTINCT tag) AS len
        ORDER BY len DESC LIMIT 3
        RETURN they.username AS similar_user, tags
        """

        return graph.cypher.execute(query, username=self.username)
models.py

Visiting Another User's Profile

class User:

    ...

    def get_commonality_of_user(self, username):
        # Find how many of the logged-in user's posts the other user
        # has liked and which tags they've both blogged about.
        query = """
        MATCH (they:User {username:{they}}),
              (you:User {username:{you}})
        OPTIONAL MATCH (they)-[:LIKED]->(post:Post)<-[:PUBLISHED]-(you)
        OPTIONAL MATCH (they)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag:Tag),
                       (you)-[:PUBLISHED]->(:Post)<-[:TAGGED]-(tag)
        RETURN COUNT(DISTINCT post) AS likes, COLLECT(DISTINCT tag.name) AS tags
        """

        return graph.cypher.execute(query, they=username, you=self.username)[0]
models.py

Questions?

github.com/nicolewhite/neo4j-flask

@_nicolemargaret

nicole@neotechnology.com

Made with Slides.com