From f5406dd13a833aa80fbd0a5deeca25c0cef7c48a Mon Sep 17 00:00:00 2001 From: Sebastiaan Huber Date: Wed, 21 Nov 2018 17:37:45 +0100 Subject: [PATCH] Add support for `Comment` ORM class to `QueryBuilder` 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. --- .../tests/cmdline/commands/test_comment.py | 23 +-- aiida/backends/tests/nodes.py | 14 +- aiida/backends/tests/orm/comment.py | 37 ++++ aiida/cmdline/commands/cmd_comment.py | 172 +++++++----------- .../orm/implementation/django/dummy_model.py | 29 +++ aiida/orm/implementation/django/node.py | 90 --------- .../orm/implementation/django/querybuilder.py | 4 + aiida/orm/implementation/general/node.py | 67 ++++--- aiida/orm/implementation/querybuilder.py | 7 + aiida/orm/implementation/sqlalchemy/node.py | 112 +----------- .../implementation/sqlalchemy/querybuilder.py | 5 + aiida/orm/querybuilder.py | 38 +++- docs/source/developer_guide/internals.rst | 6 +- 13 files changed, 234 insertions(+), 370 deletions(-) diff --git a/aiida/backends/tests/cmdline/commands/test_comment.py b/aiida/backends/tests/cmdline/commands/test_comment.py index afe90b7961..c8fcacff17 100644 --- a/aiida/backends/tests/cmdline/commands/test_comment.py +++ b/aiida/backends/tests/cmdline/commands/test_comment.py @@ -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 @@ -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) diff --git a/aiida/backends/tests/nodes.py b/aiida/backends/tests/nodes.py index 0a6bb49643..d39fc09611 100644 --- a/aiida/backends/tests/nodes.py +++ b/aiida/backends/tests/nodes.py @@ -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'), ]) diff --git a/aiida/backends/tests/orm/comment.py b/aiida/backends/tests/orm/comment.py index e3d2126c81..ba67a0f605 100644 --- a/aiida/backends/tests/orm/comment.py +++ b/aiida/backends/tests/orm/comment.py @@ -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' @@ -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]) diff --git a/aiida/cmdline/commands/cmd_comment.py b/aiida/cmdline/commands/cmd_comment.py index 86fce8a4ad..55a99a2751 100644 --- a/aiida/cmdline/commands/cmd_comment.py +++ b/aiida/cmdline/commands/cmd_comment.py @@ -7,45 +7,59 @@ # 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() @@ -53,112 +67,50 @@ def add(comment, nodes): @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)) diff --git a/aiida/orm/implementation/django/dummy_model.py b/aiida/orm/implementation/django/dummy_model.py index 9f4f9807cc..f3712dbe1a 100644 --- a/aiida/orm/implementation/django/dummy_model.py +++ b/aiida/orm/implementation/django/dummy_model.py @@ -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) diff --git a/aiida/orm/implementation/django/node.py b/aiida/orm/implementation/django/node.py index 16d4520074..931b6a18b4 100644 --- a/aiida/orm/implementation/django/node.py +++ b/aiida/orm/implementation/django/node.py @@ -396,96 +396,6 @@ def _db_attrs(self): for attr in attrlist: yield attr.key - def add_comment(self, content, user=None): - from aiida.backends.djsite.db.models import DbComment - from aiida import orm - - if not self.is_stored: - raise ModificationNotAllowed("Comments can be added only after " - "storing the node") - - if user is None: - user = orm.User.objects(self.backend).get_default() - - return DbComment.objects.create(dbnode=self._dbnode, - user=user.backend_entity.dbmodel, - content=content).id - - def get_comment_obj(self, comment_id=None, user=None): - from aiida.backends.djsite.db.models import DbComment - import operator - from django.db.models import Q - from aiida import orm - query_list = [] - - # If an id is specified then we add it to the query - if comment_id is not None: - query_list.append(Q(pk=comment_id)) - - # If a user is specified then we add it to the query - if user is not None: - query_list.append(Q(user=user.backend_entity.dbmodel)) - - dbcomments = DbComment.objects.filter( - reduce(operator.and_, query_list)) - comments = [] - from aiida.orm.implementation.django.comment import DjangoComment - for dbcomment in dbcomments: - comments.append(DjangoComment.from_dbmodel(dbcomment, orm.construct_backend())) - return comments - - def get_comments(self, pk=None): - from aiida.backends.djsite.db.models import DbComment - if pk is not None: - try: - correct = all([isinstance(_, int) for _ in pk]) - if not correct: - raise ValueError('pk must be an integer or a list of integers') - except TypeError: - if not isinstance(pk, int): - raise ValueError('pk must be an integer or a list of integers') - return list(DbComment.objects.filter( - dbnode=self._dbnode, pk=pk).order_by('pk').values( - 'pk', 'user__email', 'ctime', 'mtime', 'content')) - - return list(DbComment.objects.filter(dbnode=self._dbnode).order_by( - 'pk').values('pk', 'user__email', 'ctime', 'mtime', 'content')) - - def _get_dbcomments(self, pk=None): - from aiida.backends.djsite.db.models import DbComment - if pk is not None: - try: - correct = all([isinstance(_, int) for _ in pk]) - if not correct: - raise ValueError('pk must be an integer or a list of integers') - return list(DbComment.objects.filter(dbnode=self._dbnode, pk__in=pk).order_by('pk')) - except TypeError: - if not isinstance(pk, int): - raise ValueError('pk must be an integer or a list of integers') - return list(DbComment.objects.filter(dbnode=self._dbnode, pk=pk).order_by('pk')) - - return list(DbComment.objects.filter(dbnode=self._dbnode).order_by('pk')) - - def _update_comment(self, new_field, comment_pk, user): - from aiida.backends.djsite.db.models import DbComment - comment = list(DbComment.objects.filter(dbnode=self._dbnode, - pk=comment_pk, user=user))[0] - - if not isinstance(new_field, six.string_types): - raise ValueError("Non string comments are not accepted") - - if not comment: - raise NotExistent("Found no comment for user {} and pk {}".format( - user, comment_pk)) - - comment.content = new_field - comment.save() - - def _remove_comment(self, comment_pk, user): - from aiida.backends.djsite.db.models import DbComment - comment = DbComment.objects.filter(dbnode=self._dbnode, pk=comment_pk)[0] - comment.delete() - def _increment_version_number_db(self): from aiida.backends.djsite.db.models import DbNode # I increment the node number using a filter diff --git a/aiida/orm/implementation/django/querybuilder.py b/aiida/orm/implementation/django/querybuilder.py index 6dee31bf50..b826d40242 100644 --- a/aiida/orm/implementation/django/querybuilder.py +++ b/aiida/orm/implementation/django/querybuilder.py @@ -57,6 +57,10 @@ def Group(self): def AuthInfo(self): return dummy_model.DbAuthInfo + @property + def Comment(self): + return dummy_model.DbComment + @property def log_model_class(self): return dummy_model.DbLog diff --git a/aiida/orm/implementation/general/node.py b/aiida/orm/implementation/general/node.py index 4c7aaf9e77..4f51133a1a 100644 --- a/aiida/orm/implementation/general/node.py +++ b/aiida/orm/implementation/general/node.py @@ -25,15 +25,16 @@ from aiida.common.caching import get_use_cache from aiida.common.exceptions import InternalError, ModificationNotAllowed, UniquenessError, ValidationError from aiida.common.folders import SandboxFolder +from aiida.common.hashing import _HASH_EXTRA_KEY from aiida.common.lang import override from aiida.common.links import LinkType from aiida.common.utils import abstractclassmethod from aiida.common.utils import combomethod, classproperty from aiida.plugins.loader import get_query_type_from_type_string, get_type_string_from_class -from aiida.common.hashing import _HASH_EXTRA_KEY _NO_DEFAULT = tuple() + def clean_value(value): """ Get value from input and (recursively) replace, if needed, all occurrences @@ -1345,53 +1346,63 @@ def get_attrs(self): """ return dict(self.iterattrs()) - @abstractmethod def add_comment(self, content, user=None): """ Add a new comment. :param content: string with comment - :return: An ID for the newly added comment + :return: the newly created comment """ - pass + from aiida import orm + from aiida.orm.comments import Comment - @abstractmethod - def get_comments(self, pk=None): + if user is None: + user = orm.User.objects.get_default() + + return Comment(node=self, user=user, content=content).store() + + def get_comment(self, identifier): """ - Return a sorted list of comment values, one for each comment associated - to the node. + Return a comment corresponding to the given identifier. - :param pk: integer or list of integers. If it is specified, returns the - comment values with desired pks. (pk refers to DbComment.pk) - :return: the list of comments, sorted by pk; each element of the - list is a dictionary, containing (pk, email, ctime, mtime, content) + :param identifier: the comment pk + :raise NotExistent: if the comment with the given id does not exist + :raise MultipleObjectsError: if the id cannot be uniquely resolved to a comment + :return: the comment """ - pass + from aiida.orm.comments import Comment + return Comment.objects.get(comment=identifier) - @abstractmethod - def _get_dbcomments(self, pk=None): + def get_comments(self): """ - Return a sorted list of DbComment associated with the Node. + Return a sorted list of comments for this node. - :param pk: integer or list of integers. If it is specified, returns the - comment values with desired pks. (pk refers to DbComment.pk) - :return: the list of DbComment, sorted by pk. + :return: the list of comments, sorted by pk """ - pass + from aiida.orm.comments import Comment + return Comment.objects.find(filters={'dbnode_id': self.pk}) - @abstractmethod - def _update_comment(self, new_field, comment_pk, user): + def update_comment(self, identifier, content): """ - Function called by verdi comment update + Update the content of an existing comment. + + :param identifier: the comment pk + :param content: the new comment content + :raise NotExistent: if the comment with the given id does not exist + :raise MultipleObjectsError: if the id cannot be uniquely resolved to a comment """ - pass + from aiida.orm.comments import Comment + comment = Comment.objects.get(comment=identifier) + comment.set_content(content) - @abstractmethod - def _remove_comment(self, comment_pk, user): + def remove_comment(self, identifier): """ - Function called by verdi comment remove + Delete an existing comment. + + :param identifier: the comment pk """ - pass + from aiida.orm.comments import Comment + Comment.objects.delete(comment=identifier) @abstractmethod def _increment_version_number_db(self): diff --git a/aiida/orm/implementation/querybuilder.py b/aiida/orm/implementation/querybuilder.py index 67bbd2f5e7..15dbfc7c1c 100644 --- a/aiida/orm/implementation/querybuilder.py +++ b/aiida/orm/implementation/querybuilder.py @@ -74,6 +74,13 @@ def AuthInfo(self): """ pass + @abc.abstractmethod + def Comment(self): + """ + A property, decorated with @property. Returns the implementation for the Comment + """ + pass + @abc.abstractmethod def log_model_class(self): """ diff --git a/aiida/orm/implementation/sqlalchemy/node.py b/aiida/orm/implementation/sqlalchemy/node.py index 35897688ce..6344eb5afc 100644 --- a/aiida/orm/implementation/sqlalchemy/node.py +++ b/aiida/orm/implementation/sqlalchemy/node.py @@ -55,9 +55,9 @@ def __init__(self, **kwargs): if dbnode is not None: type_check(dbnode, DbNode) if dbnode.id is None: - raise ValueError("If cannot load an aiida.orm.Node instance " "from an unsaved DbNode object.") + raise ValueError("I cannot load an aiida.orm.Node instance from an unsaved DbNode object.") if kwargs: - raise ValueError("If you pass a dbnode, you cannot pass any " "further parameter") + raise ValueError("If you pass a dbnode, you cannot pass any further parameter") # If I am loading, I cannot modify it self._to_be_stored = False @@ -440,114 +440,6 @@ def _db_attrs(self): for key in self._attributes().keys(): yield key - def add_comment(self, content, user=None): - from aiida import orm - from aiida.backends.sqlalchemy import get_scoped_session - - session = get_scoped_session() - - if self._to_be_stored: - raise ModificationNotAllowed("Comments can be added only after " "storing the node") - - if user is None: - user = orm.User.objects(self.backend).get_default() - - comment = DbComment(dbnode=self._dbnode, user=user.backend_entity.dbmodel, content=content) - session.add(comment) - try: - session.commit() - except: - session = get_scoped_session() - session.rollback() - raise - - return comment.id - - def get_comment_obj(self, comment_id=None, user=None): - """ - Get comment models objects for this node, optionally for a given comment - id or user. - - :param comment_id: Filter for a particular comment id - :param user: Filter for a particular user - :return: A list of comment model instances - """ - from aiida import orm - - query = DbComment.query.filter_by(dbnode=self._dbnode) - - if comment_id is not None: - query = query.filter_by(id=comment_id) - if user is not None: - if isinstance(user, orm.User): - user = user.backend_entity - query = query.filter_by(user=user.dbmodel) - - dbcomments = query.all() - comments = [] - from aiida.orm.implementation.sqlalchemy.comment import SqlaComment - for dbcomment in dbcomments: - comments.append(SqlaComment.from_dbmodel(dbcomment, orm.construct_backend())) - return comments - - def get_comments(self, pk=None): - comments = self._get_dbcomments(pk) - - return [{ - "pk": c.id, - "user__email": c.user.email, - "ctime": c.ctime, - "mtime": c.mtime, - "content": c.content - } for c in comments] - - def _get_dbcomments(self, pk=None): - comments = DbComment.query.filter_by(dbnode=self._dbnode) - - if pk is not None: - try: - correct = all([isinstance(_, int) for _ in pk]) - if not correct: - raise ValueError('id must be an integer or a list of integers') - comments = comments.filter(DbComment.id.in_(pk)) - except TypeError: - if not isinstance(pk, int): - raise ValueError('id must be an integer or a list of integers') - - comments = comments.filter_by(id=pk) - - comments = comments.order_by('id').all() - return comments - - def _update_comment(self, new_field, comment_pk, user): - comment = DbComment.query.filter_by(dbnode=self._dbnode, id=comment_pk, user=user.dbmodel).first() - - if not isinstance(new_field, six.string_types): - raise ValueError("Non string comments are not accepted") - - if not comment: - raise NotExistent("Found no comment for user {} and id {}".format(user, comment_pk)) - - comment.content = new_field - try: - comment.save() - except: - from aiida.backends.sqlalchemy import get_scoped_session - session = get_scoped_session() - session.rollback() - raise - - def _remove_comment(self, comment_pk, user): - comment = DbComment.query.filter_by(dbnode=self._dbnode, id=comment_pk, user=user.dbmodel).first() - if comment: - try: - comment.delete() - except: - from aiida.backends.sqlalchemy import get_scoped_session - session = get_scoped_session() - session.rollback() - raise - def _increment_version_number_db(self): self._dbnode.nodeversion = self.nodeversion + 1 try: diff --git a/aiida/orm/implementation/sqlalchemy/querybuilder.py b/aiida/orm/implementation/sqlalchemy/querybuilder.py index 45df52d8d1..e8379aaff7 100644 --- a/aiida/orm/implementation/sqlalchemy/querybuilder.py +++ b/aiida/orm/implementation/sqlalchemy/querybuilder.py @@ -105,6 +105,11 @@ def AuthInfo(self): import aiida.backends.sqlalchemy.models.authinfo return aiida.backends.sqlalchemy.models.authinfo.DbAuthInfo + @property + def Comment(self): + import aiida.backends.sqlalchemy.models.comment + return aiida.backends.sqlalchemy.models.comment.DbComment + @property def log_model_class(self): import aiida.backends.sqlalchemy.models.log diff --git a/aiida/orm/querybuilder.py b/aiida/orm/querybuilder.py index 713c31fd81..644d1c132d 100644 --- a/aiida/orm/querybuilder.py +++ b/aiida/orm/querybuilder.py @@ -45,6 +45,7 @@ from aiida.orm.utils import convert from . import authinfos +from . import comments from . import computers from . import entities from . import groups @@ -115,6 +116,16 @@ def get_querybuilder_classifiers_from_cls(cls, obj): query_type_string = None ormclass = obj.AuthInfo + # Comment + elif issubclass(cls, obj.Comment): + ormclasstype = 'comment' + query_type_string = None + ormclass = cls + elif issubclass(cls, comments.Comment): + ormclasstype = 'comment' + query_type_string = None + ormclass = obj.Comment + # Log elif issubclass(cls, obj.log_model_class): ormclasstype = 'log' @@ -1441,6 +1452,22 @@ def _join_user_group(self, joined_entity, entity_to_join, isouterjoin): self._check_dbentities((joined_entity, self._impl.User), (entity_to_join, self._impl.Group), 'with_user') self._query = self._query.join(entity_to_join, joined_entity.id == entity_to_join.user_id, isouter=isouterjoin) + def _join_node_comment(self, joined_entity, entity_to_join, isouterjoin): + """ + :param joined_entity: An aliased node + :param entity_to_join: aliased comment + """ + self._check_dbentities((joined_entity, self._impl.Node), (entity_to_join, self._impl.Comment), 'with_node') + self._query = self._query.join(entity_to_join, joined_entity.id == entity_to_join.dbnode_id, isouter=isouterjoin) + + def _join_comment_node(self, joined_entity, entity_to_join, isouterjoin): + """ + :param joined_entity: An aliased comment + :param entity_to_join: aliased node + """ + self._check_dbentities((joined_entity, self._impl.Comment), (entity_to_join, self._impl.Node), 'with_comment') + self._query = self._query.join(entity_to_join, joined_entity.dbnode_id == entity_to_join.id, isouter=isouterjoin) + def _get_function_map(self): """ Map relationship type keywords to functions @@ -1449,6 +1476,7 @@ def _get_function_map(self): """ mapping = { 'node': { + 'with_comment': self._join_comment_node, 'with_incoming': self._join_outputs, 'with_outgoing': self._join_inputs, 'ancestor_of': self._join_ancestors_recursive, @@ -1482,6 +1510,10 @@ def _get_function_map(self): 'group_of': self._deprecate(self._join_groups, 'group_of', 'with_node'), 'belongs_to': self._deprecate(self._join_user_group, 'belongs_to', 'with_user') }, + 'comment': { + 'with_node': self._join_node_comment, + 'direction': None, + }, } return mapping @@ -1495,10 +1527,8 @@ def _get_connecting_node(self, index, joining_keyword=None, joining_value=None, :param joining_keyword: the relation on which to join :param joining_value: the tag of the nodes to be joined """ - from aiida.cmdline.utils import echo - # Set the calling entity - to allow for the correct join relation to be set - if self._path[index]['type'] not in ['computer', 'user', 'group']: + if self._path[index]['type'] not in ['computer', 'user', 'group', 'comment']: calling_entity = 'node' else: calling_entity = self._path[index]['type'] @@ -1514,7 +1544,7 @@ def _get_connecting_node(self, index, joining_keyword=None, joining_value=None, try: func = self._get_function_map()[calling_entity][joining_keyword] except KeyError: - echo.echo_critical("'{}' is not a valid joining keyword for a '{}' type entity".format( + raise InputValidationError("'{}' is not a valid joining keyword for a '{}' type entity".format( joining_keyword, calling_entity)) if isinstance(joining_value, int): diff --git a/docs/source/developer_guide/internals.rst b/docs/source/developer_guide/internals.rst index 26de98350a..7bf6b38b80 100644 --- a/docs/source/developer_guide/internals.rst +++ b/docs/source/developer_guide/internals.rst @@ -85,11 +85,9 @@ The :py:class:`~aiida.orm.implementation.general.node.AbstractNode` can be annot - :py:meth:`~aiida.orm.implementation.general.node.AbstractNode.get_comments` returns a sorted list of the comments. -- :py:meth:`~aiida.orm.implementation.general.node.AbstractNode._get_dbcomments` is similar to :py:meth:`~aiida.orm.implementation.general.node.AbstractNode.get_comments`, just the sorting changes. +- :py:meth:`~aiida.orm.implementation.general.node.AbstractNode.update_comment` updates the node comment. It can be done by ``verdi comment update``. -- :py:meth:`~aiida.orm.implementation.general.node.AbstractNode._update_comment` updates the node comment. It can be done by ``verdi comment update``. - -- :py:meth:`~aiida.orm.implementation.general.node.AbstractNode._remove_comment` removes the node comment. It can be done by ``verdi comment remove``. +- :py:meth:`~aiida.orm.implementation.general.node.AbstractNode.remove_comment` removes the node comment. It can be done by ``verdi comment remove``.