Skip to content

Instantly share code, notes, and snippets.

@sbernatsky
Last active March 21, 2022 15:19
Show Gist options
  • Save sbernatsky/8a2d40b0bf8a1f26e633aae1276b2054 to your computer and use it in GitHub Desktop.
Save sbernatsky/8a2d40b0bf8a1f26e633aae1276b2054 to your computer and use it in GitHub Desktop.

Preparation steps

Create TimeSpanProject template

  1. Enable TimeSpan aspect for Project under CoreAdmin -> Configuration -> Aspect Info.
  2. Create TimeSpan dynatype based on Project base type under CoreAdmin -> Configuration -> Dynatypes (use project_status from drop down as status list).
  3. Create TimeSpanProject project template template under CoreAdmin -> Templates.
  4. Remember template id for further usage in examples
  5. Ensure that Contract dynatype based on File is geristered and required aspects contain ContractDate and LetterOut (under CoreAdmin -> Configuration -> Dynatypes).

Get or generate api key for user

Coredata administrator may use following code snippet to get or generate key api via repl:

from core2.auth.models import ApiKey
from django.contrib.auth.models import User


def get_or_create_apikey(username):
    key = ApiKey.objects.filter(user__username=username).first()
    if key:
        return key.key
    else:
        user = User.objects.filter(username=username).all()[0]
        key = ApiKey(user=user)
        key.key = key.generate_key()
        key.save()
        return key.key


api_username = 'Administrator'
api_key = get_or_create_apikey(api_username)
print 'api key for %s: %s' % (api_username, api_key)

Find out ids for the space where projects should be created and fileplan

  1. Find required space id under CoreAdmin -> Spaces.
  2. Find required fileplan id under CoreAdmin -> Fileplans.

Run examples

Example below uses V2 api to perform following tasks:

  1. Creates new project using TimeSpan project template and fills in both fileplan and specific fields like all_day and start_time.
  2. Uploads Contract file into project and fills in specific fields (authors, date, valid_from and valid_to).
  3. Updates created project with new status, description and end time.
import requests
import urlparse

from django.utils.http import urlquote_plus
from datetime import datetime, timedelta


#####################################################################
# Base api support classes
#####################################################################

def create_apikey_v2_authenticator(username, key):
    """
    Apikey authentication for v2 api passes both username and key
    """
    def func():
        return 'apikey %s:%s' % (username, key)
    return func


class CoreDataConnectionError(Exception):
    pass


class CoreDataApi(object):
    def __init__(self, base_url, authenticator):
        self.base_url = base_url
        self.authenticator = authenticator
        self._session = requests.Session()

    def execute_request(
        self, method, url, json=None, data=None, files=None, headers=None,
        application='coredata'
    ):
        """
        :param method: Request method.
        :param url: Relative URL to make request to.
        :param json: Dictionary to be json-ified to send as request body.
        :param files: Dictionary of file-like-objects for multipart encoding
                      upload.
        :param headers: Dictionary of headers to be parsed with the request
        :return: Response object, json or location.
        """
        headers = headers or {
            'content-type': 'application/json',
        }
        headers.update({'Authorization': self.authenticator()})
        url = self.base_url + url

        response = None
        try:
            response = self._session.request(
                method, url, json=json, data=data, files=files,
                headers=headers, allow_redirects=False)
            response.raise_for_status()
        except (requests.ConnectionError, requests.HTTPError) as e:
            if response is not None:
                raise CoreDataConnectionError(response.content)
            else:
                raise CoreDataConnectionError(e)
        return response


class CoreDataV2(object):

    def __init__(self, base_url, authenticator):
        base_url = urlparse.urljoin(base_url, '/api/v2')
        self.coredata_api = CoreDataApi(base_url, authenticator)

    def create_project(self, json):
        response = self.coredata_api.execute_request(
            'POST', '/projects/', json=json)
        # Location header contains path to the project:
        # /api/v2/projects/947a6c82-507b-11e9-aa3e-df012f56c42f/
        location = response.headers['Location']
        return location[17:-1]

    def update_project(self, project_id, json):
        url = '/projects/{}/'.format(project_id)
        self.coredata_api.execute_request('PUT', url, json=json)

    def get_projects(self):
        return self.coredata_api.execute_request('GET', '/projects/').json()

    def get_project(self, project_id):
        url = '/projects/{}/'.format(project_id)
        return self.coredata_api.execute_request('GET', url).json()

    def create_file(self, data, content):
        headers = {
            'Content-Disposition': u'attachment; filename={}'.format(
                urlquote_plus(data['filename'])
            ),
        }
        content.seek(0)
        response = self.coredata_api.execute_request(
            'POST',
            '/files/',
            headers=headers,
            data=data,
            files={
                'content': (data['filename'], content)
            }
        )
        # Location header contains path to the project:
        # /api/v2/files/947a6c82-507b-11e9-aa3e-df012f56c42f/
        location = response.headers['Location']
        return location[14:-1]

    def update_file(self, file_id, json):
        url = '/files/{}/'.format(file_id)
        self.coredata_api.execute_request('PUT', url, json=json)

    def get_spaces(self):
        return self.coredata_api.execute_request('GET', '/spaces/').json()

    def get_space(self, space_id):
        url = '/spaces/{}/'.format(space_id)
        return self.coredata_api.execute_request('GET', url).json()

    def get_space_projects(self, space_id):
        url = '/spaces/{}/projects/'.format(space_id)
        return self.coredata_api.execute_request('GET', url).json()

    def get_space_project_templates(self, space_id):
        url = '/spaces/{}/projects/templates/'.format(space_id)
        return self.coredata_api.execute_request('GET', url).json()

    def get_space_fileplans(self, space_id):
        """
        Returns list of dictionaries with attributes: 'id', 'title'
        """
        url = '/spaces/{}/fileplans/'.format(space_id)
        return self.coredata_api.execute_request('GET', url).json()


#####################################################################
# Example functions
#####################################################################

def create_new_timespan_project(
        coredata, template_id, space_id, fileplan_id, responsible_user):
    """
    Creates new project using TimeSpan template and populates timespan aspect fields.
    Please note how start_time/end_time fields are populated.
    """
    start_datetime = datetime.now() - timedelta(hours=1)
    start_date = start_datetime.strftime('%d-%m-%Y')
    start_time = start_datetime.strftime('%H:%M')
    json = {
        'description': 'This is sample project',
        'space': space_id,
        'title': 'Sample Project (TimeSpan)',
        'fileplan': fileplan_id,
        'responsible_users': [responsible_user],
        'template': template_id,
        'timespan__all_day': 'false',
        'timespan__start_time': [start_date, start_time],
    }

    created_project_id = coredata.create_project(json)
    return created_project_id


def upload_contract_file(coredata, project_id, filename, author):
    """
    Creates Contract file in coredata by specifying dynatype.
    Api v2 does not support creating files with dynatype and setting aspect
    values in one request.
    That's why Contract file is created first and only after that aspect
    values are updated.
    """
    with open(filename, 'rb') as file:
        created_file_id = coredata.create_file(
            data={
                'filename': filename,
                'project': project_id,
                'title': 'Contract for %s' % filename,
                'dynatype': 'Contract',
            },
            content=file
        )

    now = datetime.now().strftime('%Y-%m-%d')
    yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
    month = (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d')
    aspect_data = {
        'letterout__authors': [author],
        'letterout__date': now,
        'contractdate__valid_from': yesterday,
        'contractdate__valid_to': month,
    }
    coredata.update_file(created_file_id, aspect_data)

    return created_file_id


def update_project(coredata, project_id, status):
    """
    Updates project description, title, status and end time
    Aspects values are fully replaced => previous values are required
    """
    project = coredata.get_project(project_id)

    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    now_date = datetime.now().strftime('%d-%m-%Y')
    now_time = datetime.now().strftime('%H:%M')
    title = project['title']
    timespan__all_day = str(project['aspects']['timespan']['all_day']).lower()
    timespan__start_time = datetime.strptime(
        project['aspects']['timespan']['start_time'],
        '%Y-%m-%dT%H:%M:%S')
    json = {
        'description': 'This project was updated at %s. More info: http://google.com' % now,
        'title': '%s [%s]' % (title, now),
        'status': status,
        'timespan__all_day': timespan__all_day,
        'timespan__start_time': [timespan__start_time.strftime('%d-%m-%Y'),
                                 timespan__start_time.strftime('%H:%M')],
        'timespan__end_time': [now_date, now_time],
    }
    coredata.update_project(project_id, json)


#####################################################################
# V2 Example
#####################################################################

# External configuration parameters
api_username = 'Administrator'
api_key = '4fb69e4a8a39c1560c590da482373c732ba874b6'
space_id = 'e4dc5228-8b66-11e9-a2fd-9f4406f7b89f'
timespan_project_template_id = 'b33a7644-8b7c-11e9-8f19-c72291ab1e6b'
fileplan_id = 'be7f2472-8b6e-11e9-9d08-9f1e292e30b1'
# project status format is: '{status_list}:{status_id}:'
updated_project_status = 'project_status:In_progress:'

coredata_v2 = CoreDataV2(
    "http://127.0.0.1:8100",
    create_apikey_v2_authenticator(api_username, api_key))


print '*** Running sample project creation and file uploading'
print 'creating new timespan project...'
project_id = create_new_timespan_project(
    coredata_v2, timespan_project_template_id, space_id, fileplan_id,
    api_username)
print '  timespan project created. id: %s' % project_id

print 'uploading new contract...'
contract_id = upload_contract_file(
    coredata_v2, project_id, 'userlist.txt', api_username)
print '  new contract uploaded. id: %s' % contract_id

print 'updating project...'
update_project(coredata_v2, project_id, updated_project_status)
print '  project updated'

Api v2 pain points:

  • Cannot set aspect fields during creation call; needs additional document update call to set aspect fields (see upload_contract_file method);
  • Field format follows format defined by html forms (see how timespan end_time/start_time fields are populated in timespan project creation in create_new_timespan_project method);
  • Returned field values representation is not compatible with creation expected format (see how start_time is parsed/formatted in update_project method)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment