From 84a57568b5893a2e9a6e98a290f79919ffc1f11a Mon Sep 17 00:00:00 2001 From: Sebastiaan Huber Date: Tue, 20 Nov 2018 22:17:26 +0100 Subject: [PATCH 1/2] Move the `Comment` class to the new backend interface --- .pre-commit-config.yaml | 3 - aiida/backends/djsite/db/models.py | 1 - aiida/backends/sqlalchemy/models/comment.py | 2 +- aiida/backends/sqlalchemy/models/node.py | 2 +- aiida/backends/sqlalchemy/models/settings.py | 2 +- aiida/backends/sqlalchemy/models/workflow.py | 2 +- aiida/backends/tests/__init__.py | 3 +- .../tests/cmdline/commands/test_comment.py | 65 ++---- aiida/backends/tests/orm/comment.py | 65 ++++++ aiida/orm/__init__.py | 12 +- aiida/orm/comments.py | 91 ++++++++ aiida/orm/implementation/__init__.py | 20 +- aiida/orm/implementation/backends.py | 44 ++-- aiida/orm/implementation/comments.py | 101 +++++++++ aiida/orm/implementation/django/backend.py | 27 ++- aiida/orm/implementation/django/comment.py | 212 ++++++++--------- aiida/orm/implementation/django/entities.py | 3 +- aiida/orm/implementation/django/node.py | 5 +- aiida/orm/implementation/django/users.py | 2 +- aiida/orm/implementation/django/utils.py | 13 +- aiida/orm/implementation/general/comment.py | 77 ------- .../orm/implementation/sqlalchemy/backend.py | 32 +-- .../orm/implementation/sqlalchemy/comment.py | 213 ++++++++++-------- aiida/orm/implementation/sqlalchemy/node.py | 4 +- aiida/orm/implementation/sqlalchemy/users.py | 2 +- aiida/orm/implementation/sqlalchemy/utils.py | 12 +- 26 files changed, 619 insertions(+), 396 deletions(-) create mode 100644 aiida/backends/tests/orm/comment.py create mode 100644 aiida/orm/comments.py create mode 100644 aiida/orm/implementation/comments.py delete mode 100644 aiida/orm/implementation/general/comment.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36b70314e2..3207d410ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -234,7 +234,6 @@ aiida/orm/implementation/django/authinfo.py| aiida/orm/implementation/django/backend.py| aiida/orm/implementation/django/code.py| - aiida/orm/implementation/django/comment.py| aiida/orm/implementation/django/computer.py| aiida/orm/implementation/django/group.py| aiida/orm/implementation/django/__init__.py| @@ -242,7 +241,6 @@ aiida/orm/implementation/django/user.py| aiida/orm/implementation/django/utils.py| aiida/orm/implementation/django/workflow.py| - aiida/orm/implementation/general/comment.py| aiida/orm/implementation/general/group.py| aiida/orm/implementation/general/__init__.py| aiida/orm/implementation/general/node.py| @@ -251,7 +249,6 @@ aiida/orm/implementation/sqlalchemy/authinfo.py| aiida/orm/implementation/sqlalchemy/backend.py| aiida/orm/implementation/sqlalchemy/code.py| - aiida/orm/implementation/sqlalchemy/comment.py| aiida/orm/implementation/sqlalchemy/computer.py| aiida/orm/implementation/sqlalchemy/group.py| aiida/orm/implementation/sqlalchemy/__init__.py| diff --git a/aiida/backends/djsite/db/models.py b/aiida/backends/djsite/db/models.py index cc9126afd1..4688d3973b 100644 --- a/aiida/backends/djsite/db/models.py +++ b/aiida/backends/djsite/db/models.py @@ -1456,7 +1456,6 @@ def get_aiida_class(self): return DjangoAuthInfo.from_dbmodel(self, construct_backend()) - @python_2_unicode_compatible class DbComment(m.Model): uuid = m.UUIDField(default=get_new_uuid, unique=True) diff --git a/aiida/backends/sqlalchemy/models/comment.py b/aiida/backends/sqlalchemy/models/comment.py index 6017310661..19784b6cd4 100644 --- a/aiida/backends/sqlalchemy/models/comment.py +++ b/aiida/backends/sqlalchemy/models/comment.py @@ -37,7 +37,7 @@ class DbComment(Base): ) ctime = Column(DateTime(timezone=True), default=timezone.now) - mtime = Column(DateTime(timezone=True), default=timezone.now) + mtime = Column(DateTime(timezone=True), default=timezone.now, onupdate=timezone.now) user_id = Column( Integer, diff --git a/aiida/backends/sqlalchemy/models/node.py b/aiida/backends/sqlalchemy/models/node.py index 1f4f675aa7..75cc7cee3d 100644 --- a/aiida/backends/sqlalchemy/models/node.py +++ b/aiida/backends/sqlalchemy/models/node.py @@ -47,7 +47,7 @@ class DbNode(Base): default="") # Does it make sense to be nullable and have a default? description = Column(Text(), nullable=True, default="") ctime = Column(DateTime(timezone=True), default=timezone.now) - mtime = Column(DateTime(timezone=True), default=timezone.now) + mtime = Column(DateTime(timezone=True), default=timezone.now, onupdate=timezone.now) nodeversion = Column(Integer, default=1) public = Column(Boolean, default=False) attributes = Column(JSONB) diff --git a/aiida/backends/sqlalchemy/models/settings.py b/aiida/backends/sqlalchemy/models/settings.py index 9d900b7d86..1c1d2c0e6a 100644 --- a/aiida/backends/sqlalchemy/models/settings.py +++ b/aiida/backends/sqlalchemy/models/settings.py @@ -32,7 +32,7 @@ class DbSetting(Base): # I also add a description field for the variables description = Column(String(255), default='', nullable=False) - time = Column(DateTime(timezone=True), default=timezone.utc) + time = Column(DateTime(timezone=True), default=timezone.utc, onupdate=timezone.now) def __str__(self): return "'{}'={}".format(self.key, self.getvalue()) diff --git a/aiida/backends/sqlalchemy/models/workflow.py b/aiida/backends/sqlalchemy/models/workflow.py index 8d84b3716b..0114c6dd56 100644 --- a/aiida/backends/sqlalchemy/models/workflow.py +++ b/aiida/backends/sqlalchemy/models/workflow.py @@ -39,7 +39,7 @@ class DbWorkflow(Base): uuid = Column(UUID(as_uuid=True), default=uuid_func) ctime = Column(DateTime(timezone=True), default=timezone.now) - mtime = 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')) user = relationship('DbUser') diff --git a/aiida/backends/tests/__init__.py b/aiida/backends/tests/__init__.py index e03a102aee..d267470267 100644 --- a/aiida/backends/tests/__init__.py +++ b/aiida/backends/tests/__init__.py @@ -86,8 +86,9 @@ 'common.archive': ['aiida.backends.tests.common.test_archive'], 'common.datastructures': ['aiida.backends.tests.common.test_datastructures'], 'daemon.client': ['aiida.backends.tests.daemon.test_client'], - 'orm.computer': ['aiida.backends.tests.computer'], 'orm.authinfo': ['aiida.backends.tests.orm.authinfo'], + 'orm.comment': ['aiida.backends.tests.orm.comment'], + 'orm.computer': ['aiida.backends.tests.computer'], 'orm.data.frozendict': ['aiida.backends.tests.orm.data.frozendict'], 'orm.data.remote': ['aiida.backends.tests.orm.data.remote'], 'orm.log': ['aiida.backends.tests.orm.log'], diff --git a/aiida/backends/tests/cmdline/commands/test_comment.py b/aiida/backends/tests/cmdline/commands/test_comment.py index 956ba1846a..afe90b7961 100644 --- a/aiida/backends/tests/cmdline/commands/test_comment.py +++ b/aiida/backends/tests/cmdline/commands/test_comment.py @@ -15,6 +15,7 @@ from click.testing import CliRunner from aiida.backends.testbase import AiidaTestCase +from aiida.cmdline.commands import cmd_comment from aiida import orm COMMENT = u'Well I never...' @@ -24,68 +25,52 @@ class TestVerdiUserCommand(AiidaTestCase): def setUp(self): self.cli_runner = CliRunner() + self.node = orm.Node().store() def test_comment_show_simple(self): - """ test simply calling the show command (without data to show) """ - from aiida.cmdline.commands.cmd_comment import show - - result = CliRunner().invoke(show, [], catch_exceptions=False) - self.assertEqual(result.output, "") + """Test simply calling the show command (without data to show).""" + result = self.cli_runner.invoke(cmd_comment.show, [], catch_exceptions=False) + self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_comment_show(self): - """ Test showing an existing comment """ - from aiida.cmdline.commands.cmd_comment import show - - node = orm.Node() - node.store() - node.add_comment(COMMENT) + """Test showing an existing comment.""" + self.node.add_comment(COMMENT) - result = CliRunner().invoke(show, [str(node.pk)], catch_exceptions=False) + options = [str(self.node.pk)] + result = self.cli_runner.invoke(cmd_comment.show, options, catch_exceptions=False) self.assertNotEqual(result.output.find(COMMENT), -1) self.assertEqual(result.exit_code, 0) def test_comment_add(self): - """ Test adding a comment """ - from aiida.cmdline.commands.cmd_comment import add - - node = orm.Node() - node.store() - - result = CliRunner().invoke(add, ['-c{}'.format(COMMENT), str(node.pk)], catch_exceptions=False) + """Test adding a comment.""" + options = ['-c{}'.format(COMMENT), str(self.node.pk)] + result = self.cli_runner.invoke(cmd_comment.add, options, catch_exceptions=False) self.assertEqual(result.exit_code, 0) - comment = node.get_comments() + comment = self.node.get_comments() self.assertEquals(len(comment), 1) self.assertEqual(comment[0]['content'], COMMENT) def test_comment_remove(self): - """ Test removing a comment """ - from aiida.cmdline.commands.cmd_comment import remove + """Test removing a comment.""" + pk = self.node.add_comment(COMMENT) - node = orm.Node() - node.store() - comment_id = node.add_comment(COMMENT) + self.assertEquals(len(self.node.get_comments()), 1) - self.assertEquals(len(node.get_comments()), 1) - - result = CliRunner().invoke(remove, [str(node.pk), str(comment_id), '--force'], catch_exceptions=False) + options = [str(self.node.pk), str(pk), '--force'] + result = self.cli_runner.invoke(cmd_comment.remove, options, catch_exceptions=False) self.assertEqual(result.exit_code, 0) - - self.assertEquals(len(node.get_comments()), 0) + self.assertEquals(len(self.node.get_comments()), 0) def test_comment_remove_all(self): - """ Test removing all comments from a node """ - from aiida.cmdline.commands.cmd_comment import remove - - node = orm.Node() - node.store() + """Test removing all comments from a self.node.""" for _ in range(10): - node.add_comment(COMMENT) + self.node.add_comment(COMMENT) - self.assertEqual(len(node.get_comments()), 10) + self.assertEqual(len(self.node.get_comments()), 10) - result = CliRunner().invoke(remove, [str(node.pk), '--all', '--force'], catch_exceptions=False) + 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(node.get_comments()), 0) + self.assertEqual(len(self.node.get_comments()), 0) diff --git a/aiida/backends/tests/orm/comment.py b/aiida/backends/tests/orm/comment.py new file mode 100644 index 0000000000..e3d2126c81 --- /dev/null +++ b/aiida/backends/tests/orm/comment.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +########################################################################### +# Copyright (c), The AiiDA team. All rights reserved. # +# This file is part of the AiiDA code. # +# # +# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core # +# For further information on the license, see the LICENSE.txt file # +# For further information please visit http://www.aiida.net # +########################################################################### +"""Unit tests for the Comment ORM class.""" +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +from aiida import orm +from aiida.orm.comments import Comment +from aiida.backends.testbase import AiidaTestCase +from aiida.common import exceptions + + +class TestComment(AiidaTestCase): + """Unit tests for the Comment ORM class.""" + + def setUp(self): + super(TestComment, self).setUp() + self.node = orm.Node().store() + self.user = orm.User.objects.get_default() + self.content = 'Sometimes when I am freestyling, I lose confidence' + self.comment = Comment(self.node, self.user, self.content).store() + + def test_comment_content(self): + """Test getting and setting content of a Comment.""" + content = 'Be more constructive with your feedback' + self.comment.set_content(content) + self.assertEqual(self.comment.content, content) + + def test_comment_mtime(self): + """Test getting and setting mtime of a Comment.""" + mtime = self.comment.mtime + self.comment.set_content('Changing an attribute should automatically change the mtime') + self.assertEqual(self.comment.content, 'Changing an attribute should automatically change the mtime') + self.assertNotEqual(self.comment.mtime, mtime) + + def test_comment_node(self): + """Test getting the node of a Comment.""" + self.assertEqual(self.comment.node.uuid, self.node.uuid) + + def test_comment_user(self): + """Test getting the user of a Comment.""" + self.assertEqual(self.comment.user.uuid, self.user.uuid) + + def test_comment_collection_get(self): + """Test retrieving a Comment through the collection.""" + comment = Comment.objects.get(comment=self.comment.pk) + self.assertEqual(self.comment.uuid, comment.uuid) + + def test_comment_collection_delete(self): + """Test deleting a Comment through the collection.""" + comment = Comment(self.node, self.user, 'I will perish').store() + comment_pk = comment.pk + + Comment.objects.delete(comment=comment.pk) + + with self.assertRaises(exceptions.NotExistent): + Comment.objects.get(comment=comment_pk) diff --git a/aiida/orm/__init__.py b/aiida/orm/__init__.py index 1a3f55659c..9e3aaf81a5 100644 --- a/aiida/orm/__init__.py +++ b/aiida/orm/__init__.py @@ -18,16 +18,16 @@ from aiida.orm.workflow import Workflow from .authinfos import * +from .backends import * from .calculation import * from .computers import * from .entities import * from .groups import * from .logs import * from .node import * -from .backends import * +from .querybuilder import * from .users import * from .utils import * -from .querybuilder import * # For legacy reasons support the singulars as well authinfo = authinfos @@ -41,12 +41,12 @@ __all__ = (_local + authinfos.__all__ + + backends.__all__ + calculation.__all__ + computers.__all__ + entities.__all__ + + groups.__all__ + logs.__all__ + - utils.__all__ + - users.__all__ + - backends.__all__ + querybuilder.__all__ + - groups.__all__) + users.__all__ + + utils.__all__) diff --git a/aiida/orm/comments.py b/aiida/orm/comments.py new file mode 100644 index 0000000000..f75bfa8cc9 --- /dev/null +++ b/aiida/orm/comments.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +########################################################################### +# Copyright (c), The AiiDA team. All rights reserved. # +# This file is part of the AiiDA code. # +# # +# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core # +# For further information on the license, see the LICENSE.txt file # +# For further information please visit http://www.aiida.net # +########################################################################### +"""Comment objects and functions""" +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +from . import backends +from . import entities +from . import users + +__all__ = ('Comment',) + + +class Comment(entities.Entity): + """Base class to map a DbComment that represents a comment attached to a certain Node.""" + + class Collection(entities.Collection): + """The collection of Comment entries.""" + + def delete(self, comment): + """ + Remove a Comment from the collection with the given id + + :param comment: the id of the comment to delete + """ + self._backend.comments.delete(comment) + + def get(self, comment): + """ + Return a Comment given its id + + :param comment: the id of the comment to retrieve + :return: the comment + :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 self._backend.comments.get(comment) + + def __init__(self, node, user, content=None, backend=None): + """ + Create a Comment for a given node and user + + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + backend = backend or backends.construct_backend() + model = backend.comments.create(node=node, user=user.backend_entity, content=content) + super(Comment, self).__init__(model) + + def __str__(self): + arguments = [self.uuid, self.node.pk, self.user.email, self.content] + return 'Comment<{}> for node<{}> and user<{}>: {}'.format(*arguments) + + @property + def ctime(self): + return self._backend_entity.ctime + + @property + def mtime(self): + return self._backend_entity.mtime + + def set_mtime(self, value): + return self._backend_entity.set_mtime(value) + + @property + def node(self): + return self._backend_entity.node + + @property + def user(self): + return users.User.from_backend_entity(self._backend_entity.user) + + def set_user(self, value): + self._backend_entity.set_user(value.backend_entity) + + @property + def content(self): + return self._backend_entity.content + + def set_content(self, value): + return self._backend_entity.set_content(value) diff --git a/aiida/orm/implementation/__init__.py b/aiida/orm/implementation/__init__.py index 0cc8637a94..e2bb87fe65 100644 --- a/aiida/orm/implementation/__init__.py +++ b/aiida/orm/implementation/__init__.py @@ -7,44 +7,44 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### - from __future__ import division from __future__ import print_function from __future__ import absolute_import + +from aiida.backends.profile import BACKEND_DJANGO, BACKEND_SQLA from aiida.backends.settings import BACKEND from aiida.common.exceptions import ConfigurationError -from aiida.backends.profile import BACKEND_DJANGO, BACKEND_SQLA from .authinfos import * from .backends import * +from .comments import * from .computers import * +from .groups import * from .logs import * from .querybuilder import * -from .groups import * from .users import * -_local = 'Node', 'Workflow', 'kill_all', 'get_all_running_steps', 'get_workflow_info', 'delete_code', 'Comment', +_local = 'Node', 'Workflow', 'kill_all', 'get_all_running_steps', 'get_workflow_info', 'delete_code', __all__ = (_local + - computers.__all__ + - logs.__all__ + - users.__all__ + authinfos.__all__ + backends.__all__ + + comments.__all__ + + computers.__all__ + + groups.__all__ + + logs.__all__ + querybuilder.__all__ + - groups.__all__) + users.__all__) if BACKEND == BACKEND_SQLA: from aiida.orm.implementation.sqlalchemy.node import Node from aiida.orm.implementation.sqlalchemy.workflow import Workflow, kill_all, get_workflow_info, \ get_all_running_steps from aiida.orm.implementation.sqlalchemy.code import delete_code - from aiida.orm.implementation.sqlalchemy.comment import Comment elif BACKEND == BACKEND_DJANGO: from aiida.orm.implementation.django.node import Node from aiida.orm.implementation.django.workflow import Workflow, kill_all, get_workflow_info, get_all_running_steps from aiida.orm.implementation.django.code import delete_code - from aiida.orm.implementation.django.comment import Comment elif BACKEND is None: raise ConfigurationError("settings.BACKEND has not been set.\n" "Hint: Have you called aiida.load_dbenv?") diff --git a/aiida/orm/implementation/backends.py b/aiida/orm/implementation/backends.py index 3e4747f086..86b4f36691 100644 --- a/aiida/orm/implementation/backends.py +++ b/aiida/orm/implementation/backends.py @@ -26,30 +26,21 @@ class Backend(object): """The public interface that defines a backend factory that creates backend specific concrete objects.""" @abc.abstractproperty - def logs(self): - """ - Return the collection of log entries - - :return: the log collection - :rtype: :class:`aiida.orm.implementation.BackendLogCollection` - """ - - @abc.abstractproperty - def users(self): + def authinfos(self): """ - Return the collection of users + Return the collection of authorisation information objects - :return: the users collection - :rtype: :class:`aiida.orm.implementation.BackendUserCollection` + :return: the authinfo collection + :rtype: :class:`aiida.orm.implementation.BackendAuthInfoCollection` """ @abc.abstractproperty - def authinfos(self): + def comments(self): """ - Return the collection of authorisation information objects + Return the collection of comment objects - :return: the authinfo collection - :rtype: :class:`aiida.orm.implementation.BackendAuthInfoCollection` + :return: the comment collection + :rtype: :class:`aiida.orm.implementation.BackendCommentCollection` """ @abc.abstractproperty @@ -70,6 +61,15 @@ def groups(self): :rtype: :class:`aiida.orm.implementation.BackendGroupCollection` """ + @abc.abstractproperty + def logs(self): + """ + Return the collection of log entries + + :return: the log collection + :rtype: :class:`aiida.orm.implementation.BackendLogCollection` + """ + @abc.abstractproperty def query_manager(self): """ @@ -88,6 +88,15 @@ def query(self): :rtype: :class:`aiida.orm.implementation.BackendQueryBuilder` """ + @abc.abstractproperty + def users(self): + """ + Return the collection of users + + :return: the users collection + :rtype: :class:`aiida.orm.implementation.BackendUserCollection` + """ + @six.add_metaclass(abc.ABCMeta) class BackendEntity(object): @@ -104,6 +113,7 @@ def id(self): # pylint: disable=invalid-name :return: the entity id """ + pass @property def backend(self): diff --git a/aiida/orm/implementation/comments.py b/aiida/orm/implementation/comments.py new file mode 100644 index 0000000000..d1e7b7536a --- /dev/null +++ b/aiida/orm/implementation/comments.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +########################################################################### +# Copyright (c), The AiiDA team. All rights reserved. # +# This file is part of the AiiDA code. # +# # +# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core # +# For further information on the license, see the LICENSE.txt file # +# For further information please visit http://www.aiida.net # +########################################################################### +"""Module for comment backend classes.""" +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +import abc +import six + +from . import backends + +__all__ = 'BackendComment', 'BackendCommentCollection' + + +@six.add_metaclass(abc.ABCMeta) +class BackendComment(backends.BackendEntity): + """Base class for a node comment.""" + + @abc.abstractproperty + def uuid(self): + pass + + @abc.abstractproperty + def ctime(self): + pass + + @abc.abstractproperty + def mtime(self): + pass + + @abc.abstractmethod + def set_mtime(self, value): + pass + + @abc.abstractproperty + def node(self): + pass + + @abc.abstractproperty + def user(self): + pass + + @abc.abstractmethod + def set_user(self, value): + pass + + @abc.abstractproperty + def content(self): + pass + + @abc.abstractmethod + def set_content(self, value): + pass + + +@six.add_metaclass(abc.ABCMeta) +class BackendCommentCollection(backends.BackendCollection[BackendComment]): + """The collection of Comment entries.""" + + ENTITY_CLASS = BackendComment + + @abc.abstractmethod + def create(self, node, user, content=None): + """ + Create a Comment for a given node and user + + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + pass + + @abc.abstractmethod + def delete(self, comment): + """ + Remove a Comment from the collection with the given id + + :param comment: the id of the comment to delete + """ + pass + + @abc.abstractmethod + def get(self, comment): + """ + Return a Comment given its id + + :param comment: the id of the comment to retrieve + :return: the comment + :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 diff --git a/aiida/orm/implementation/django/backend.py b/aiida/orm/implementation/django/backend.py index 0f0455bd50..c1a2c40a70 100644 --- a/aiida/orm/implementation/django/backend.py +++ b/aiida/orm/implementation/django/backend.py @@ -14,35 +14,35 @@ from aiida.orm.implementation.backends import Backend from aiida.backends.djsite.queries import DjangoQueryManager from . import authinfo +from . import comment from . import computer from . import groups from . import log from . import querybuilder from . import users +__all__ = ('DjangoBackend',) + class DjangoBackend(Backend): def __init__(self): - self._logs = log.DjangoLogCollection(self) - self._users = users.DjangoUserCollection(self) self._authinfos = authinfo.DjangoAuthInfoCollection(self) + self._comments = comment.DjangoCommentCollection(self) self._computers = computer.DjangoComputerCollection(self) self._groups = groups.DjangoGroupCollection(self) + self._logs = log.DjangoLogCollection(self) self._query_manager = DjangoQueryManager(self) - - @property - def logs(self): - return self._logs - - @property - def users(self): - return self._users + self._users = users.DjangoUserCollection(self) @property def authinfos(self): return self._authinfos + @property + def comments(self): + return self._comments + @property def computers(self): return self._computers @@ -51,6 +51,10 @@ def computers(self): def groups(self): return self._groups + @property + def logs(self): + return self._logs + @property def query_manager(self): return self._query_manager @@ -58,3 +62,6 @@ def query_manager(self): def query(self): return querybuilder.DjangoQueryBuilder(self) + @property + def users(self): + return self._users diff --git a/aiida/orm/implementation/django/comment.py b/aiida/orm/implementation/django/comment.py index a6934a6463..4105e3c3ce 100644 --- a/aiida/orm/implementation/django/comment.py +++ b/aiida/orm/implementation/django/comment.py @@ -7,126 +7,132 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### - +"""Django implementations for the Comment entity and collection.""" from __future__ import division from __future__ import print_function from __future__ import absolute_import -from functools import reduce import six -from aiida.backends.djsite.db.models import DbComment -from aiida.orm.implementation.general.comment import AbstractComment -from aiida.common.exceptions import NotExistent, MultipleObjectsError - - -class Comment(AbstractComment): - - def __init__(self, **kwargs): - - # If no arguments are passed, then create a new DbComment - if not kwargs: - self.dbcomment = DbComment() - - # If a DbComment is passed as argument. Just use it and - # wrap it with a Comment object - elif 'dbcomment' in kwargs: - # When a dbcomment is passed as argument, then no other arguments - # should be passed. - if len(kwargs) > 1: - raise ValueError("When a DbComment is passed as argument, no" - "further arguments are accepted.") - dbcomment = kwargs.pop('dbcomment') - if not isinstance(dbcomment, DbComment): - raise ValueError("Expected a DbComment. Object of a different" - "class was given as argument.") - self.dbcomment = dbcomment - - else: - id = kwargs.pop('id', None) - if id is None: - id = kwargs.pop('pk', None) - user = kwargs.pop('user', None) - dbnode = kwargs.pop('dbnode', None) - - # Constructing the default query - import operator - from django.db.models import Q - query_list = [] - - # If an id is specified then we add it to the query - if id is not None: - query_list.append(Q(pk=id)) - - # If a user is specified then we add it to the query - if user is not None: - query_list.append(Q(user=user)) - - # If a dbnode is specified then we add it to the query - if dbnode is not None: - query_list.append(Q(dbnode=dbnode)) - - res = DbComment.objects.filter(reduce(operator.and_, query_list)) - ccount = len(res) - if ccount > 1: - raise MultipleObjectsError( - "The arguments that you specified were too vague. More " - "than one comments with this data were found in the " - "database") - elif ccount == 0: - raise NotExistent("No comments were found with the given " - "arguments") - - self.dbcomment = res[0] +from aiida.backends.djsite.db import models +from aiida.common import exceptions +from aiida.common import utils - @property - def pk(self): - return self.dbnode.pk +from ..comments import BackendComment, BackendCommentCollection +from .utils import ModelWrapper +from . import entities +from . import users - @property - def id(self): - return self.dbnode.pk - @property - def to_be_stored(self): - return self.dbcomment.pk is None +class DjangoComment(entities.DjangoModelEntity[models.DbComment], BackendComment): + """Comment implementation for Django.""" - @property - def uuid(self): - return six.text_type(self.dbcomment.uuid) + MODEL_CLASS = models.DbComment + _auto_flush = ('mtime',) - def get_ctime(self): - return self.dbcomment.ctime + def __init__(self, backend, node, user, content=None): + """ + Construct a DjangoComment. - def set_ctime(self, val): - self.dbcomment.ctime = val - if not self.to_be_stored: - self.dbcomment.save() + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + super(DjangoComment, self).__init__(backend) + utils.type_check(user, users.DjangoUser) # pylint: disable=no-member + self._dbmodel = ModelWrapper( + models.DbComment(dbnode=node.dbnode, user=user.dbmodel, content=content), auto_flush=self._auto_flush) - def get_mtime(self): - return self.dbcomment.mtime + def store(self): + """Can only store if both the node and user are stored as well.""" + if self._dbmodel.dbnode.id is None or self._dbmodel.user.id is None: + raise exceptions.ModificationNotAllowed('The corresponding node and/or user are not stored') - def set_mtime(self, val): - self.dbcomment.mtime = val - if not self.to_be_stored: - self.dbcomment.save() + super(DjangoComment, self).store() - def get_user(self): - return self.dbcomment.user + @property + def uuid(self): + return six.text_type(self._dbmodel.uuid) - def set_user(self, val): - self.dbcomment.user = val - if not self.to_be_stored: - self.dbcomment.save() + @property + def ctime(self): + return self._dbmodel.ctime - def get_content(self): - return self.dbcomment.content + @property + def mtime(self): + return self._dbmodel.mtime - def set_content(self, val): - self.dbcomment.content = val - if not self.to_be_stored: - self.dbcomment.save() + def set_mtime(self, value): + self._dbmodel.mtime = value - def delete(self): - self.dbcomment.delete() + @property + def node(self): + from aiida.orm import Node + return Node(dbnode=self._dbmodel.dbnode) + @property + def user(self): + return self._backend.users.from_dbmodel(self._dbmodel.user) + + def set_user(self, value): + self._dbmodel.user = value + + @property + def content(self): + return self._dbmodel.content + + def set_content(self, value): + self._dbmodel.content = value + # self._dbmodel.save() + + +class DjangoCommentCollection(BackendCommentCollection): + """Django implementation for the CommentCollection.""" + + def from_dbmodel(self, dbmodel): + return DjangoComment.from_dbmodel(dbmodel, self.backend) + + def create(self, node, user, content=None): + """ + Create a Comment for a given node and user + + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + return DjangoComment(self.backend, node, user, content) + + def delete(self, comment): + """ + Remove a Comment from the collection with the given id + + :param comment: the id of the comment to delete + """ + # pylint: disable=no-name-in-module,import-error + from django.core.exceptions import ObjectDoesNotExist + try: + models.DbComment.objects.get(pk=comment).delete() + except ObjectDoesNotExist: + raise exceptions.NotExistent("Comment with id '{}' not found".format(comment)) + + def get(self, comment): + """ + Return a Comment given its id + + :param comment: the id of the comment to retrieve + :return: the comment + :raise NotExistent: if the comment with the given id does not exist + :raise MultipleObjectsError: if the id cannot be uniquely resolved to a comment + """ + # pylint: disable=no-name-in-module,import-error + from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned + + try: + comment = models.DbComment.objects.get(id=comment) + return self.from_dbmodel(comment) + except ObjectDoesNotExist: + raise exceptions.NotExistent('Could not find Comment<{}>'.format(comment)) + except MultipleObjectsReturned: + raise exceptions.MultipleObjectsError('Found multiple Comments for pk<{}>'.format(comment)) diff --git a/aiida/orm/implementation/django/entities.py b/aiida/orm/implementation/django/entities.py index 701de1c1a9..9966ae4aac 100644 --- a/aiida/orm/implementation/django/entities.py +++ b/aiida/orm/implementation/django/entities.py @@ -26,6 +26,7 @@ class DjangoModelEntity(typing.Generic[ModelType]): MODEL_CLASS = None _dbmodel = None + _auto_flush = () @classmethod def _class_check(cls): @@ -45,7 +46,7 @@ def from_dbmodel(cls, dbmodel, backend): type_check(dbmodel, cls.MODEL_CLASS) entity = cls.__new__(cls) super(DjangoModelEntity, entity).__init__(backend) - entity._dbmodel = utils.ModelWrapper(dbmodel) # pylint: disable=protected-access + entity._dbmodel = utils.ModelWrapper(dbmodel, auto_flush=cls._auto_flush) # pylint: disable=protected-access return entity @classmethod diff --git a/aiida/orm/implementation/django/node.py b/aiida/orm/implementation/django/node.py index 4b73ac7dfb..16d4520074 100644 --- a/aiida/orm/implementation/django/node.py +++ b/aiida/orm/implementation/django/node.py @@ -415,6 +415,7 @@ 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 @@ -428,9 +429,9 @@ def get_comment_obj(self, comment_id=None, user=None): dbcomments = DbComment.objects.filter( reduce(operator.and_, query_list)) comments = [] - from aiida.orm.implementation.django.comment import Comment + from aiida.orm.implementation.django.comment import DjangoComment for dbcomment in dbcomments: - comments.append(Comment(dbcomment=dbcomment)) + comments.append(DjangoComment.from_dbmodel(dbcomment, orm.construct_backend())) return comments def get_comments(self, pk=None): diff --git a/aiida/orm/implementation/django/users.py b/aiida/orm/implementation/django/users.py index 616eaca2c7..dd979f05af 100644 --- a/aiida/orm/implementation/django/users.py +++ b/aiida/orm/implementation/django/users.py @@ -20,7 +20,7 @@ from . import entities from . import utils -__all__ = 'DjangoUser', 'DjangoUserCollection' +__all__ = ('DjangoUser', 'DjangoUserCollection') class DjangoUserCollection(users.BackendUserCollection): diff --git a/aiida/orm/implementation/django/utils.py b/aiida/orm/implementation/django/utils.py index 25738c8cf6..fae8b39dcc 100644 --- a/aiida/orm/implementation/django/utils.py +++ b/aiida/orm/implementation/django/utils.py @@ -25,10 +25,17 @@ class ModelWrapper(object): * after ever write the updated value is flushed to the database. """ - def __init__(self, model): + def __init__(self, model, auto_flush=()): + """Construct the ModelWrapper. + + :param model: the database model instance to wrap + :param auto_flush: an optional tuple of database model fields that are always to be flushed, in addition to + the field that corresponds to the attribute being set through `__setattr__`. + """ super(ModelWrapper, self).__init__() # Have to do it this way because we overwrite __setattr__ object.__setattr__(self, '_model', model) + object.__setattr__(self, '_auto_flush', auto_flush) def __getattr__(self, item): if self.is_saved() and self._is_model_field(item): @@ -39,7 +46,8 @@ def __getattr__(self, item): def __setattr__(self, key, value): setattr(self._model, key, value) if self.is_saved() and self._is_model_field(key): - self._flush(fields=(key,)) + fields = set((key,) + self._auto_flush) + self._flush(fields=fields) def is_saved(self): return self._model.pk is not None @@ -82,4 +90,3 @@ def _ensure_model_uptodate(self, fields=None): new_model = self._model.__class__.objects.get(pk=self._model.pk) # Have to save this way so we don't hit the __setattr__ above object.__setattr__(self, '_model', new_model) - diff --git a/aiida/orm/implementation/general/comment.py b/aiida/orm/implementation/general/comment.py deleted file mode 100644 index 11328aed1c..0000000000 --- a/aiida/orm/implementation/general/comment.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -########################################################################### -# Copyright (c), The AiiDA team. All rights reserved. # -# This file is part of the AiiDA code. # -# # -# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core # -# For further information on the license, see the LICENSE.txt file # -# For further information please visit http://www.aiida.net # -########################################################################### -from __future__ import division -from __future__ import print_function -from __future__ import absolute_import -import logging -from abc import abstractmethod, abstractproperty, ABCMeta - -import six - - -@six.add_metaclass(ABCMeta) -class AbstractComment(object): - _logger = logging.getLogger(__name__) - - @abstractmethod - def __init__(self, **kwargs): - pass - - @abstractproperty - def pk(self): - pass - - @abstractproperty - def id(self): - pass - - @abstractproperty - def to_be_stored(self): - pass - - @abstractproperty - def uuid(self): - pass - - @abstractmethod - def get_ctime(self): - pass - - @abstractmethod - def set_ctime(self, val): - pass - - @abstractmethod - def get_mtime(self): - pass - - @abstractmethod - def set_mtime(self, val): - pass - - @abstractmethod - def get_user(self): - pass - - @abstractmethod - def set_user(self, val): - pass - - @abstractmethod - def get_content(self): - pass - - @abstractmethod - def set_content(self, val): - pass - - @abstractmethod - def delete(self): - pass diff --git a/aiida/orm/implementation/sqlalchemy/backend.py b/aiida/orm/implementation/sqlalchemy/backend.py index 9f6f8dadac..953998c64f 100644 --- a/aiida/orm/implementation/sqlalchemy/backend.py +++ b/aiida/orm/implementation/sqlalchemy/backend.py @@ -10,14 +10,15 @@ from __future__ import division from __future__ import print_function from __future__ import absolute_import -from aiida.orm.implementation.backends import Backend +from aiida.orm.implementation.backends import Backend from aiida.backends.sqlalchemy.queries import SqlaQueryManager from . import authinfo +from . import comment from . import computer +from . import groups from . import log from . import querybuilder -from . import groups from . import users __all__ = ('SqlaBackend',) @@ -26,25 +27,22 @@ class SqlaBackend(Backend): def __init__(self): - self._logs = log.SqlaLogCollection(self) - self._users = users.SqlaUserCollection(self) self._authinfos = authinfo.SqlaAuthInfoCollection(self) - self._groups = groups.SqlaGroupCollection(self) + self._comments = comment.SqlaCommentCollection(self) self._computers = computer.SqlaComputerCollection(self) + self._groups = groups.SqlaGroupCollection(self) + self._logs = log.SqlaLogCollection(self) self._query_manager = SqlaQueryManager(self) - - @property - def logs(self): - return self._logs - - @property - def users(self): - return self._users + self._users = users.SqlaUserCollection(self) @property def authinfos(self): return self._authinfos + @property + def comments(self): + return self._comments + @property def computers(self): return self._computers @@ -53,9 +51,17 @@ def computers(self): def groups(self): return self._groups + @property + def logs(self): + return self._logs + @property def query_manager(self): return self._query_manager def query(self): return querybuilder.SqlaQueryBuilder(self) + + @property + def users(self): + return self._users diff --git a/aiida/orm/implementation/sqlalchemy/comment.py b/aiida/orm/implementation/sqlalchemy/comment.py index 5e2f6d9537..5922a8615b 100644 --- a/aiida/orm/implementation/sqlalchemy/comment.py +++ b/aiida/orm/implementation/sqlalchemy/comment.py @@ -7,121 +7,136 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### - +"""Sqla implementations for the Comment entity and collection.""" from __future__ import division from __future__ import print_function from __future__ import absolute_import import six -from aiida.backends.sqlalchemy.models.comment import DbComment -from aiida.orm.implementation.general.comment import AbstractComment -from aiida.common.exceptions import NotExistent, MultipleObjectsError - - -class Comment(AbstractComment): - - def __init__(self, **kwargs): - - # If no arguments are passed, then create a new DbComment - if not kwargs: - self.dbcomment = DbComment() - - # If a DbComment is passed as argument. Just use it and - # wrap it with a Comment object - elif 'dbcomment' in kwargs: - # When a dbcomment is passed as argument, then no other arguments - # should be passed. - if len(kwargs) > 1: - raise ValueError("When a DbComment is passed as argument, no" - "further arguments are accepted.") - dbcomment = kwargs.pop('dbcomment') - if not isinstance(dbcomment, DbComment): - raise ValueError("Expected a DbComment. Object of a different" - "class was given as argument.") - self.dbcomment = dbcomment - - else: - id = kwargs.pop('id', None) - if id is None: - id = kwargs.pop('pk', None) - user = kwargs.pop('user', None) - dbnode = kwargs.pop('dbnode', None) - - # Constructing the default query - dbcomment_query = DbComment.query - - # If an id is specified then we add it to the query - if id is not None: - dbcomment_query = dbcomment_query.filter_by(id=id) - - # If a user is specified then we add it to the query - if user is not None: - dbcomment_query = dbcomment_query.filter_by(user=user) - - # If a dbnode is specified then we add it to the query - if dbnode is not None: - dbcomment_query = dbcomment_query.filter_by(dbnode=dbnode) - - ccount = dbcomment_query.count() - if ccount > 1: - raise MultipleObjectsError( - "The arguments that you specified were too vague. More " - "than one comments with this data were found in the " - "database") - elif ccount == 0: - raise NotExistent("No comments were found with the given " - "arguments") - - self.dbcomment = dbcomment_query.first() +from aiida.backends.sqlalchemy.models import comment as models +from aiida.common import exceptions +from aiida.common import utils - @property - def pk(self): - return self.dbcomment.id +from ..comments import BackendComment, BackendCommentCollection +from .utils import ModelWrapper +from . import entities +from . import users - @property - def id(self): - return self.dbcomment.id - @property - def to_be_stored(self): - return self.dbcomment.id is None +class SqlaComment(entities.SqlaModelEntity[models.DbComment], BackendComment): + """Comment implementation for Sqla.""" - @property - def uuid(self): - return six.text_type(self.dbcomment.uuid) + MODEL_CLASS = models.DbComment + + def __init__(self, backend, node, user, content=None): + """ + Construct a SqlaComment. + + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + super(SqlaComment, self).__init__(backend) + utils.type_check(user, users.SqlaUser) # pylint: disable=no-member + self._dbmodel = ModelWrapper(models.DbComment(dbnode=node.dbnode, user=user.dbmodel, content=content)) - def get_ctime(self): - return self.dbcomment.ctime + def store(self): + """Can only store if both the node and user are stored as well.""" + if self._dbmodel.dbnode.id is None or self._dbmodel.user.id is None: + self._dbmodel.dbnode = None + raise exceptions.ModificationNotAllowed('The corresponding node and/or user are not stored') - def set_ctime(self, val): - self.dbcomment.ctime = val - if not self.to_be_stored: - self.dbcomment.save() + super(SqlaComment, self).store() - def get_mtime(self): - return self.dbcomment.mtime + @property + def uuid(self): + return six.text_type(self._dbmodel.uuid) + + @property + def ctime(self): + return self._dbmodel.ctime - def set_mtime(self, val): - self.dbcomment.mtime = val - if not self.to_be_stored: - self.dbcomment.save() + @property + def mtime(self): + return self._dbmodel.mtime - def get_user(self): - return self.dbcomment.user + def set_mtime(self, value): + self._dbmodel.mtime = value - def set_user(self, val): - self.dbcomment.user = val - if not self.to_be_stored: - self.dbcomment.save() + @property + def node(self): + from aiida.orm import Node + return Node(dbnode=self._dbmodel.dbnode) - def get_content(self): - return self.dbcomment.content + @property + def user(self): + return self._backend.users.from_dbmodel(self._dbmodel.user) - def set_content(self, val): - self.dbcomment.content = val - if not self.to_be_stored: - self.dbcomment.save() + def set_user(self, value): + self._dbmodel.user = value - def delete(self): - self.dbcomment.delete() + @property + def content(self): + return self._dbmodel.content + + def set_content(self, value): + self._dbmodel.content = value + + +class SqlaCommentCollection(BackendCommentCollection): + """SqlAlchemy implementation for the CommentCollection.""" + + def from_dbmodel(self, dbmodel): + return SqlaComment.from_dbmodel(dbmodel, self.backend) + + def create(self, node, user, content=None): + """ + Create a Comment for a given node and user + + :param node: a Node instance + :param user: a User instance + :param content: the comment content + :return: a Comment object associated to the given node and user + """ + return SqlaComment(self.backend, node, user, content) + + def delete(self, comment): + """ + Remove a Comment from the collection with the given id + + :param comment: the id of the comment to delete + """ + # pylint: disable=no-name-in-module,import-error + from sqlalchemy.orm.exc import NoResultFound + from aiida.backends.sqlalchemy import get_scoped_session + session = get_scoped_session() + + try: + session.query(models.DbComment).filter_by(id=comment).delete() + session.commit() + except NoResultFound: + raise exceptions.NotExistent("Comment with id '{}' not found".format(comment)) + + def get(self, comment): + """ + Return a Comment given its id + + :param comment: the id of the comment to retrieve + :return: the comment + :raise NotExistent: if the comment with the given id does not exist + :raise MultipleObjectsError: if the id cannot be uniquely resolved to a comment + """ + # pylint: disable=no-name-in-module,import-error + from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound + from aiida.backends.sqlalchemy import get_scoped_session + session = get_scoped_session() + + try: + comment = session.query(models.DbComment).filter_by(id=comment).one() + return self.from_dbmodel(comment) + except NoResultFound: + raise exceptions.NotExistent('Could not find Comment<{}>'.format(comment)) + except MultipleResultsFound: + raise exceptions.MultipleObjectsError('Found multiple Comments for pk<{}>'.format(comment)) diff --git a/aiida/orm/implementation/sqlalchemy/node.py b/aiida/orm/implementation/sqlalchemy/node.py index 6eb640aa26..35897688ce 100644 --- a/aiida/orm/implementation/sqlalchemy/node.py +++ b/aiida/orm/implementation/sqlalchemy/node.py @@ -485,9 +485,9 @@ def get_comment_obj(self, comment_id=None, user=None): dbcomments = query.all() comments = [] - from aiida.orm.implementation.sqlalchemy.comment import Comment + from aiida.orm.implementation.sqlalchemy.comment import SqlaComment for dbcomment in dbcomments: - comments.append(Comment(dbcomment=dbcomment)) + comments.append(SqlaComment.from_dbmodel(dbcomment, orm.construct_backend())) return comments def get_comments(self, pk=None): diff --git a/aiida/orm/implementation/sqlalchemy/users.py b/aiida/orm/implementation/sqlalchemy/users.py index 7ed4a0ba47..c8ca6e081c 100644 --- a/aiida/orm/implementation/sqlalchemy/users.py +++ b/aiida/orm/implementation/sqlalchemy/users.py @@ -16,7 +16,7 @@ from . import entities from . import utils -__all__ = 'SqlaUserCollection', 'SqlaUser' +__all__ = ('SqlaUserCollection', 'SqlaUser') class SqlaUserCollection(users.BackendUserCollection): diff --git a/aiida/orm/implementation/sqlalchemy/utils.py b/aiida/orm/implementation/sqlalchemy/utils.py index 7d072bba48..501b97e56e 100644 --- a/aiida/orm/implementation/sqlalchemy/utils.py +++ b/aiida/orm/implementation/sqlalchemy/utils.py @@ -34,10 +34,17 @@ class ModelWrapper(object): * after ever write the updated value is flushed to the database. """ - def __init__(self, model): + def __init__(self, model, auto_flush=()): + """Construct the ModelWrapper. + + :param model: the database model instance to wrap + :param auto_flush: an optional tuple of database model fields that are always to be flushed, in addition to + the field that corresponds to the attribute being set through `__setattr__`. + """ super(ModelWrapper, self).__init__() # Have to do it this way because we overwrite __setattr__ object.__setattr__(self, '_model', model) + object.__setattr__(self, '_auto_flush', auto_flush) def __getattr__(self, item): # Python 3's implementation of copy.copy does not call __init__ on the new object @@ -54,7 +61,8 @@ def __getattr__(self, item): def __setattr__(self, key, value): setattr(self._model, key, value) if self.is_saved() and self._is_model_field(key): - self._flush(fields=(key,)) + fields = set((key,) + self._auto_flush) + self._flush(fields=fields) def is_saved(self): return self._model.id is not None From f5406dd13a833aa80fbd0a5deeca25c0cef7c48a Mon Sep 17 00:00:00 2001 From: Sebastiaan Huber Date: Wed, 21 Nov 2018 17:37:45 +0100 Subject: [PATCH 2/2] 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``.