Skip to content

Commit

Permalink
Add support for Comment ORM class to QueryBuilder
Browse files Browse the repository at this point in the history
This allows to make the methods of `Node` backend independent and
as a result makes the implementation a lot simpler. The command line
interface is also adapted and simplified.
  • Loading branch information
sphuber committed Nov 22, 2018
1 parent 84a5756 commit f5406dd
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 370 deletions.
23 changes: 5 additions & 18 deletions aiida/backends/tests/cmdline/commands/test_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from __future__ import print_function
from __future__ import absolute_import

from six.moves import range
from click.testing import CliRunner

from aiida.backends.testbase import AiidaTestCase
Expand Down Expand Up @@ -44,33 +43,21 @@ def test_comment_show(self):

def test_comment_add(self):
"""Test adding a comment."""
options = ['-c{}'.format(COMMENT), str(self.node.pk)]
options = ['-N', str(self.node.pk), '--', '{}'.format(COMMENT)]
result = self.cli_runner.invoke(cmd_comment.add, options, catch_exceptions=False)
self.assertEqual(result.exit_code, 0)

comment = self.node.get_comments()
self.assertEquals(len(comment), 1)
self.assertEqual(comment[0]['content'], COMMENT)
self.assertEqual(comment[0].content, COMMENT)

def test_comment_remove(self):
"""Test removing a comment."""
pk = self.node.add_comment(COMMENT)
comment = self.node.add_comment(COMMENT)

self.assertEquals(len(self.node.get_comments()), 1)

options = [str(self.node.pk), str(pk), '--force']
options = [str(comment.pk), '--force']
result = self.cli_runner.invoke(cmd_comment.remove, options, catch_exceptions=False)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exit_code, 0, result.output)
self.assertEquals(len(self.node.get_comments()), 0)

def test_comment_remove_all(self):
"""Test removing all comments from a self.node."""
for _ in range(10):
self.node.add_comment(COMMENT)

self.assertEqual(len(self.node.get_comments()), 10)

options = [str(self.node.pk), '--all', '--force']
result = self.cli_runner.invoke(cmd_comment.remove, options, catch_exceptions=False)
self.assertEqual(result.exit_code, 0)
self.assertEqual(len(self.node.get_comments()), 0)
14 changes: 8 additions & 6 deletions aiida/backends/tests/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,31 +1207,33 @@ def test_comments(self):
# of directly loading datetime.datetime.now(), or you can get a
# "can't compare offset-naive and offset-aware datetimes" error
from aiida.utils import timezone
from aiida.orm.backends import construct_backend
import time
from time import sleep

user = User.objects(self.backend).get_default()

a = Node()
with self.assertRaises(ModificationNotAllowed):
a.add_comment('text', user=user)

a.store()
self.assertEquals(a.get_comments(), [])

before = timezone.now()
time.sleep(1) # I wait 1 second because MySql time precision is 1 sec
sleep(1) # I wait 1 second because MySql time precision is 1 sec
a.add_comment('text', user=user)
a.add_comment('text2', user=user)
time.sleep(1)
sleep(1)
after = timezone.now()

comments = a.get_comments()

times = [i['mtime'] for i in comments]
times = [i.ctime for i in comments]

for time in times:
self.assertTrue(time > before)
self.assertTrue(time < after)

self.assertEquals([(i['user__email'], i['content']) for i in comments], [
self.assertEquals([(i.user.email, i.content) for i in comments], [
(self.user_email, 'text'),
(self.user_email, 'text2'),
])
Expand Down
37 changes: 37 additions & 0 deletions aiida/backends/tests/orm/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def setUp(self):
self.content = 'Sometimes when I am freestyling, I lose confidence'
self.comment = Comment(self.node, self.user, self.content).store()

def tearDown(self):
super(TestComment, self).tearDown()
comments = Comment.objects.all()
for comment in comments:
Comment.objects.delete(comment.id)

def test_comment_content(self):
"""Test getting and setting content of a Comment."""
content = 'Be more constructive with your feedback'
Expand Down Expand Up @@ -63,3 +69,34 @@ def test_comment_collection_delete(self):

with self.assertRaises(exceptions.NotExistent):
Comment.objects.get(comment=comment_pk)

def test_comment_querybuilder(self):
"""Test querying for comments by joining on nodes in the QueryBuilder."""
node_one = orm.Node().store()
comment_one = Comment(node_one, self.user, 'comment_one').store()

node_two = orm.Node().store()
comment_three = Comment(node_two, self.user, 'comment_three').store()
comment_four = Comment(node_two, self.user, 'comment_four').store()

# Retrieve a node by joining on a specific comment
nodes = orm.QueryBuilder().append(
Comment, tag='comment', filters={
'id': comment_one.id
}).append(
orm.Node, with_comment='comment', project=['uuid']).all()

self.assertEqual(len(nodes), 1)
for node in nodes:
self.assertIn(str(node[0]), [node_one.uuid])

# Retrieve a comment by joining on a specific node
comments = orm.QueryBuilder().append(
orm.Node, tag='node', filters={
'id': node_two.id
}).append(
Comment, with_node='node', project=['uuid']).all()

self.assertEqual(len(comments), 2)
for comment in comments:
self.assertIn(str(comment[0]), [comment_three.uuid, comment_four.uuid])
172 changes: 62 additions & 110 deletions aiida/cmdline/commands/cmd_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,158 +7,110 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
# pylint: disable=superfluous-parens
"""
This allows to manage comments from command line.
"""
"""`verdi comment` command."""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

import click

from aiida.cmdline.commands.cmd_verdi import verdi
from aiida.cmdline.params import arguments, options
from aiida.cmdline.utils import decorators, echo, multi_line_input
from aiida.common import exceptions


@verdi.group('comment')
def verdi_comment():
"""Inspect, create and manage comments."""
"""Inspect, create and manage node comments."""
pass


@verdi_comment.command()
@click.option('--comment', '-c', type=str, required=False)
@arguments.NODES(required=True)
@options.NODES()
@click.argument('content', type=click.STRING, required=False)
@decorators.with_dbenv()
def add(comment, nodes):
"""
Add comment to one or more nodes in the database
"""
from aiida import orm
def add(nodes, content):
"""Add a comment to one or multiple nodes."""
if not content:
content = multi_line_input.edit_comment()

for node in nodes:
node.add_comment(content)

user = orm.User.objects.get_default()
echo.echo_success('comment added to {} nodes'.format(len(nodes)))

if not comment:
comment = multi_line_input.edit_comment()

for node in nodes:
node.add_comment(comment, user)
@verdi_comment.command()
@click.argument('comment_id', type=int, metavar='COMMENT_ID')
@click.argument('content', type=click.STRING, required=False)
@decorators.with_dbenv()
def update(comment_id, content):
"""Update a comment."""
from aiida.orm.comments import Comment

try:
comment = Comment.objects.get(comment_id)
except (exceptions.NotExistent, exceptions.MultipleObjectsError):
echo.echo_critical('comment<{}> not found'.format(comment_id))

echo.echo_info("Comment added to node(s) '{}'".format(", ".join([str(node.pk) for node in nodes])))
if content is None:
content = multi_line_input.edit_comment(comment.content)

comment.set_content(content)

echo.echo_success('comment<{}> updated'.format(comment_id))


@verdi_comment.command()
@options.USER()
@arguments.NODES()
@decorators.with_dbenv()
def show(user, nodes):
"""
Show the comments of (a) node(s) in the database
"""
"""Show the comments for one or multiple nodes."""
for node in nodes:

all_comments = node.get_comments()

if user is not None:
to_print = [i for i in all_comments if i['user__email'] == user.email]
if not to_print:
valid_users = ", ".join(set(["'" + i['user__email'] + "'" for i in all_comments]))
echo.echo_info("Nothing found for user '{}'.\n"
"Valid users found for Node {} are: {}.".format(user, node.pk, valid_users))
comments = [comment for comment in all_comments if comment.user.email == user.email]

if not comments:
valid_users = ', '.join(set([comment.user.email for comment in all_comments]))
echo.echo_warning('no comments found for user {}'.format(user))
echo.echo_info('valid users found for Node<{}>: {}'.format(node.pk, valid_users))

else:
to_print = all_comments
comments = all_comments

for i in to_print:
for comment in comments:
comment_msg = [
"***********************************************************", "Comment of '{}' on {}".format(
i['user__email'], i['ctime'].strftime("%Y-%m-%d %H:%M")), "PK {} ID {}. Last modified on {}".format(
node.pk, i['pk'], i['mtime'].strftime("%Y-%m-%d %H:%M")), "", "{}".format(i['content']), ""
'***********************************************************',
'Comment<{}> for Node<{}> by {}'.format(comment.id, node.pk, comment.user.email),
'Created on {}'.format(comment.ctime.strftime('%Y-%m-%d %H:%M')),
'Last modified on {}'.format(comment.mtime.strftime('%Y-%m-%d %H:%M')),
'\n{}\n'.format(comment.content),
]
echo.echo_info("\n".join(comment_msg))
echo.echo('\n'.join(comment_msg))

# If there is nothing to print, print a message
if not to_print:
echo.echo_info("No comments found.")
if not comments:
echo.echo_info('no comments found')


@verdi_comment.command()
@click.option(
'--all',
'-a',
'remove_all',
default=False,
is_flag=True,
help='If used, deletes all the comments of the active user attached to the node')
@options.FORCE()
@arguments.NODE()
@click.argument('comment_id', type=int, required=False, metavar='COMMENT_ID')
@click.argument('comment', type=int, required=False, metavar='COMMENT_ID')
@decorators.with_dbenv()
def remove(remove_all, force, node, comment_id):
"""
Remove comment(s) of a node. The user can only remove their own comments.
pk = The pk (an integer) of the node
id = #ID of the comment to be removed from node #PK
"""
# Note: in fact, the user can still manually delete any comment
from aiida import orm

user = orm.User.objects.get_default()

if comment_id is None and not remove_all:
echo.echo_error("One argument between -a and ID must be provided")
return 101

if comment_id is not None and remove_all:
echo.echo_error("Cannot use -a together with a comment id")
return 102

if remove_all:
comment_id = None
def remove(force, comment):
"""Remove a comment."""
from aiida.orm.comments import Comment

if not force:
if remove_all:
click.confirm("Delete all comments of user {} on node <{}>? ".format(user, node.pk), abort=True)
else:
click.confirm("Delete comment? ", abort=True)

comments = node.get_comment_obj(comment_id=comment_id, user=user)
for comment in comments:
comment.delete()
echo.echo_info("Deleted {} comments.".format(len(comments)))

return 0


@verdi_comment.command()
@click.option('--comment', '-c', type=str, required=False)
@arguments.NODE()
@click.argument('comment_id', type=int, metavar='COMMENT_ID')
@decorators.with_dbenv()
def update(comment, node, comment_id):
"""
Update a comment.
id = The id of the comment
comment = The comment (a string) to be added to the node(s)
"""
from aiida import orm

user = orm.User.objects.get_default()

# read the comment from terminal if it is not on command line
if comment is None:
try:
current_comment = node.get_comments(comment_id)[0]
except IndexError:
echo.echo_error("Comment with id '{}' not found".format(comment_id))
return 1

comment = multi_line_input.edit_comment(current_comment['content'])

# pylint: disable=protected-access
node._update_comment(comment, comment_id, user)

return 0
click.confirm('Are you sure you want to remove comment<{}>'.format(comment), abort=True)

try:
Comment.objects.delete(comment)
except exceptions.NotExistent as exception:
echo.echo_critical('failed to remove comment<{}>: {}'.format(comment, exception))
else:
echo.echo_success('removed comment<{}>'.format(comment))
29 changes: 29 additions & 0 deletions aiida/orm/implementation/django/dummy_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,35 @@ def get_aiida_class(self):
return dblog.get_aiida_class()


class DbComment(Base):
__tablename__ = "db_dbcomment"

id = Column(Integer, primary_key=True)
uuid = Column(UUID(as_uuid=True), default=uuid_func)
dbnode_id = Column(Integer, ForeignKey('db_dbnode.id', ondelete="CASCADE", deferrable=True, initially="DEFERRED"))

ctime = Column(DateTime(timezone=True), default=timezone.now)
mtime = Column(DateTime(timezone=True), default=timezone.now, onupdate=timezone.now)

user_id = Column(Integer, ForeignKey('db_dbuser.id', ondelete="CASCADE", deferrable=True, initially="DEFERRED"))
content = Column(Text, nullable=True)

dbnode = relationship('DbNode', backref='dbcomments')
user = relationship("DbUser")

def get_aiida_class(self):
from aiida.backends.djsite.db.models import DbComment as DjangoDbComment
dbcomment = DjangoDbComment(
id=self.id,
uuid=self.uuid,
dbnode=self.dbnode_id,
ctime=self.ctime,
mtime=self.mtime,
user=self.user_id,
content=self.content)
return dbcomment.get_aiida_class()


profile = get_profile_config(settings.AIIDADB_PROFILE)


Expand Down
Loading

0 comments on commit f5406dd

Please sign in to comment.