vj4/handler/discussion.py

Summary

Maintainability
A
3 hrs
Test Coverage
import asyncio
import functools
from bson import objectid

from vj4 import app
from vj4 import error
from vj4.model import builtin
from vj4.model import document
from vj4.model import domain
from vj4.model import oplog
from vj4.model import user
from vj4.model.adaptor import discussion
from vj4.model.adaptor import contest
from vj4.handler import base
from vj4.util import pagination


def node_url(handler, name, node_or_dtuple):
  if isinstance(node_or_dtuple, tuple):
    name += '_document_as_node'
    kwargs = {'doc_type': node_or_dtuple[0], 'doc_id': node_or_dtuple[1]}
  else:
    kwargs = {'doc_id': node_or_dtuple}
  return handler.reverse_url(name, **kwargs)


@app.route('/discuss', 'discussion_main')
class DiscussionMainHandler(base.Handler):
  DISCUSSIONS_PER_PAGE = 15

  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.get_argument
  @base.sanitize
  async def get(self, *, page: int=1):
    # TODO(iceboy): continuation based pagination.
    nodes, (ddocs, dpcount, _) = await asyncio.gather(
        discussion.get_nodes(self.domain_id),
        # TODO(twd2): exclude problem/contest discussions?
        pagination.paginate(discussion.get_multi(self.domain_id), page, self.DISCUSSIONS_PER_PAGE))
    udict, dudict, vndict = await asyncio.gather(
        user.get_dict(ddoc['owner_uid'] for ddoc in ddocs),
        domain.get_dict_user_by_uid(domain_id=self.domain_id, uids=(ddoc['owner_uid'] for ddoc in ddocs)),
        discussion.get_dict_vnodes(self.domain_id, map(discussion.node_id, ddocs)))
    self.render('discussion_main_or_node.html', discussion_nodes=nodes, ddocs=ddocs,
                udict=udict, dudict=dudict, vndict=vndict, page=page, dpcount=dpcount)


@app.route('/discuss/{doc_type:-?\d+}/{doc_id}', 'discussion_node_document_as_node')
@app.route('/discuss/{doc_id:\w{1,23}|\w{25,}|[^/]*[^/\w][^/]*}', 'discussion_node')
class DiscussionNodeHandler(contest.ContestStatusMixin, base.Handler):
  DISCUSSIONS_PER_PAGE = 15

  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.get_argument
  @base.route_argument
  @base.sanitize
  async def get(self, *, doc_type: int=None, doc_id: str, page: int=1):
    if doc_type is None:
      node_or_dtuple = doc_id
    else:
      node_or_dtuple = (doc_type, document.convert_doc_id(doc_id))
    nodes, vnode = await discussion.get_nodes_and_vnode(self.domain_id, node_or_dtuple)
    if not vnode:
      raise error.DiscussionNodeNotFoundError(self.domain_id, node_or_dtuple)
    if vnode['doc_type'] == document.TYPE_PROBLEM and vnode.get('hidden', False):
      self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN)
    # TODO(twd2): do more visibility check eg. contest
    # TODO(iceboy): continuation based pagination.
    ddocs, dpcount, _ = await pagination.paginate(
        discussion.get_multi(self.domain_id,
                             parent_doc_type=vnode['doc_type'],
                             parent_doc_id=vnode['doc_id']),
        page, self.DISCUSSIONS_PER_PAGE)
    uids = set(ddoc['owner_uid'] for ddoc in ddocs)
    if 'owner_uid' in vnode:
      uids.add(vnode['owner_uid'])
    udict, dudict = await asyncio.gather(user.get_dict(uids),
                                         domain.get_dict_user_by_uid(self.domain_id, uids))
    vndict = {node_or_dtuple: vnode}
    vncontext = {} # TODO(twd2): eg. psdoc, tsdoc, ...
    path_components = self.build_path(
        (self.translate('discussion_main'), self.reverse_url('discussion_main')),
        (vnode['title'], None))
    self.render('discussion_main_or_node.html', discussion_nodes=nodes, vnode=vnode, ddocs=ddocs,
                udict=udict, dudict=dudict, vndict=vndict, page=page, dpcount=dpcount, **vncontext,
                path_components=path_components)


@app.route('/discuss/{doc_type:-?\d+}/{doc_id}/create', 'discussion_create_document_as_node')
@app.route('/discuss/{doc_id}/create', 'discussion_create')
class DiscussionCreateHandler(base.Handler):
  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.require_perm(builtin.PERM_CREATE_DISCUSSION)
  @base.route_argument
  @base.sanitize
  async def get(self, *, doc_type: int=None, doc_id: str):
    if doc_type is None:
      node_or_dtuple = doc_id
    else:
      node_or_dtuple = (doc_type, document.convert_doc_id(doc_id))
    nodes, vnode = await discussion.get_nodes_and_vnode(self.domain_id, node_or_dtuple)
    if not vnode:
      raise error.DiscussionNodeNotFoundError(self.domain_id, node_or_dtuple)
    if vnode['doc_type'] == document.TYPE_PROBLEM and vnode.get('hidden', False):
      self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN)
    # TODO(twd2): do more visibility check eg. contest
    path_components = self.build_path(
        (self.translate('discussion_main'), self.reverse_url('discussion_main')),
        (vnode['title'], node_url(self, 'discussion_node', node_or_dtuple)),
        (self.translate('discussion_create'), None))
    self.render('discussion_create.html', vnode=vnode, path_components=path_components)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.require_perm(builtin.PERM_CREATE_DISCUSSION)
  @base.route_argument
  @base.post_argument
  @base.require_csrf_token
  @base.sanitize
  @base.limit_rate('add_discussion', 3600, 30)
  async def post(self, *, doc_type: int=None, doc_id: str, title: str, content: str,
                 highlight: str=None):
    if doc_type is None:
      node_or_dtuple = doc_id
    else:
      node_or_dtuple = (doc_type, document.convert_doc_id(doc_id))
    vnode = await discussion.get_vnode(self.domain_id, node_or_dtuple)
    if not vnode:
      raise error.DiscussionNodeNotFoundError(self.domain_id, node_or_dtuple)
    if vnode['doc_type'] == document.TYPE_PROBLEM and vnode.get('hidden', False):
      self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN)
    # TODO(twd2): do more visibility check eg. contest
    flags = {}
    if highlight:
      self.check_perm(builtin.PERM_HIGHLIGHT_DISCUSSION)
      flags['highlight'] = True
    did = await discussion.add(self.domain_id, node_or_dtuple, self.user['_id'], title, content,
                               self.remote_ip, **flags)
    self.json_or_redirect(self.reverse_url('discussion_detail', did=did), did=did)


@app.route('/discuss/{did:\w{24}}', 'discussion_detail')
class DiscussionDetailHandler(base.OperationHandler):
  REPLIES_PER_PAGE = 50

  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.get_argument
  @base.route_argument
  @base.sanitize
  async def get(self, *, did: document.convert_doc_id, page: int=1):
    ddoc = await discussion.inc_views(self.domain_id, did)
    if self.has_priv(builtin.PRIV_USER_PROFILE):
      dsdoc = await discussion.get_status(self.domain_id, ddoc['doc_id'], self.user['_id'])
    else:
      dsdoc = None
    vnode, (drdocs, pcount, drcount) = await asyncio.gather(
        discussion.get_vnode(self.domain_id, discussion.node_id(ddoc)),
        pagination.paginate(discussion.get_multi_reply(self.domain_id, ddoc['doc_id']),
                            page, self.REPLIES_PER_PAGE))
    if not vnode:
      vnode = builtin.VNODE_MISSING
    elif vnode['doc_type'] == document.TYPE_PROBLEM and vnode.get('hidden', False):
      self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN)
    # TODO(twd2): do more visibility check eg. contest
    uids = {ddoc['owner_uid']}
    uids.update(drdoc['owner_uid'] for drdoc in drdocs)
    for drdoc in drdocs:
      if 'reply' in drdoc:
        uids.update(drrdoc['owner_uid'] for drrdoc in drdoc['reply'])
    if 'owner_uid' in vnode:
      uids.add(vnode['owner_uid'])
    udict, dudict = await asyncio.gather(user.get_dict(uids),
                                         domain.get_dict_user_by_uid(self.domain_id, uids))
    path_components = self.build_path(
        (self.translate('discussion_main'), self.reverse_url('discussion_main')),
        (vnode['title'], node_url(self, 'discussion_node', discussion.node_id(ddoc))),
        (ddoc['title'], None))
    self.render('discussion_detail.html', page_title=ddoc['title'], path_components=path_components,
                ddoc=ddoc, dsdoc=dsdoc, drdocs=drdocs, page=page, pcount=pcount, drcount=drcount,
                vnode=vnode, udict=udict, dudict=dudict)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.require_perm(builtin.PERM_REPLY_DISCUSSION)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  @base.limit_rate('add_discussion', 3600, 30)
  async def post_reply(self, *, did: document.convert_doc_id, content: str):
    ddoc = await discussion.get(self.domain_id, did)
    await discussion.add_reply(self.domain_id, ddoc['doc_id'], self.user['_id'], content,
                               self.remote_ip)
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.require_perm(builtin.PERM_REPLY_DISCUSSION)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  @base.limit_rate('add_discussion', 3600, 30)
  async def post_tail_reply(self, *,
                            did: document.convert_doc_id,
                            drid: document.convert_doc_id,
                            content: str):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc = await discussion.get_reply(self.domain_id, drid, ddoc['doc_id'])
    await discussion.add_tail_reply(self.domain_id, drdoc['doc_id'], self.user['_id'], content,
                                    self.remote_ip)
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_edit_reply(self, *, did: document.convert_doc_id,
                            drid: document.convert_doc_id, content: str):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc = await discussion.get_reply(self.domain_id, drid, ddoc['doc_id'])
    if (not self.own(ddoc, builtin.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION)
        and not self.own(drdoc, builtin.PERM_EDIT_DISCUSSION_REPLY_SELF)):
      self.check_perm(builtin.PERM_EDIT_DISCUSSION_REPLY)
    await discussion.edit_reply(self.domain_id, drdoc['doc_id'],
                                content=content)
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_delete_reply(self, *, did: document.convert_doc_id,
                              drid: document.convert_doc_id):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc = await discussion.get_reply(self.domain_id, drid, ddoc['doc_id'])
    if (not self.own(ddoc, builtin.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION)
        and not self.own(drdoc, builtin.PERM_DELETE_DISCUSSION_REPLY_SELF)):
      self.check_perm(builtin.PERM_DELETE_DISCUSSION_REPLY)
    await oplog.add(self.user['_id'], oplog.TYPE_DELETE_DOCUMENT, doc=drdoc)
    await discussion.delete_reply(self.domain_id, drdoc['doc_id'])
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_edit_tail_reply(self, *, did: document.convert_doc_id,
                                 drid: document.convert_doc_id, drrid: document.convert_doc_id,
                                 content: str):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc, drrdoc = await discussion.get_tail_reply(self.domain_id, drid, drrid)
    if not drdoc or drdoc['parent_doc_id'] != ddoc['doc_id']:
      raise error.DocumentNotFoundError(domain_id, document.TYPE_DISCUSSION_REPLY, drid)
    if (not self.own(ddoc, builtin.PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION)
        and not self.own(drrdoc, builtin.PERM_EDIT_DISCUSSION_REPLY_SELF)):
      self.check_perm(builtin.PERM_EDIT_DISCUSSION_REPLY)
    await discussion.edit_tail_reply(self.domain_id, drid, drrid, content)
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_delete_tail_reply(self, *, did: document.convert_doc_id,
                                   drid: document.convert_doc_id, drrid: objectid.ObjectId):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc, drrdoc = await discussion.get_tail_reply(self.domain_id, drid, drrid)
    if not drdoc or drdoc['parent_doc_id'] != ddoc['doc_id']:
      raise error.DocumentNotFoundError(domain_id, document.TYPE_DISCUSSION_REPLY, drid)
    if (not self.own(ddoc, builtin.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION)
        and not self.own(drrdoc, builtin.PERM_DELETE_DISCUSSION_REPLY_SELF)):
      self.check_perm(builtin.PERM_DELETE_DISCUSSION_REPLY)
    await oplog.add(self.user['_id'], oplog.TYPE_DELETE_SUB_DOCUMENT, sub_doc=drrdoc,
                    doc_type=drdoc['doc_type'], doc_id=drdoc['doc_id'])
    await discussion.delete_tail_reply(self.domain_id, drid, drrid)
    self.json_or_redirect(self.url)

  @base.require_priv(builtin.PRIV_USER_PROFILE)
  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def star_unstar(self, *, did: document.convert_doc_id, star: bool):
    ddoc = await discussion.get(self.domain_id, did)
    ddoc = await discussion.set_star(self.domain_id, ddoc['doc_id'], self.user['_id'], star)
    self.json_or_redirect(self.url, star=ddoc['star'])

  post_star = functools.partialmethod(star_unstar, star=True)
  post_unstar = functools.partialmethod(star_unstar, star=False)


@app.route('/discuss/{did:\w{24}}/raw', 'discussion_detail_raw')
class DiscussionDetailRawHandler(base.Handler):
  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.route_argument
  @base.sanitize
  async def get(self, *, did: document.convert_doc_id):
    ddoc = await discussion.get(self.domain_id, did)
    self.response.content_type = 'text/markdown'
    self.response.text = ddoc['content']


@app.route('/discuss/{did:\w{24}}/{drid:\w{24}}/raw', 'discussion_reply_raw')
class DiscussionReplyRawHandler(base.Handler):
  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.route_argument
  @base.sanitize
  async def get(self, *, did: document.convert_doc_id, drid: document.convert_doc_id):
    ddoc = await discussion.get(self.domain_id, did)
    drdoc = await discussion.get_reply(self.domain_id, drid, ddoc['doc_id'])
    self.response.content_type = 'text/markdown'
    self.response.text = drdoc['content']


@app.route('/discuss/{did:\w{24}}/{drid:\w{24}}/{drrid:\w{24}}/raw', 'discussion_tail_reply_raw')
class DiscussionTailReplyRawHandler(base.Handler):
  @base.require_perm(builtin.PERM_VIEW_DISCUSSION)
  @base.route_argument
  @base.sanitize
  async def get(self, *, did: document.convert_doc_id, drid: document.convert_doc_id,
                drrid: objectid.ObjectId):
    await discussion.get(self.domain_id, did)
    _, drrdoc = await discussion.get_tail_reply(self.domain_id, drid, drrid)
    self.response.content_type = 'text/markdown'
    self.response.text = drrdoc['content']


@app.route('/discuss/{did:\w{24}}/edit', 'discussion_edit')
class DiscussionEditHandler(base.OperationHandler):
  DEFAULT_OPERATION = 'update'

  @base.route_argument
  @base.sanitize
  async def get(self, *, did: document.convert_doc_id):
    ddoc = await discussion.get(self.domain_id, did)
    if not ddoc:
      raise error.DiscussionNotFoundError(self.domain_id, did)
    if not self.own(ddoc, builtin.PERM_EDIT_DISCUSSION_SELF):
      self.check_perm(builtin.PERM_EDIT_DISCUSSION)
    self.render('discussion_edit.html', ddoc=ddoc)

  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_update(self, *, did: document.convert_doc_id, title: str, content: str,
                        highlight: str=None):
    ddoc = await discussion.get(self.domain_id, did)
    if not ddoc:
      raise error.DiscussionNotFoundError(self.domain_id, did)
    if not self.own(ddoc, builtin.PERM_EDIT_DISCUSSION_SELF):
      self.check_perm(builtin.PERM_EDIT_DISCUSSION)
    flags = {}
    if highlight:
      if not ddoc.get('highlight'):
        self.check_perm(builtin.PERM_HIGHLIGHT_DISCUSSION)
      flags['highlight'] = True
    else:
      flags['highlight'] = False
    ddoc = await discussion.edit(self.domain_id, did, title=title, content=content, **flags)
    self.json_or_redirect(self.reverse_url('discussion_detail', did=ddoc['doc_id']))

  @base.route_argument
  @base.require_csrf_token
  @base.sanitize
  async def post_delete(self, *, did: document.convert_doc_id):
    ddoc = await discussion.get(self.domain_id, did)
    if not ddoc:
      raise error.DiscussionNotFoundError(self.domain_id, did)
    if not self.own(ddoc, builtin.PERM_DELETE_DISCUSSION_SELF):
      self.check_perm(builtin.PERM_DELETE_DISCUSSION)
    await oplog.add(self.user['_id'], oplog.TYPE_DELETE_DOCUMENT, doc=ddoc)
    await discussion.delete(self.domain_id, did)
    self.json_or_redirect(node_url(self, 'discussion_node', discussion.node_id(ddoc)))