class D4S2Project(object):
    """
    Transfers a project to another user via share and final delivery.
    Uses eternal API to send messages.
    """
    def __init__(self, config, remote_store, print_func):
        """
        Setup for sharing a project and sending email on the D4S2 service.
        :param config: Config configuration specifying which remote_store to use.
        :param remote_store: RemoteStore remote store we will be sharing a project from
        :param print_func: func used to print output somewhere
        """
        self.config = config
        self.client = Client(config)
        auth = DataServiceAuth(self.config)
        api_token = auth.get_auth()
        self.api = D4S2Api(config.d4s2_url, api_token)
        self.remote_store = remote_store
        self.print_func = print_func

    def share(self, project, to_user, force_send, auth_role, user_message):
        """
        Send mail and give user specified access to the project.
        :param project: RemoteProject project to share
        :param to_user: RemoteUser user to receive email/access
        :param auth_role: str project role eg 'project_admin' to give to the user
        :param user_message: str message to be sent with the share
        :return: str email we share the project with
        """
        if self._is_current_user(to_user):
            raise ShareWithSelfError(SHARE_WITH_SELF_MESSAGE.format("share"))
        if not to_user.email:
            self._raise_user_missing_email_exception("share")
        self.set_user_project_permission(project, to_user, auth_role)
        return self._share_project(D4S2Api.SHARE_DESTINATION, project, to_user, force_send, auth_role, user_message)

    def set_user_project_permission(self, project, user, auth_role):
        """
        Give user access permissions for a project.
        :param project: RemoteProject project to update permissions on
        :param user: RemoteUser user to receive permissions
        :param auth_role: str project role eg 'project_admin'
        """
        self.remote_store.set_user_project_permission(project, user, auth_role)

    def deliver(self, project, new_project_name, to_user, share_users, force_send, path_filter, user_message):
        """
        Remove access to project_name for to_user, copy to new_project_name if not None,
        send message to service to email user so they can have access.
        :param project: RemoteProject pre-existing project to be delivered
        :param new_project_name: str name of non-existing project to copy project_name to, if None we don't copy
        :param to_user: RemoteUser user we are handing over the project to
        :param share_users: [RemoteUser] who will have project shared with them once to_user accepts the project
        :param force_send: boolean enables resending of email for existing projects
        :param path_filter: PathFilter: filters what files are shared
        :param user_message: str message to be sent with the share
        :return: str email we sent deliver to
        """
        if self._is_current_user(to_user):
            raise ShareWithSelfError(SHARE_WITH_SELF_MESSAGE.format("deliver"))
        if not to_user.email:
            self._raise_user_missing_email_exception("deliver")
        self.remove_user_permission(project, to_user)
        if new_project_name:
            project = self._copy_project(project, new_project_name, path_filter)
        return self._share_project(D4S2Api.DELIVER_DESTINATION, project, to_user,
                                   force_send, user_message=user_message, share_users=share_users)

    def remove_user_permission(self, project, user):
        """
        Take away user's access to project.
        :param project: RemoteProject project to remove permissions on
        :param user: RemoteUser user who should no longer have access
        """
        self.remote_store.revoke_user_project_permission(project, user)

    def _share_project(self, destination, project, to_user, force_send, auth_role='', user_message='',
                       share_users=None):
        """
        Send message to remote service to email/share project with to_user.
        :param destination: str which type of sharing we are doing (SHARE_DESTINATION or DELIVER_DESTINATION)
        :param project: RemoteProject project we are sharing
        :param to_user: RemoteUser user we are sharing with
        :param auth_role: str project role eg 'project_admin' email is customized based on this setting.
        :param user_message: str message to be sent with the share
        :param share_users: [RemoteUser] users to have this project shared with after delivery (delivery only)
        :return: the email the user should receive a message on soon
        """
        from_user = self.remote_store.get_current_user()
        share_user_ids = None
        if share_users:
            share_user_ids = [share_user.id for share_user in share_users]
        item = D4S2Item(destination=destination,
                        from_user_id=from_user.id,
                        to_user_id=to_user.id,
                        project_id=project.id,
                        project_name=project.name,
                        auth_role=auth_role,
                        user_message=user_message,
                        share_user_ids=share_user_ids)
        item.send(self.api, force_send)
        return to_user.email

    def _copy_project(self, project, new_project_name, path_filter):
        """
        Copy pre-existing project with name project_name to non-existing project new_project_name.
        :param project: remotestore.RemoteProject project to copy from
        :param new_project_name: str project to copy to
        :param path_filter: PathFilter: filters what files are shared
        :return: RemoteProject new project we copied data to
        """
        temp_directory = tempfile.mkdtemp()
        new_project_name_or_id = ProjectNameOrId.create_from_name(new_project_name)
        remote_project = self.remote_store.fetch_remote_project(new_project_name_or_id)
        if remote_project:
            raise DDSUserException("A project with name '{}' already exists.".format(new_project_name))
        activity = CopyActivity(self.remote_store.data_service, project, new_project_name)
        self._download_project(activity, project.id, temp_directory, path_filter)
        self._upload_project(activity, new_project_name, temp_directory)
        activity.finished()
        shutil.rmtree(temp_directory)
        return self.remote_store.fetch_remote_project(new_project_name_or_id, must_exist=True)

    def _download_project(self, activity, project_id, temp_directory, path_filter):
        """
        Download the project with project_name to temp_directory.
        :param activity: CopyActivity: info about the copy activity are downloading for
        :param project_id: uuid of the project to download
        :param temp_directory: str path to directory we can download into
        :param path_filter: PathFilter: filters what files are shared
        """
        project = self.client.get_project_by_id(project_id)
        self.print_func("Downloading a copy of '{}'.".format(project.name))

        downloader = ProjectFileDownloader(self.config, temp_directory, project, path_filter)
        downloader.run()

        downloaded_file_relations = DownloadedFileRelations(activity)
        for remote_path, dds_file in project.get_path_to_files().items():
            if path_filter.include_path(remote_path):
                downloaded_file_relations.add(self.remote_store.data_service,
                                              remote_path,
                                              dds_file.current_version['id'])

    def _upload_project(self, activity, project_name, temp_directory):
        """
        Upload the contents of temp_directory into project_name
        :param activity: CopyActivity: info about the copy activity are uploading for
        :param project_name: str project name we will upload files to
        :param temp_directory: str path to directory who's files we will upload
        """
        self.print_func("Uploading to '{}'.".format(project_name))
        paths_to_send = [os.path.join(temp_directory, item) for item in os.listdir(os.path.abspath(temp_directory))]
        project_name_or_id = ProjectNameOrId.create_from_name(project_name)

        project_upload = ProjectUpload.create_for_paths(self.config, self.remote_store, project_name_or_id,
                                                        paths_to_send,
                                                        file_upload_post_processor=UploadedFileRelations(activity))
        project_upload.run()

    def _is_current_user(self, some_user):
        """
        Is the specified user the current user?
        :param some_user: RemoteUser user we want to check against the current user
        :return: boolean: True if the current user is the passed in user
        """
        current_user = self.remote_store.get_current_user()
        return current_user.id == some_user.id

    @staticmethod
    def _raise_user_missing_email_exception(cmd):
        msg = USER_WITHOUT_EMAIL_MESSAGE.format(cmd, cmd)
        raise UserMissingEmailError(msg) class CopyActivity(object):
    def __init__(self, data_service, project, new_project_name):
        """
        Create an activity for our copy operation so users can trace back where the copied files came from.
        :param data_service: DataServiceApi: service used to create the activity
        :param project: RemoteProject project name we will download files from
        :param new_project_name: str project name we will upload files into
        """
        self.data_service = data_service
        self.name = "DukeDSClient copying project: {}".format(project.name)
        self.desc = "Copying {} to project {} using DukeDSClient{}".format(project.name, new_project_name,
                                                                           get_internal_version_str())
        self.started = self._current_timestamp_str()
        result = data_service.create_activity(self.name, self.desc, started_on=self.started)
        self.id = result.json()['id']
        self.remote_path_to_file_version_id = {}

    def finished(self):
        """
        Mark the activity as finished
        """
        self.data_service.update_activity(self.id, self.name, self.desc,
                                          started_on=self.started,
                                          ended_on=self._current_timestamp_str())

    @staticmethod
    def _current_timestamp_str():
        return datetime.datetime.now(pytz.utc).isoformat() class DownloadedFileRelations(object):
    """
    Contains run method that will be called via project download file pre-processor.
    """
    def __init__(self, activity):
        """
        :param activity: CopyActivity: info about the activity associated with the files we are downloading
        """
        self.activity = activity

    def add(self, data_service, remote_path, file_version_id):
        """
        Attach a remote file to activity with used relationship.
        """
        data_service.create_used_relation(self.activity.id, KindType.file_str, file_version_id)
        self.activity.remote_path_to_file_version_id[remote_path] = file_version_id class UploadedFileRelations(object):
    """
    Contains run method that will be called via project upload file post-processor.
    """
    def __init__(self, activity):
        """
        :param activity: CopyActivity: info about the activity associated with the files we are uploading
        """
        self.activity = activity

    def run(self, data_service, file_details):
        """
        Attach a remote file to activity with was generated by relationship.
        :param data_service: DataServiceApi: service used to attach relationship
        :param file_details: dict: response from DukeDS POST to /files/ containing current_version id
        """
        file_version_id = file_details['current_version']['id']
        data_service.create_was_generated_by_relation(self.activity.id, KindType.file_str, file_version_id)
        used_entity_id = self._lookup_used_entity_id(file_details)
        data_service.create_was_derived_from_relation(used_entity_id, KindType.file_str,
                                                      file_version_id, KindType.file_str)

    def _lookup_used_entity_id(self, file_details):
        """
        Return the file_version_id associated with the path from file_details.
        The file_version_id is looked up from a dictionary in the activity.
        :param file_details: dict: response from DukeDS POST to /files/
        :return: str: file_version_id uuid
        """
        # Since this uses the response from POST to /files/ this will include the ancestors and not be
        # effected by exclude_response_fields that were used when listing the project
        name_parts = [ancestor['name'] for ancestor in file_details['ancestors']
                      if ancestor['kind'] == KindType.folder_str]
        name_parts.append(file_details['name'])
        remote_path = RemotePath.add_leading_slash(os.sep.join(name_parts))
        return self.activity.remote_path_to_file_version_id[remote_path] class ShareWithSelfError(DDSUserException):
    """
    Error raised whe user attempts to share/deliver a project just themselves
    """
    def __init__(self, message):
        """
        :param message: str reason for the error
        """
        Exception.__init__(self, message) SHARE_WITH_SELF_MESSAGE = """
ERROR: You cannot {} a project to yourself.

""" class UserMissingEmailError(DDSUserException):
    """
    Raised when attempting to deliver or share with a DukeDS user that has a null email
    """
    def __init__(self, message):
        """
        :param message: str reason for the error
        """
        Exception.__init__(self, message) USER_WITHOUT_EMAIL_MESSAGE = """
ERROR: The user you are trying to {} to has no email setup in DukeDS.
We are unable to contact them to {} your project.

""" class KindType(object):
    """
    The types of items that are part of a project. Strings are from the duke-data-service.
    """
    file_str = 'dds-file'
    folder_str = 'dds-folder'
    project_str = 'dds-project'

    @staticmethod
    def is_file(item):
        return item.kind == KindType.file_str

    @staticmethod
    def is_folder(item):
        return item.kind == KindType.folder_str

    @staticmethod
    def is_project(item):
        return item.kind == KindType.project_str class PathFilter(object):
    """
    Represents a list of path filters that allows a path to be checked via include_path method.
    By default all paths return True. Specify the list to include or exclude by calling set_include_paths or
    set_exclude_paths. Returns which paths have been seen via get_unused_paths method.
    """
    def __init__(self, include_paths, exclude_paths):
        """
        Creates a path filter based on either include_paths or exclude_paths if both are filled in raises error.
        :param include_paths: [str]: list of paths that should be included
        :param exclude_paths: [str]: list of paths that should be excluded
        """
        if include_paths and exclude_paths:
            raise ValueError("Programming Error: Should not specify both include_path and exclude_paths")

        path_filter = IncludeAll()
        if include_paths:
            path_filter = IncludeFilter(include_paths)
        elif exclude_paths:
            path_filter = ExcludeFilter(exclude_paths)

        self.filter = path_filter
        self.seen_paths = set()

    def include_path(self, path):
        """
        Should this path be included based on the include_paths or exclude_paths.
        Keeps track of paths seen to allow finding unused filters.
        :param path: str: remote path to be filtered
        :return: bool: True if we should include the path
        """
        self.seen_paths.add(path)
        return self.filter.include(path)

    def reset_seen_paths(self):
        """
        Clear list of paths seen via include_path method.
        """
        self.seen_paths = set()

    def get_unused_paths(self):
        """
        Returns which include_paths or exclude_paths that were not used via include_path method.
        :return: [str] list of filtering paths that were not used.
        """
        return [path for path in self.filter.paths if path not in self.seen_paths]
from unittest import TestCase
from ddsc.core.d4s2 import D4S2Project, CopyActivity, DownloadedFileRelations, UploadedFileRelations, \
    ShareWithSelfError, SHARE_WITH_SELF_MESSAGE, UserMissingEmailError, USER_WITHOUT_EMAIL_MESSAGE
from ddsc.core.util import KindType
from ddsc.core.pathfilter import PathFilter
from mock import patch, MagicMock, Mock
from __future__ import absolute_import


class TestD4S2Project(TestCase):
    @patch('ddsc.core.d4s2.D4S2Api')
    @patch('ddsc.core.d4s2.requests')
    def test_share(self, mock_requests, mock_d4s2api):
        mock_d4s2api().get_existing_item.return_value = Mock(json=Mock(return_value=[]))
        project = D4S2Project(config=MagicMock(), remote_store=MagicMock(), print_func=MagicMock())
        project.share(project=Mock(name='mouserna'),
                      to_user=MagicMock(id='123'),
                      force_send=False,
                      auth_role='project_viewer',
                      user_message='This is a test.')
        args, kwargs = mock_d4s2api().create_item.call_args
        item = args[0]
        self.assertEqual(mock_d4s2api.SHARE_DESTINATION, item.destination)
        self.assertEqual('123', item.to_user_id)
        self.assertEqual('project_viewer', item.auth_role)
        self.assertEqual('This is a test.', item.user_message)
        mock_d4s2api().send_item.assert_called()

    @patch('ddsc.core.d4s2.D4S2Api')
    @patch('ddsc.core.d4s2.requests')
    def test_share_with_self(self, mock_requests, mock_d4s2api):
        mock_d4s2api.return_value.get_existing_item.return_value = Mock(json=Mock(return_value=[]))
        mock_requests.get.return_value = None
        mock_remote_store = MagicMock()
        mock_current_user = MagicMock()
        mock_current_user.id = '123'
        mock_to_user = MagicMock()
        mock_to_user.id = '123'
        mock_remote_store.get_current_user.return_value = mock_current_user
        project = D4S2Project(config=MagicMock(), remote_store=mock_remote_store, print_func=MagicMock())
        with self.assertRaises(ShareWithSelfError) as raised_error:
            project.share(project=Mock(name='mouserna'),
                          to_user=mock_to_user,
                          force_send=False,
                          auth_role='project_viewer',
                          user_message='This is a test.')
        self.assertEqual(str(raised_error.exception), SHARE_WITH_SELF_MESSAGE.format('share'))

    @patch('ddsc.core.d4s2.D4S2Api')
    @patch('ddsc.core.d4s2.requests')
    def test_share_to_bad_email(self, mock_requests, mock_d4s2api):
        mock_d4s2api().get_existing_item.return_value = Mock(json=Mock(return_value=[]))
        project = D4S2Project(config=MagicMock(), remote_store=MagicMock(), print_func=MagicMock())

        with self.assertRaises(UserMissingEmailError) as raised_error: