Skip to content
This repository was archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Comment infrastructure and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
buraksezer committed Mar 3, 2014
1 parent fd4b009 commit 0de2200
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 12 deletions.
142 changes: 142 additions & 0 deletions chapstream/api/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import json
import logging
import calendar

import tornado.web

from chapstream import config
from chapstream.api import decorators
from chapstream.api import CsRequestHandler, process_response
from chapstream.backend.db.models.post import Post
from chapstream.backend.db.models.comment import Comment
from chapstream.backend.db.models.user import UserRelation
from chapstream.backend.tasks import push_comment

logger = logging.getLogger(__name__)


class CommentHandler(CsRequestHandler):
@tornado.web.authenticated
@decorators.api_response
def post(self, post_id):
post = self.session.query(Post).filter_by(
id=post_id).first()
if not post:
return process_response(status=config.API_FAIL,
message="Post:%s could not be found."
% post_id)

logger.info('Creating a new comment by %s',
self.current_user.name)

# Create a database record for this comment on
# PostgreSQL database
data = json.loads(self.request.body)
body = data["body"].decode('UTF-8')
new_comment = Comment(
body=body,
post=post,
user=self.current_user
)
self.session.add(new_comment)
self.session.commit()

created_at = calendar.timegm(new_comment.created_at.utctimetuple())
comment = {
'type': config.REALTIME_COMMENT,
'post_id': post.id,
'id': new_comment.id,
'body': body,
'created_at': created_at,
'user_id': self.current_user.id,
'name': self.current_user.name,
'fullname': self.current_user.fullname
}

# Comment summary is a list on Redis that stores
# first two comments and last one to render the user timeline
# as fast as possible.
comment_summary = "cs::" + str(post.id)
length = self.redis_conn.llen(comment_summary)
comment_json = json.dumps(comment)
if length in (0, 1):
self.redis_conn.rpush(comment_summary, comment_json)
else:
if length >= 3:
while length > 2:
if self.redis_conn.rpop(comment_summary):
length -= 1
self.redis_conn.rpush(comment_summary, comment_json)

# TODO: make a helper func. for this
if post.user_id != self.current_user.id:
userintr_hash = "userintr:" + str(self.current_user.id)
val = config.REALTIME_COMMENT+str(created_at)
self.redis_conn.hset(userintr_hash, str(post.id), val)
#rel = self.session.query(UserRelation).filter_by(
# user_id=self.current_user.id, chap_id=post.id).first()
#if not rel:
# intr_hash = "intr:" + str(post.id)
# self.redis_conn.hset(intr_hash,
# str(self.current_user.id),
# config.REALTIME_COMMENT)

# Send a task for realtime interaction
push_comment(comment)
data = {'comment': comment}
return process_response(data=data)

@tornado.web.authenticated
@decorators.api_response
def get(self, post_id):
post = self.session.query(Post).filter_by(
id=post_id).first()
if not post:
return process_response(status=config.API_FAIL,
message="Post:%s could not be found."
% post_id)
result = []
for comment in post.comments:
created_at = calendar.timegm(comment.created_at.utctimetuple())
comment_dict = {
'id': comment.id,
'body': comment.body,
'created_at': created_at,
'user_id': comment.user.id,
'name': comment.user.name,
'fullname': comment.user.fullname
}
result.append(comment_dict)

return process_response(data={"comments": result})

@tornado.web.authenticated
@decorators.api_response
def delete(self, comment_id):
comment = self.session.query(Comment).filter_by(
id=comment_id).first()
if not comment:
return process_response(status=config.API_FAIL,
message="Comment:%s could not be found."
% comment_id)

post_id = comment.post.id
comment_summary = "cs::" + str(post_id)
items = self.redis_conn.lrange(comment_summary, 0, 3)
for item in items:
comment_json = json.loads(item)
if comment_json["id"] == comment.id:
self.redis_conn.lrem(comment_summary, item)

self.session.delete(comment)
self.session.commit()

other_comments = self.session.query(Comment).filter_by(
user_id=self.current_user.id,
post_id=post_id
).count()

if not other_comments:
userintr_hash = "userintr:" + str(self.current_user.id)
self.redis_conn.hdel(userintr_hash, str(post_id))

2 changes: 1 addition & 1 deletion chapstream/api/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,6 @@ def delete(self, post_id):
self.redis_conn.decr(like_count)
else:
# TODO: write a suitable warning message
return process_response(status=config.API_WARN,
return process_response(status=config.API_WARNING,
message="Post:%s a warning message"
% post_id)
1 change: 1 addition & 0 deletions chapstream/backend/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from chapstream.backend.db.models.user import *
from chapstream.backend.db.models.notification import *
from chapstream.backend.db.models.group import *
from chapstream.backend.db.models.comment import *
26 changes: 26 additions & 0 deletions chapstream/backend/db/models/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from sqlalchemy import func
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy import Column, Integer, String, \
DateTime, UnicodeText, ForeignKey, \
Sequence, BigInteger

from chapstream.backend.db.models.user import User
from chapstream.backend.db.models.post import Post
from chapstream.backend.db.orm import Base


class Comment(Base):
__tablename__ = 'comments'

id = Column(BigInteger, Sequence(
'seq_comment_id', start=1, increment=1), primary_key=True)
body = Column(UnicodeText, nullable=True)
likes = Column(ARRAY(String), nullable=True)
created_at = Column(DateTime, default=func.current_timestamp())
updated_at = Column(DateTime, onupdate=func.current_timestamp())
user_id = Column(Integer, ForeignKey(User.id, ondelete='CASCADE'))
post_id = Column(Integer, ForeignKey(Post.id, ondelete='CASCADE'))

def __repr__(self):
return "<Comment(id: '%s', user_id: '%s', post_id: '%s')>" % \
(self.id, self.user_id, self.post_id)
7 changes: 6 additions & 1 deletion chapstream/backend/db/models/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqlalchemy import Column, Integer, String, \
DateTime, Boolean, UnicodeText, ForeignKey, \
Sequence, BigInteger
from sqlalchemy.orm import relationship

from chapstream.backend.db.models.user import User
from chapstream.backend.db.orm import Base
Expand All @@ -19,7 +20,11 @@ class Post(Base):
created_at = Column(DateTime, default=func.current_timestamp())
updated_at = Column(DateTime, onupdate=func.current_timestamp())
user_id = Column(Integer, ForeignKey(User.id, ondelete='CASCADE'))

comments = relationship('Comment',
backref='post',
cascade="all, delete",
lazy='dynamic',
passive_deletes=True)
def __repr__(self):
return "<Post(id: '%s', user_id: '%s')>" % \
(self.id, self.user_id)
5 changes: 5 additions & 0 deletions chapstream/backend/db/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class User(Base):
backref='user',
lazy='dynamic',
passive_deletes=True)
comments = relationship('Comment',
backref='user',
cascade="all, delete",
lazy='dynamic',
passive_deletes=True)

def __repr__(self):
return "<User(id:'%s', name:'%s', email:'%s')>" % \
Expand Down
34 changes: 28 additions & 6 deletions chapstream/backend/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from chapstream.backend.db.models.user import User, UserRelation
from chapstream.backend.db.models.group import Group
from chapstream.backend.db.models.notification import Notification
from chapstream.backend.db.models.comment import Comment


logging.basicConfig(level=logging.INFO)
Expand Down Expand Up @@ -167,11 +168,6 @@ def remove_posts(timeline, blocked_user):

@kuyruk.task
def push_notification(user_ids, message):
redis_conn = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT
)

for user_id in user_ids:
user = User.get(user_id)
if not user:
Expand All @@ -189,4 +185,30 @@ def push_notification(user_ids, message):
redis_conn.publish(channel, message)
except redis.ConnectionError as err:
logger.error('Connection error: %s', err)
# TODO: Handle failed tasks
# TODO: Handle failed tasks


@kuyruk.task
def push_comment(comment):
def push(user_id, rule=None):
try:
channel = str(user_id) + '_channel'
comment["rule"] = rule
comment_json = json.dumps(comment)
logger.info('Sending a comment to %s', channel)
redis_conn.publish(channel, comment_json)
except redis.ConnectionError as err:
logger.error('Connection error: %s', err)
# TODO: Handle failed tasks

intr_users = Comment.query.with_entities(Comment.user_id).\
filter_by(post_id=comment["post_id"]).all()
intr_users = set(intr_users)
for (user_id,) in intr_users:
push(user_id, rule=config.REALTIME_COMMENT)

relations = UserRelation.query.with_entities(UserRelation.user_id).\
filter_by(chap_id=comment['user_id'],
is_banned=False).all()
for (user_id,) in relations:
push(user_id)
8 changes: 7 additions & 1 deletion chapstream/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@
API_OK = "OK"
API_ERROR = "ERROR"
API_FAIL = "FAIL"
API_WARNING = "WARNING"

# Redis Configuration
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_PASSWORD = None
REDIS_PASSWORD = None

# Realtime content types
REALTIME_COMMENT = "COMMENT"
REALTIME_POST = "POST"
REALTIME_LIKE = "LIKE"
3 changes: 3 additions & 0 deletions chapstream/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from chapstream.api.timeline import TimelineLoader
from chapstream.api.timeline import PostHandler
from chapstream.api.timeline import LikeHandler
from chapstream.api.comment import CommentHandler

# Group related handlers
from chapstream.api.group import GroupSubscriptionHandler
Expand All @@ -32,6 +33,8 @@
]

API_URLS = [
(r"/api/comment/(?P<post_id>[^\/]+)", CommentHandler),
(r"/api/comment-delete/(?P<comment_id>[^\/]+)", CommentHandler),
(r"/api/like/(?P<post_id>[^\/]+)", LikeHandler),
(r"/api/group/subscribe/(?P<group_id>[^\/]+)", GroupSubscriptionHandler),
(r"/api/group", GroupHandler),
Expand Down
57 changes: 56 additions & 1 deletion data/sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'SQL_ASCII';
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
Expand All @@ -29,6 +29,23 @@ SET default_tablespace = '';

SET default_with_oids = false;

--
-- Name: comments; Type: TABLE; Schema: public; Owner: csdbuser; Tablespace:
--

CREATE TABLE comments (
id bigint NOT NULL,
body text,
likes character varying[],
created_at timestamp without time zone,
updated_at timestamp without time zone,
user_id integer,
post_id integer
);


ALTER TABLE public.comments OWNER TO csdbuser;

--
-- Name: group_posts; Type: TABLE; Schema: public; Owner: csdbuser; Tablespace:
--
Expand Down Expand Up @@ -89,6 +106,20 @@ CREATE TABLE posts (

ALTER TABLE public.posts OWNER TO csdbuser;

--
-- Name: seq_comment_id; Type: SEQUENCE; Schema: public; Owner: csdbuser
--

CREATE SEQUENCE seq_comment_id
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


ALTER TABLE public.seq_comment_id OWNER TO csdbuser;

--
-- Name: seq_group_id; Type: SEQUENCE; Schema: public; Owner: csdbuser
--
Expand Down Expand Up @@ -194,6 +225,14 @@ CREATE TABLE users (

ALTER TABLE public.users OWNER TO csdbuser;

--
-- Name: comments_pkey; Type: CONSTRAINT; Schema: public; Owner: csdbuser; Tablespace:
--

ALTER TABLE ONLY comments
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);


--
-- Name: groups_pkey; Type: CONSTRAINT; Schema: public; Owner: csdbuser; Tablespace:
--
Expand Down Expand Up @@ -234,6 +273,22 @@ ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);


--
-- Name: comments_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: csdbuser
--

ALTER TABLE ONLY comments
ADD CONSTRAINT comments_post_id_fkey FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE;


--
-- Name: comments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: csdbuser
--

ALTER TABLE ONLY comments
ADD CONSTRAINT comments_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;


--
-- Name: group_posts_gp_group_fkey; Type: FK CONSTRAINT; Schema: public; Owner: csdbuser
--
Expand Down
Loading

0 comments on commit 0de2200

Please sign in to comment.