Database Schema Design
**community/models.py**
class Thread(BaseModel):
author = ForeignKeyField(User, verbose_name="Author")
headline = CharField(max_length=200, verbose_name="Headline", null=True)
community = ForeignKeyField(CommunityGroup, verbose_name="Community")
reply_count = IntegerField(default=0, verbose_name="Reply Count")
is_featured = BooleanField(default=0, verbose_name="Featured")
is_trending = BooleanField(default=0, verbose_name="Trending")
body = TextField(verbose_name="Content")
@classmethod
def with_author(cls):
return cls.select(cls, User.id, User.display_name).join(User)
class ThreadReply(BaseModel):
commenter = ForeignKeyField(User, verbose_name="Commenter", related_name="comments_authored")
thread = ForeignKeyField(Thread, verbose_name="Thread")
parent = ForeignKeyField('self', null=True, verbose_name="Parent Reply", related_name="child_replies")
target_user = ForeignKeyField(User, verbose_name="Target User", related_name="replies_received", null=True)
message = CharField(max_length=1000, verbose_name="Message")
response_count = IntegerField(default=0, verbose_name="Response Count")
vote_count = IntegerField(default=0, verbose_name="Vote Count")
@classmethod
def with_details(cls):
commenter_alias = User.alias()
target_alias = User.alias()
return cls.select(
cls, Thread,
target_alias.id.alias('target_id'), target_alias.display_name.alias('target_name'),
commenter_alias.id.alias('commenter_id'), commenter_alias.display_name.alias('commenter_name')
).join(
Thread, join_type=JOIN.LEFT_OUTER, on=cls.thread
).switch(cls).join(
commenter_alias, join_type=JOIN.LEFT_OUTER, on=cls.commenter
).switch(cls).join(
target_alias, join_type=JOIN.LEFT_OUTER, on=cls.target_user
)
class ReplyVote(BaseModel):
voter = ForeignKeyField(User, verbose_name="Voter")
reply = ForeignKeyField(ThreadReply, verbose_name="Reply")
Creating New Threads
**community/forms.py** Parameter Validation
class ThreadForm(Form):
headline = StringField("Headline", validators=[DataRequired("Headline required")])
body = StringField("Body", validators=[DataRequired("Content required")])
**community/handlers.py** Thread Creation Handler
class ThreadHandler(RedisHandler):
@authenticated_async
async def post(self, community_id, *args, **kwargs):
response = {}
try:
community = await self.application.objects.get(CommunityGroup, id=int(community_id))
membership = await self.application.objects.get(
CommunityGroupMember,
user=self.current_user,
community=community,
status="active"
)
payload = json.loads(self.request.body.decode("utf8"))
form = ThreadForm.from_json(payload)
if form.validate():
thread = await self.application.objects.create(
Thread,
author=self.current_user,
headline=form.headline.data,
body=form.body.data,
community=community
)
response["thread_id"] = thread.id
else:
self.set_status(400)
for field in form.errors:
response[field] = form.errors[field][0]
except CommunityGroup.DoesNotExist:
self.set_status(404)
except CommunityGroupMember.DoesNotExist:
self.set_status(403)
self.finish(response)
**community/urls.py** Routing
urlpattern = (
url("/community/([0-9]+)/threads/", ThreadHandler),
)
Retrieving Threads
**community/handlers.py** Thread Retrieval Handler
class ThreadHandler(RedisHandler):
@authenticated_async
async def get(self, community_id, *args, **kwargs):
threads = []
try:
community = await self.application.objects.get(CommunityGroup, id=int(community_id))
membership = await self.application.objects.get(
CommunityGroupMember,
user=self.current_user,
community=community,
status="active"
)
query = Thread.with_author()
filter_type = self.get_argument("filter", None)
if filter_type == "trending":
query = query.filter(Thread.is_trending == True)
elif filter_type == "featured":
query = query.filter(Thread.is_featured == True)
thread_list = await self.application.objects.execute(query)
for thread in thread_list:
threads.append({
"author": {
"id": thread.author.id,
"name": thread.author.display_name
},
"id": thread.id,
"headline": thread.headline,
"body": thread.body,
"reply_count": thread.reply_count
})
except CommunityGroupMember.DoesNotExist:
self.set_status(403)
except CommunityGroup.DoesNotExist:
self.set_status(404)
self.finish(json.dumps(threads))
Thread Details
**community/handlers.py** Thread Detail Handler
class ThreadDetailHandler(RedisHandler):
@authenticated_async
async def get(self, thread_id, *args, **kwargs):
response = {}
try:
thread_query = Thread.with_author().where(Thread.id == int(thread_id))
thread_results = await self.application.objects.execute(thread_query)
for thread in thread_results:
response = {
"author": model_to_dict(thread.author),
"headline": thread.headline,
"body": thread.body,
"reply_count": thread.reply_count,
"created": thread.created.strftime("%Y-%m-%d")
}
break
else:
self.set_status(404)
except Exception:
self.set_status(500)
self.finish(response)
**community/urls.py** Routing
urlpattern = (
url("/thread/([0-9]+)/", ThreadDetailHandler),
)
Thread Comments System
**community/forms.py** Comment Validation
class ReplyForm(Form):
message = StringField("Message", validators=[
DataRequired("Message required"),
Length(min=3, message="Message too short")
])
**community/handlers.py** Comment Handler
class ThreadReplyHandler(RedisHandler):
@authenticated_async
async def post(self, thread_id, *args, **kwargs):
response = {}
payload = json.loads(self.request.body.decode("utf8"))
form = ReplyForm.from_json(payload)
if form.validate():
try:
thread = await self.application.objects.get(Thread, id=int(thread_id))
reply = await self.application.objects.create(
ThreadReply,
commenter=self.current_user,
thread=thread,
message=form.message.data
)
thread.reply_count += 1
await self.application.objects.update(thread)
response["reply_id"] = reply.id
response["commenter"] = {
"name": self.current_user.display_name,
"id": self.current_user.id
}
except Thread.DoesNotExist:
self.set_status(404)
else:
self.set_status(400)
for field in form.errors:
response[field] = form.errors[field][0]
self.finish(response)
@authenticated_async
async def get(self, thread_id, *args, **kwargs):
replies = []
try:
thread = await self.application.objects.get(Thread, id=int(thread_id))
reply_query = ThreadReply.with_details().where(
ThreadReply.thread == thread,
ThreadReply.parent.is_null(True)
).order_by(ThreadReply.created.desc())
for reply in await self.application.objects.execute(reply_query):
has_voted = False
try:
await self.application.objects.get(
ReplyVote,
reply=reply,
voter=self.current_user
)
has_voted = True
except ReplyVote.DoesNotExist:
pass
replies.append({
"commenter": model_to_dict(reply.commenter),
"message": reply.message,
"response_count": reply.response_count,
"vote_count": reply.vote_count,
"has_voted": has_voted,
"id": reply.id
})
except Thread.DoesNotExist:
self.set_status(404)
self.finish(json.dumps(replies, default=json_serial))
**community/urls.py** Routing
urlpattern = (
url("/thread/([0-9]+)/replies/", ThreadReplyHandler),
)
Reply to Comments
**community/forms.py** Response Validation
class ResponseForm(Form):
target_user = IntegerField("Target User", validators=[DataRequired("Target user required")])
message = StringField("Message", validators=[
DataRequired("Message required"),
Length(min=3, message="Message too short")
])
**community/handlers.py** Response Handler
class ReplyResponseHandler(RedisHandler):
@authenticated_async
async def post(self, reply_id, *args, **kwargs):
response = {}
payload = json.loads(self.request.body.decode("utf8"))
form = ResponseForm.from_json(payload)
if form.validate():
try:
parent_reply = await self.application.objects.get(ThreadReply, id=int(reply_id))
target = await self.application.objects.get(User, id=form.target_user.data)
thread = await self.application.objects.get(Thread, id=parent_reply.thread.id)
new_response = await self.application.objects.create(
ThreadReply,
thread=thread,
commenter=self.current_user,
parent=parent_reply,
target_user=target,
message=form.message.data
)
parent_reply.response_count += 1
await self.application.objects.update(parent_reply)
response["response_id"] = new_response.id
response["commenter"] = {
"id": self.current_user.id,
"name": self.current_user.display_name
}
# Send notification
await self.application.objects.create(
Notification,
sender=self.current_user,
recipient=target,
notification_type=2,
parent_content=parent_reply.message,
content=form.message.data
)
except ThreadReply.DoesNotExist:
self.set_status(404)
except User.DoesNotExist:
self.set_status(400)
response["target_user"] = "User not found"
else:
self.set_status(400)
for field in form.errors:
response[field] = form.errors[field][0]
self.finish(response)
@authenticated_async
async def get(self, reply_id, *args, **kwargs):
responses = []
query = ThreadReply.with_details().where(
ThreadReply.parent_id == int(reply_id)
)
for response in await self.application.objects.execute(query):
responses.append({
"commenter": model_to_dict(response.commenter),
"message": response.message,
"response_count": response.response_count,
"created": response.created.strftime("%Y-%m-%d"),
"id": response.id
})
self.finish(json.dumps(responses, default=json_serial))
**community/urls.py** Routing
urlpattern = (
url("/reply/([0-9]+)/responses/", ReplyResponseHandler),
)
Comment Voting System
**community/handlers.py** Vote Handler
class ReplyVoteHandler(RedisHandler):
@authenticated_async
async def post(self, reply_id, *args, **kwargs):
response = {}
try:
reply = await self.application.objects.get(ThreadReply, id=int(reply_id))
vote = await self.application.objects.create(
ReplyVote,
voter=self.current_user,
reply=reply
)
reply.vote_count += 1
await self.application.objects.update(reply)
response["vote_id"] = vote.id
except ThreadReply.DoesNotExist:
self.set_status(404)
self.finish(response)
**community/urls.py** Routing
urlpattern = (
url("/reply/([0-9]+)/vote/", ReplyVoteHandler),
)