Join 34,000+ subscribers and receive articles from our blog about software quality, testing, QA and security.
 

Having Dofficulty Customising YouTrack Defect Plugin


#1

Hello everyone,

I am following the example in the Support Documentation that takes you through the steps necessary to add a custom field from Test Rails to the Defect Push Dialog. I have created a Custom Field called ‘Configuration’ of type String, in YouTrack to match the new ‘config’ field in the Defect Push Dialog. However, the text entered in this field does not populate “Configuration” in YouTrack. Any help resolving this will be greatly appreciated. I have attached the contents of the customised defect plugin.

[code]<?php if (!defined('ROOTPATH')) exit('No direct script access allowed'); ?>

<?php /** * YouTrack Defect Plugin for TestRail * * Copyright Gurock Software GmbH. All rights reserved. * * This is the TestRail defect plugin for JetBrains YouTrack. Please * see http://docs.gurock.com/testrail-integration/defects-plugins * for more information about TestRail's defect plugins. * * http://www.gurock.com/testrail/ */ class YouTrack_Ned_defect_plugin extends Defect_plugin { private $_api; private $_address; private $_user; private $_password; private static $_meta = array( 'author' => 'Gurock Software', 'version' => '1.0', 'description' => 'YouTrack defect plugin for TestRail', 'can_push' => true, 'can_lookup' => true, 'default_config' => '; Please configure your YouTrack connection below [connection] address=http://localhost:8082/ user=%youtrack_username% password=%youtrack_password%' ); public function get_meta() { return self::$_meta; } // ********************************************************* // CONSTRUCT / DESTRUCT // ********************************************************* public function __construct() { } public function __destruct() { if ($this->_api) { try { $api = $this->_api; $this->_api = null; $api->logout(); } catch (Exception $e) { // Possible exceptions are ignored here. } } } // ********************************************************* // CONFIGURATION // ********************************************************* public function validate_config($config) { $ini = ini::parse($config); if (!isset($ini['connection'])) { throw new ValidationException('Missing [connection] group'); } $keys = array('address', 'user', 'password'); // Check required values for existance foreach ($keys as $key) { if (!isset($ini['connection'][$key]) || !$ini['connection'][$key]) { throw new ValidationException( "Missing configuration for key '$key'" ); } } $address = $ini['connection']['address']; // Check whether the address is a valid url (syntax only) if (!check::url($address)) { throw new ValidationException('Address is not a valid url'); } } public function configure($config) { $ini = ini::parse($config); $this->_address = str::slash($ini['connection']['address']); $this->_user = $ini['connection']['user']; $this->_password = $ini['connection']['password']; } // ********************************************************* // API / CONNECTION // ********************************************************* private function _get_api() { if ($this->_api) { return $this->_api; } $this->_api = new YouTrack_api($this->_address); $this->_api->login($this->_user, $this->_password); return $this->_api; } // ********************************************************* // PUSH // ********************************************************* public function prepare_push($context) { // Return a form with the following fields/properties return array( 'fields' => array( 'summary' => array( 'type' => 'string', 'label' => 'Summary', 'required' => true, 'size' => 'full' ), 'type' => array( 'type' => 'dropdown', 'label' => 'Type', 'required' => true, 'remember' => true, 'size' => 'compact' ), 'project' => array( 'type' => 'dropdown', 'label' => 'Project', 'required' => true, 'remember' => true, 'cascading' => true, 'size' => 'compact' ), 'subsystem' => array( 'type' => 'dropdown', 'label' => 'Subsystem', 'required' => false, 'remember' => true, 'depends_on' => 'project', 'size' => 'compact' ), 'config' => array( 'type' => 'string', 'label' => 'Configuration', 'remember' => true, 'size' => 'full' ), 'description' => array( 'type' => 'text', 'label' => 'Description', 'rows' => 10 ) ) ); } private function _get_summary_default($context) { $test = current($context['tests']); $summary = 'Failed test: ' . $test->case->title; if ($context['test_count'] > 1) { $summary .= ' (+others)'; } return $summary; } private function _get_description_default($context) { return $context['test_change']->description; } private function _to_id_name_lookup($items) { $result = array(); foreach ($items as $item) { $result[$item->id] = $item->name; } return $result; } public function prepare_field($context, $input, $field) { $data = array(); // Take into account the preferences of the user, but only // for the initial form rendering (not for dynamic loads). if ($context['event'] == 'prepare') { $prefs = arr::get($context, 'preferences'); } else { $prefs = null; } // Process those fields that do not need a connection to the // YouTrack installation. if ($field == 'summary' || $field == 'description' || $field == 'config') { switch ($field) { case 'summary': $data['default'] = $this->_get_summary_default( $context); break; case 'description': $data['default'] = $this->_get_description_default( $context); break; case 'config': $data['default'] = arr::get($prefs,'config'); break; } return $data; } // And then try to connect/login (in case we haven't set up a // working connection previously in this request) and process // the remaining fields. $api = $this->_get_api(); switch ($field) { case 'type': $data['options'] = $this->_to_id_name_lookup( $api->get_types() ); // Select the stored preference or the first item in // the list otherwise. $default = arr::get($prefs, 'type'); if ($default) { $data['default'] = $default; } else { if ($data['options']) { $data['default'] = key($data['options']); } } break; case 'project': $data['default'] = arr::get($prefs, 'project'); $data['options'] = $this->_to_id_name_lookup( $api->get_projects() ); break; case 'subsystem': if (isset($input['project'])) { $data['default'] = arr::get($prefs, 'subsystem'); $data['options'] = $this->_to_id_name_lookup( $api->get_subsystems($input['project']) ); } break; } return $data; } public function validate_push($context, $input) { } public function push($context, $input) { $api = $this->_get_api(); $data = array(); $data['summary'] = $input['summary']; $data['type'] = $input['type']; $data['project'] = $input['project']; $data['description'] = $input['description']; $data['customFieldValues'] = array( array( 'customfieldId' => 'Configuration', 'values' => array($input['config']) ) ); return $api->add_issue($data); } // ********************************************************* // LOOKUP // ********************************************************* public function lookup($defect_id) { $api = $this->_get_api(); $issue = $api->get_issue($defect_id); $attributes = array(); // Add some important attributes for the issue such as the // issue type, current status and project. Note that the // attribute values (and description) support HTML and we // thus need to escape possible HTML characters (with 'h') // in this plugin. if (isset($issue['type'])) { $attributes['Type'] = h($issue['type']); } $status = ''; if (isset($issue['state'])) { $attributes['Status'] = h($issue['state']); $status = $issue['state']; } if (isset($issue['project'])) { // Add a link to the project. $attributes['Project'] = str::format( '{2}', a($this->_address), a($issue['project']), h($issue['project']) ); } // Decide which status to return to TestRail based on the // resolved property of the issue's state. $status_id = GI_DEFECTS_STATUS_OPEN; if ($status) { $state = arr::get( obj::get_lookup( $api->get_states() ), $status ); if ($state) { if ($state->resolved) { $status_id = GI_DEFECTS_STATUS_RESOLVED; } } } // Format the description of the issue (we use a monospace // font). if (isset($issue['description']) && $issue['description']) { $description = str::format( '
{0}
', nl2br( html::link_urls( h($issue['description']) ) ) ); } else { $description = null; } return array( 'id' => $defect_id, 'url' => str::format( '{0}issue/{1}', $this->_address, $defect_id ), 'title' => $issue['summary'], 'status_id' => $status_id, 'status' => $status, 'description' => $description, 'attributes' => $attributes ); } } /** * YouTrack API * * Wrapper class for the YouTrack API with login/logout and functions * for retrieving projects etc. from a YouTrack installation. */ class YouTrack_api { private $_address; private $_cookies; private $_curl; /** * Construct * * Initializes a new YouTrack API object. Expects the web address * of the YouTrack installation including http or https prefix. */ public function __construct($address) { $this->_address = str::slash($address) . 'rest/'; } private function _throw_error($format, $params = null) { $args = func_get_args(); $format = array_shift($args); if (count($args) > 0) { $message = str::formatv($format, $args); } else { $message = $format; } throw new YouTrackException($message); } private function _send_command($method, $command, $data = null) { $url = $this->_address . $command; return $this->_send_request($method, $url, $data); } private function _send_request($method, $url, $data = null) { $options['data'] = $data; if ($this->_cookies) { $options['cookies'] = $this->_cookies; } if (!$this->_curl) { // Initialize the cURL handle. We re-use this handle to // make use of Keep-Alive, if possible. $this->_curl = http::open(); } $response = http::request_ex( $this->_curl, $method, $url, $options ); // In case debug logging is enabled, we append the data // we've sent and the entire request/response to the log. if (logger::is_on(GI_LOG_LEVEL_DEBUG)) { logger::debugr('$data', $data); logger::debugr('$response', $response); } if ($response->code == 501) { $this->_throw_error( 'The YouTrack REST API is not enabled ({0})', $response->code ); } // Parse the DOM before checking the HTTP code in order // to make use of the tag which describes any // errors in detail. $dom = $this->_parse_response($response->content); if ($response->code != 200) { $this->_throw_error( 'Invalid HTTP code ({0})', $response->code ); } if (!$this->_cookies) { // Save the cookies of the first request which serve // as login token. $this->_cookies = $response->cookies; } return $dom; } private function _parse_response($response) { if (!str::starts_with($response, '<?xml')) { // Some commands do not contain an XML header which is // needed by our XML parser for detecting the encoding // etc. $response = '<?xml version="1.0" encoding="UTF-8" ?>' .
            $response;
    }
    
    // YouTrack does not wrap its result in a <response> tag or
    // something similar. Since it is easier to work with our
    // XML api if we have such a tag, we add it here.
    $response = preg_replace(
        '/(<\?xml[^>]+\?>)(.*)/su',
        '\1<response>\2</response>',
        $response
    );
    
    $dom = xml::parse_string($response);
    
    if (isset($dom->error))
    {
        $this->_throw_error((string) $dom->error);
    }
    
    return $dom;
}

/**
 * Login
 *
 * Logs in to the YouTrack installation using the provided user
 * and password.
 */    
public function login($user, $password)
{
    $data['login'] = $user;
    $data['password'] = $password;
    $this->_send_command('POST', 'user/login', $data);
}

/**
 * Logout
 *
 * Logs the user out. You can use login() to log in again.
 */    
public function logout()
{
    // PLEASE NOTE: YouTrack's API command for logging out is
    // not documented or does not exist at all.
    $this->_cookies = null;
}

/**
 * Get Types
 *
 * Returns a list of types for the YouTrack installation. Types
 * are returned as array of objects, each with its ID and name.
 */    
public function get_types()
{
    $response = $this->_send_command('GET', 'project/types');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();        
    
    $types = $response->types;
    foreach ($types->type as $type)
    {
        $t = obj::create();
        $t->name = (string) $type['name'];
        $t->id = $t->name;
        $result[] = $t;
    }
    
    return $result;
}

/**
 * Get Projects
 *
 * Returns a list of projects for the YouTrack installation.
 * Projects are returned as array of objects, each with its ID
 * and name.
 */        
public function get_projects()
{
    $response = 
        $this->_send_command('GET', 'project/all?verbose=true');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();
    
    $projects = $response->projects;
    foreach ($projects->project as $project)
    {
        $p = obj::create();
        $p->name = (string) $project['name'];
        $p->id = (string) $project['shortName'];
        
        $p->subsystems = array();            
        if (isset($project->subsystems->sub))
        {
            foreach ($project->subsystems->sub as $sub)
            {
                $s = obj::create();
                $s->name = (string) $sub['value'];
                $s->id = $s->name;
                $p->subsystems[] = $s;
            }
        }
        
        $result[] = $p;
    }
    
    return $result;
}

/**
 * Get Subsystems
 *
 * Returns a list of subsystems for the given project for the
 * YouTrack installation. Subsystems are returned as array of
 * objects, each with its ID and name.
 */
public function get_subsystems($project_id)
{
    $project = arr::get(
        obj::get_lookup(
            $this->get_projects()
        ),
        $project_id
    );
    
    if (!$project)
    {
        return array();
    }
    
    return $project->subsystems;
}

/**
 * Get States
 *
 * Returns a list of states for the YouTrack installation.
 * States are returned as array of objects, each with its ID,
 * name and a resolved property.
 */    
public function get_states()
{
    $response = $this->_send_command('GET', 'project/states');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();        
    
    $states = $response->states;
    foreach ($states->state as $state)
    {
        $s = obj::create();
        $s->name = (string) $state['name'];
        $s->id = $s->name;
        $s->resolved = $state['resolved'] == 'true';
        $result[] = $s;
    }
    
    return $result;
}

/**
 * Get Issue
 *
 * Gets an existing case from the YouTrack installation and
 * returns it. The resulting issue object has various properties
 * such as the summary, description, project etc.
 */     
public function get_issue($issue_id)
{
    $response = $this->_send_command(
        'GET', 'issue/' . urlencode($issue_id)
    );
    
    $issue = $response->issue;        
    $mappings = array(
        'summary' => 'summary',
        'type' => 'type',
        'projectshortname' => 'project',
        'state' => 'state',
        'subsystem' => 'subsystem',
        'description' => 'description'
    );
    
    $result = array();
    foreach ($issue->field as $field)
    {
        $name = str::to_lower((string) $field['name']);
        if (!isset($mappings[$name]))
        {
            continue;
        }
        
        $value = (string) $field->value;
        $result[$mappings[$name]] = $value;
    }
    
    return $result;
}
    
/**
 * Add Issue
 *
 * Adds a new issue to the YouTrack installation with the given
 * parameters (title, project etc.) and returns its ID.
 *
 * summary:     The summary of the new issue
 * type:        The ID of the type of the new issue (bug,
 *              feature request etc.)
 * project:     The ID of the project the issue should be added
 *              to
 * subsystem:   The ID of the subsystem the issue is added to
 * description: The description of the new issue
 */    
public function add_issue($options)
{
    $response = $this->_send_command('POST', 'issue', $options);
    
    $issue = $response->issue;
    if (!isset($issue['id']))
    {
        $this->_throw_error('No issue ID received');
    }
    
    return (string) $issue['id'];
}

}

class YouTrackException extends Exception
{
}
[/code]


#2

Hello Ned,

Thanks for your posting. The key is to add the field to the prepare_push/prepare_field methods as part of the defect plugin (using the exact system/field name that YouTrack requires as part of the API). This looks as follows:

http://docs.gurock.com/testrail-integration/defects-plugins-examples#adding_custom_fields

The rest should work automatically in case of the YouTrack plugin.

I hope this helps!

Regards,
Tobias


#3

Thanks for the response! I will give that another try. I have encountered another issue that I am hoping someone will help me with. I am trying to add in a field from YouTrack to the Test Rails Push Dialog. The field is called ‘Versions’ and is of type version[*]on YouTrack. I followed the section in the Support Documentation that covered adding a ‘Built-in field’. I followed the same steps, but used ‘versions’ in place of ‘affected_versions’ throughout the code. When attempting to open the Push Dialog I get the following error 'Array to string conversion Array to string conversion [C:\Web\testrail\custom\defects\YouTrack_Custom.php:454] (at ErrorHandler::error)
Line 454 is $url = $this->_address . $command;in the send_command function.

I have uploaded the log-file with debug information. (I have uploaded it to Google Drive, if this does not suit let me know and I will copy and paste the text.)

This is the Defect Plugin.

<?php if (!defined('ROOTPATH')) exit('No direct script access allowed'); ?>
<?php

/**
 * YouTrack Defect Plugin for TestRail
 *
 * Copyright Gurock Software GmbH. All rights reserved.
 *
 * This is the TestRail defect plugin for JetBrains YouTrack. Please
 * see [url]http://docs.gurock.com/testrail-integration/defects-plugins[/url]
 * for more information about TestRail's defect plugins.
 *
 * [url]http://www.gurock.com/testrail/[/url]
 */
 
class YouTrack_Custom_defect_plugin extends Defect_plugin
{
    private $_api;
    
    private $_address;
    private $_user;
    private $_password;
    
    private static $_meta = array(
        'author' => 'TouchStore',
        'version' => '1.0',
        'description' => 'YouTrack Custom defect plugin for TouchStore',
        'can_push' => true,
        'can_lookup' => true,
        'default_config' => 
            '; Please configure your YouTrack connection below
[connection]
address=http://localhost:8082/
user=%youtrack_username%
password=%youtrack_password%'
    );
        
    public function get_meta()
    {
        return self::$_meta;
    }
    
    // *********************************************************
    // CONSTRUCT / DESTRUCT
    // *********************************************************    
    
    public function __construct()
    {
    }
    
    public function __destruct()
    {
        if ($this->_api)
        {
            try
            {
                $api = $this->_api;
                $this->_api = null;
                $api->logout();
            }
            catch (Exception $e)
            {
                // Possible exceptions are ignored here.
            }
        }
    }

    // *********************************************************
    // CONFIGURATION
    // *********************************************************
    
    public function validate_config($config)
    {
        $ini = ini::parse($config);
        
        if (!isset($ini['connection']))
        {
            throw new ValidationException('Missing [connection] group');
        }
        
        $keys = array('address', 'user', 'password');
        
        // Check required values for existance
        foreach ($keys as $key)
        {
            if (!isset($ini['connection'][$key]) ||
                !$ini['connection'][$key])
            {
                throw new ValidationException(
                    "Missing configuration for key '$key'"
                );
            }
        }
        
        $address = $ini['connection']['address'];
        
        // Check whether the address is a valid url (syntax only)
        if (!check::url($address))
        {
            throw new ValidationException('Address is not a valid url');
        }
    }
    
    public function configure($config)
    {
        $ini = ini::parse($config);
        $this->_address = str::slash($ini['connection']['address']);
        $this->_user = $ini['connection']['user'];
        $this->_password = $ini['connection']['password'];    
    }
    
    // *********************************************************
    // API / CONNECTION
    // *********************************************************
    
    private function _get_api()
    {
        if ($this->_api)
        {
            return $this->_api;
        }
        
        $this->_api = new YouTrack_api($this->_address);
        $this->_api->login($this->_user, $this->_password);
        return $this->_api;
    }
    
    // *********************************************************
    // PUSH
    // *********************************************************

    public function prepare_push($context)
    {
        // Return a form with the following fields/properties
        return array(
            'fields' => array(
                'summary' => array(
                    'type' => 'string',
                    'label' => 'Summary',
                    'required' => true,
                    'size' => 'full'
                ),
                'type' => array(
                    'type' => 'dropdown',
                    'label' => 'Type',
                    'required' => true,
                    'remember' => true,
                    'size' => 'compact'
                ),
                'project' => array(
                    'type' => 'dropdown',
                    'label' => 'Project',
                    'required' => true,
                    'remember' => true,
                    'cascading' => true,
                    'size' => 'compact'
                ),

                /*'subsystem' => array(
                    'type' => 'dropdown',
                    'label' => 'Subsystem',
                    'required' => false,
                    'remember' => true,
                    'depends_on' => 'project',
                    'size' => 'compact'
                ),*/

                'versions' => array(
                    'type' => 'dropdown',
                    'label' => 'Versions',
                    'required' => false,
                    'remember' => true,
                    'depends_on' => 'project',
                    'size' => 'compact'                    
                ),

            )
        );
    }
    
    private function _get_summary_default($context)
    {
        $test = current($context['tests']);
        $summary = 'Failed test: ' . $test->case->title;
        
        if ($context['test_count'] > 1)
        {
            $summary .= ' (+others)';
        }
        
        return $summary;
    }
    
    private function _get_description_default($context)
    {
        return $context['test_change']->description;
    }

    private function _to_id_name_lookup($items)
    {
        $result = array();
        foreach ($items as $item)
        {
            $result[$item->id] = $item->name;
        }
        return $result;
    }
    
    public function prepare_field($context, $input, $field)

    {

        $data = array();
        
        // Process those fields that do not need a connection to the
        // YouTrack installation.        
        if ($field == 'summary' || $field == 'description')
        {
            switch ($field)
            {
                case 'summary':
                    $data['default'] = $this->_get_summary_default(
                        $context);
                    break;
                    
                case 'description':
                    $data['default'] = $this->_get_description_default(
                        $context);
                    break;                
            }
        
            return $data;
        }
        
        // Take into account the preferences of the user, but only
        // for the initial form rendering (not for dynamic loads).
        if ($context['event'] == 'prepare')
        {
            $prefs = arr::get($context, 'preferences');
        }
        else
        {
            $prefs = null;
        }
        
        // And then try to connect/login (in case we haven't set up a
        // working connection previously in this request) and process
        // the remaining fields.
        $api = $this->_get_api();
        
        switch ($field)
        {
            case 'type':
                $data['options'] = $this->_to_id_name_lookup(
                    $api->get_types()
                );
                
                // Select the stored preference or the first item in
                // the list otherwise.
                $default = arr::get($prefs, 'type');
                if ($default)
                {
                    $data['default'] = $default;
                }
                else
                {
                    if ($data['options'])
                    {
                        $data['default'] = key($data['options']);
                    }
                }
                break;

            case 'project':
                $data['default'] = arr::get($prefs, 'project');
                $data['options'] = $this->_to_id_name_lookup(
                    $api->get_projects()
                );
                break;

            case 'versions':
                if (isset($input['project']))
                {
                    $data['default'] = arr::get($prefs, 'versions');
                    $data['options'] = $this->_to_id_name_lookup(
                        $api->get_versions($input['project'])
                    );
                }
                break;

            /*case 'subsystem':
                if (isset($input['project']))
                {
                    $data['default'] = arr::get($prefs, 'subsystem');
                    $data['options'] = $this->_to_id_name_lookup(
                        $api->get_subsystems($input['project'])
                    );
                }
                break;*/
        }
        
        return $data;
    }
    
    public function validate_push($context, $input)
    {
    }

    public function push($context, $input)
    {
        $api = $this->_get_api();
        return $api->add_issue($input);
    }
    
    // *********************************************************
    // LOOKUP
    // *********************************************************
    
    public function lookup($defect_id)
    {
        $api = $this->_get_api();
        $issue = $api->get_issue($defect_id);

        $attributes = array();
        
        // Add some important attributes for the issue such as the
        // issue type, current status and project. Note that the
        // attribute values (and description) support HTML and we
        // thus need to escape possible HTML characters (with 'h')
        // in this plugin.
        
        if (isset($issue['type']))
        {
            $attributes['Type'] = h($issue['type']);
        }

        $status = '';
        if (isset($issue['state']))
        {
            $attributes['Status'] = h($issue['state']);
            $status = $issue['state'];
        }

        if (isset($issue['project']))
        {
            // Add a link to the project.
            $attributes['Project'] = str::format(
                '<a target="_blank" href="{0}issues?q=project%3A+{1}">{2}</a>',
                a($this->_address),
                a($issue['project']),
                h($issue['project'])
            );
        }
        
        // Decide which status to return to TestRail based on the
        // resolved property of the issue's state.
        $status_id = GI_DEFECTS_STATUS_OPEN;

        if ($status)
        {
            $state = arr::get(
                obj::get_lookup(
                    $api->get_states()
                ),
                $status
            );

            if ($state)
            {
                if ($state->resolved)
                {
                    $status_id = GI_DEFECTS_STATUS_RESOLVED;
                }
            }
        }
        
        // Format the description of the issue (we use a monospace
        // font).
        if (isset($issue['description']) && $issue['description'])
        {
            $description = str::format(
                '<div class="monospace">{0}</div>',
                nl2br(
                    html::link_urls(
                        h($issue['description'])
                    )
                )
            );
        }
        else
        {
            $description = null;
        }

        return array(
            'id' => $defect_id,
            'url' => str::format(
                '{0}issue/{1}',
                $this->_address,
                $defect_id
            ),
            'title' => $issue['summary'],
            'status_id' => $status_id,
            'status' => $status,
            'description' => $description,
            'attributes' => $attributes
        );
    }
}

/**
 * YouTrack API
 *
 * Wrapper class for the YouTrack API with login/logout and functions
 * for retrieving projects etc. from a YouTrack installation.
 */
class YouTrack_api
{
    private $_address;
    private $_cookies;
    private $_curl;
    
    /**
     * Construct
     *
     * Initializes a new YouTrack API object. Expects the web address
     * of the YouTrack installation including http or https prefix.
     */    
    public function __construct($address)
    {
        $this->_address = str::slash($address) . 'rest/';
    }

    private function _throw_error($format, $params = null)
    {
        $args = func_get_args();
        $format = array_shift($args);
        
        if (count($args) > 0)
        {
            $message = str::formatv($format, $args);
        }
        else 
        {
            $message = $format;
        }
        
        throw new YouTrackException($message);
    }
    
    private function _send_command($method, $command, $data = null)
    {
        
        logger::debug('Now in _send_command');        
        logger::debugr('Ned: $data is the following',$data);
        logger::debugr('Ned: $command is the following',$command);
        logger::debugr('Ned: $method is the following',$method);    
        $url = $this->_address . $command;
        logger::debugr('URL is',$url);    
        return $this->_send_request($method, $url, $data);
    }
    
    private function _send_request($method, $url, $data = null)
    {
        $options['data'] = $data;
        if ($this->_cookies)
        {
            $options['cookies'] = $this->_cookies;
        }
        
        if (!$this->_curl)
        {
            // Initialize the cURL handle. We re-use this shandle to
            // make use of Keep-Alive, if possible.
            $this->_curl = http::open();
        }
                
        $response = http::request_ex(
            $this->_curl,
            $method, 
            $url, 
            $options
        );
        
        // In case debug logging is enabled, we append the data
        // we've sent and the entire request/response to the log.
        if (logger::is_on(GI_LOG_LEVEL_DEBUG))
        {        

            logger::debugr('$data', $data);
            logger::debugr('$response', $response);

        }
        
        if ($response->code == 501)
        {
            $this->_throw_error(
                'The YouTrack REST API is not enabled ({0})',
                $response->code
            );
        }
        
        // Parse the DOM before checking the HTTP code in order
        // to make use of the <error> tag which describes any
        // errors in detail.
        $dom = $this->_parse_response($response->content);
        
        if ($response->code != 200)
        {
            $this->_throw_error(
                'Invalid HTTP code ({0})', $response->code
            );
        }
        
        if (!$this->_cookies)
        {
            // Save the cookies of the first request which serve
            // as login token.
            $this->_cookies = $response->cookies;
        }
        
        return $dom;
    }
    
    private function _parse_response($response)
    {
        if (!str::starts_with($response, '<?xml'))
        {
            // Some commands do not contain an XML header which is
            // needed by our XML parser for detecting the encoding
            // etc.
            $response = '<?xml version="1.0" encoding="UTF-8" ?>' . 
                $response;
        }
        
        // YouTrack does not wrap its result in a <response> tag or
        // something similar. Since it is easier to work with our
        // XML api if we have such a tag, we add it here.
        $response = preg_replace(
            '/(<\?xml[^>]+\?>)(.*)/su',
            '\1<response>\2</response>',
            $response
        );
        
        $dom = xml::parse_string($response);
        
        if (isset($dom->error))
        {
            $this->_throw_error((string) $dom->error);
        }
        
        return $dom;
    }
    
    /**
     * Login
     *
     * Logs in to the YouTrack installation using the provided user
     * and password.
     */    
    public function login($user, $password)
    {
        $data['login'] = $user;
        $data['password'] = $password;
        $this->_send_command('POST', 'user/login', $data);
    }
    
    /**
     * Logout
     *
     * Logs the user out. You can use login() to log in again.
     */    
    public function logout()
    {
        // PLEASE NOTE: YouTrack's API command for logging out is
        // not documented or does not exist at all.
        $this->_cookies = null;
    }

    /**
     * Get Types
     *
     * Returns a list of types for the YouTrack installation. Types
     * are returned as array of objects, each with its ID and name.
     */    
    public function get_types()
    {
        $response = $this->_send_command('GET', 'project/types');
        
        if (!$response)
        {
            return array();
        }
        
        $result = array();        
        
        $types = $response->types;
        foreach ($types->type as $type)
        {
            $t = obj::create();
            $t->name = (string) $type['name'];
            $t->id = $t->name;
            $result[] = $t;
        }
        
        return $result;
    }
    
    /**
     * Get Projects
     *
     * Returns a list of projects for the YouTrack installation.
     * Projects are returned as array of objects, each with its ID
     * and name.
     */        
    public function get_projects()
    {

        $response = 
            $this->_send_command('GET', 'project/all?verbose=true');
        
        if (!$response)
        {
            return array();
        }
        
        $result = array();
        
        $projects = $response->projects;
        foreach ($projects->project as $project)
        {
            $p = obj::create();
            $p->name = (string) $project['name'];
            $p->id = (string) $project['shortName'];
            
            $p->subsystems = array();            
            if (isset($project->subsystems->sub))
            {
                foreach ($project->subsystems->sub as $sub)
                {
                    $s = obj::create();
                    $s->name = (string) $sub['value'];
                    $s->id = $s->name;
                    $p->subsystems[] = $s;
                }
            }
            
            $result[] = $p;

        }
        

        return $result;
    }

    
    /**
     * Get Subsystems
     *
     * Returns a list of subsystems for the given project for the
     * YouTrack installation. Subsystems are returned as array of
     * objects, each with its ID and name.
     */
    public function get_subsystems($project_id)
    {
        $project = arr::get(
            obj::get_lookup(
                $this->get_projects()
            ),
            $project_id
        );
        
        if (!$project)
        {
            return array();
        }
        
        return $project->subsystems;
    }
    
    /**
     * Get States
     *
     * Returns a list of states for the YouTrack installation.
     * States are returned as array of objects, each with its ID,
     * name and a resolved property.
     */    
    public function get_states()
    {
        $response = $this->_send_command('GET', 'project/states');
        
        if (!$response)
        {
            return array();
        }
        
        $result = array();        
        
        $states = $response->states;
        foreach ($states->state as $state)
        {
            $s = obj::create();
            $s->name = (string) $state['name'];
            $s->id = $s->name;
            $s->resolved = $state['resolved'] == 'true';
            $result[] = $s;
        }
        
        return $result;
    }


    /**
     * Get Versions
     *
     * Returns a list of versions for a YouTrack project. The versions
     * are returned as an array of objects, each with its ID and name.
     */    
    public function get_versions($project_id)
    {
        $data = array($project_id);
        $response = $this->_send_command('getVersions', $data);
     
        if (!$response)
        {
            return array();
        }
     
        $result = array();
        foreach ($response as $version)
        {
            $c = obj::create();
            $c->id = (string) $version->id;
            $c->name = (string) $version->name;
            $result[] = $c;
        }
     
        return $result;
    }
    
    /**
     * Get Issue
     *
     * Gets an existing case from the YouTrack installation and
     * returns it. The resulting issue object has various properties
     * such as the summary, description, project etc.
     */     
    public function get_issue($issue_id)
    {
        $response = $this->_send_command(
            'GET', 'issue/' . urlencode($issue_id)
        );
        
        $issue = $response->issue;        
        $mappings = array(
            'summary' => 'summary',
            'type' => 'type',
            'projectshortname' => 'project',
            'state' => 'state',
            'subsystem' => 'subsystem',
            'description' => 'description'
        );
        
        $result = array();
        foreach ($issue->field as $field)
        {
            $name = str::to_lower((string) $field['name']);
            if (!isset($mappings[$name]))
            {
                continue;
            }
            
            $value = (string) $field->value;
            $result[$mappings[$name]] = $value;
        }
        
        return $result;
    }
        
    /**
     * Add Issue
     *
     * Adds a new issue to the YouTrack installation with the given
     * parameters (title, project etc.) and returns its ID.
     *
     * summary:     The summary of the new issue
     * type:        The ID of the type of the new issue (bug,
     *              feature request etc.)
     * project:     The ID of the project the issue should be added
     *              to
     * subsystem:   The ID of the subsystem the issue is added to
     * description: The description of the new issue
     */    
    public function add_issue($options)
    {
        $response = $this->_send_command('POST', 'issue', $options);
        
        $issue = $response->issue;

        if (!isset($issue['id']))
        {
            $this->_throw_error('No issue ID received');
        }
        
        return (string) $issue['id'];

        if (isset($options['affects_version']))
        {

            $version = array(
                'id' => $options['versions'],
                'archived' => false,
                'released' => false
            );
            $options['versions'] = array(
                $version
            );
        }
        
        return (string) $issue['id'];        
    }
}

class YouTrackException extends Exception
{
}

Any assistance is greatly appreciated :slight_smile:


#4

Thanks for the additional details. The call to _send_command in get_versions looks incorrect as it doesn’t use the same calling conventions as the other methods in the API class. Please note that you cannot simply copy the code from the JIRA customization example as every defect plugin and API is different.

I hope this helps!

Regards,
Tobias


#5

Apologies, I am not yet any good with coding.
Well, at least I have the Raised In Version Field Visible on the Push Defect Dialog. I just need to get it to fetch the versions from YouTrack. I’m a bit lost regarding updating add_issue also :smiley:

[code]<?php if (!defined('ROOTPATH')) exit('No direct script access allowed'); ?>

<?php /** * YouTrack Defect Plugin for TestRail * * Copyright Gurock Software GmbH. All rights reserved. * * This is the TestRail defect plugin for JetBrains YouTrack. Please * see http://docs.gurock.com/testrail-integration/defects-plugins * for more information about TestRail's defect plugins. * * http://www.gurock.com/testrail/ */ class YouTrack_Ned_defect_plugin extends Defect_plugin { private $_api; private $_address; private $_user; private $_password; private static $_meta = array( 'author' => 'Gurock Software', 'version' => '1.0', 'description' => 'YouTrack defect plugin for TestRail', 'can_push' => true, 'can_lookup' => true, 'default_config' => '; Please configure your YouTrack connection below [connection] address=http://localhost:8082/ user=%youtrack_username% password=%youtrack_password%' ); public function get_meta() { return self::$_meta; } // ********************************************************* // CONSTRUCT / DESTRUCT // ********************************************************* public function __construct() { } public function __destruct() { if ($this->_api) { try { $api = $this->_api; $this->_api = null; $api->logout(); } catch (Exception $e) { // Possible exceptions are ignored here. } } } // ********************************************************* // CONFIGURATION // ********************************************************* public function validate_config($config) { $ini = ini::parse($config); if (!isset($ini['connection'])) { throw new ValidationException('Missing [connection] group'); } $keys = array('address', 'user', 'password'); // Check required values for existance foreach ($keys as $key) { if (!isset($ini['connection'][$key]) || !$ini['connection'][$key]) { throw new ValidationException( "Missing configuration for key '$key'" ); } } $address = $ini['connection']['address']; // Check whether the address is a valid url (syntax only) if (!check::url($address)) { throw new ValidationException('Address is not a valid url'); } } public function configure($config) { $ini = ini::parse($config); $this->_address = str::slash($ini['connection']['address']); $this->_user = $ini['connection']['user']; $this->_password = $ini['connection']['password']; } // ********************************************************* // API / CONNECTION // ********************************************************* private function _get_api() { if ($this->_api) { return $this->_api; } $this->_api = new YouTrack_api($this->_address); $this->_api->login($this->_user, $this->_password); return $this->_api; } // ********************************************************* // PUSH // ********************************************************* public function prepare_push($context) { // Return a form with the following fields/properties return array( 'fields' => array( 'summary' => array( 'type' => 'string', 'label' => 'Summary', 'required' => true, 'size' => 'full' ), 'type' => array( 'type' => 'dropdown', 'label' => 'Type', 'required' => true, 'remember' => true, 'size' => 'compact' ), 'project' => array( 'type' => 'dropdown', 'label' => 'Project', 'required' => true, 'remember' => true, 'cascading' => true, 'size' => 'compact' ), 'subsystem' => array( 'type' => 'dropdown', 'label' => 'Subsystem', 'required' => false, 'remember' => true, 'depends_on' => 'project', 'size' => 'compact' ), 'raised_in_version' => array( 'type' => 'dropdown', 'label' => 'Raised in Version', 'required' => false, 'remember' => true, 'depends_on' => 'project', 'size' => 'compact' ), 'description' => array( 'type' => 'text', 'label' => 'Description', 'rows' => 10 ) ) ); } private function _get_summary_default($context) { $test = current($context['tests']); $summary = 'Failed test: ' . $test->case->title; if ($context['test_count'] > 1) { $summary .= ' (+others)'; } return $summary; } private function _get_description_default($context) { return $context['test_change']->description; } private function _to_id_name_lookup($items) { $result = array(); foreach ($items as $item) { $result[$item->id] = $item->name; } return $result; } public function prepare_field($context, $input, $field) { $data = array(); // Take into account the preferences of the user, but only // for the initial form rendering (not for dynamic loads). if ($context['event'] == 'prepare') { $prefs = arr::get($context, 'preferences'); } else { $prefs = null; } // Process those fields that do not need a connection to the // YouTrack installation. if ($field == 'summary' || $field == 'description') { switch ($field) { case 'summary': $data['default'] = $this->_get_summary_default( $context); break; case 'description': $data['default'] = $this->_get_description_default( $context); break; } return $data; } // And then try to connect/login (in case we haven't set up a // working connection previously in this request) and process // the remaining fields. $api = $this->_get_api(); switch ($field) { case 'type': $data['options'] = $this->_to_id_name_lookup( $api->get_types() ); // Select the stored preference or the first item in // the list otherwise. $default = arr::get($prefs, 'type'); if ($default) { $data['default'] = $default; } else { if ($data['options']) { $data['default'] = key($data['options']); } } break; case 'project': $data['default'] = arr::get($prefs, 'project'); $data['options'] = $this->_to_id_name_lookup( $api->get_projects() ); break; case 'subsystem': if (isset($input['project'])) { $data['default'] = arr::get($prefs, 'subsystem'); $data['options'] = $this->_to_id_name_lookup( $api->get_subsystems($input['project']) ); } break; case 'raised_in_version': if (isset($input['project'])) { $data['default'] = arr::get($prefs, 'raised_in_version'); $data['options'] = $this->_to_id_name_lookup( $api->get_raised_in_version($input['project']) ); } break; } return $data; } public function validate_push($context, $input) { } public function push($context, $input) { $api = $this->_get_api(); $data = array(); $data['summary'] = $input['summary']; $data['type'] = $input['type']; $data['project'] = $input['project']; $data['description'] = $input['description']; return $api->add_issue($data); } // ********************************************************* // LOOKUP // ********************************************************* public function lookup($defect_id) { $api = $this->_get_api(); $issue = $api->get_issue($defect_id); $attributes = array(); // Add some important attributes for the issue such as the // issue type, current status and project. Note that the // attribute values (and description) support HTML and we // thus need to escape possible HTML characters (with 'h') // in this plugin. if (isset($issue['type'])) { $attributes['Type'] = h($issue['type']); } $status = ''; if (isset($issue['state'])) { $attributes['Status'] = h($issue['state']); $status = $issue['state']; } if (isset($issue['project'])) { // Add a link to the project. $attributes['Project'] = str::format( '{2}', a($this->_address), a($issue['project']), h($issue['project']) ); } // Decide which status to return to TestRail based on the // resolved property of the issue's state. $status_id = GI_DEFECTS_STATUS_OPEN; if ($status) { $state = arr::get( obj::get_lookup( $api->get_states() ), $status ); if ($state) { if ($state->resolved) { $status_id = GI_DEFECTS_STATUS_RESOLVED; } } } // Format the description of the issue (we use a monospace // font). if (isset($issue['description']) && $issue['description']) { $description = str::format( '
{0}
', nl2br( html::link_urls( h($issue['description']) ) ) ); } else { $description = null; } return array( 'id' => $defect_id, 'url' => str::format( '{0}issue/{1}', $this->_address, $defect_id ), 'title' => $issue['summary'], 'status_id' => $status_id, 'status' => $status, 'description' => $description, 'attributes' => $attributes ); } } /** * YouTrack API * * Wrapper class for the YouTrack API with login/logout and functions * for retrieving projects etc. from a YouTrack installation. */ class YouTrack_api { private $_address; private $_cookies; private $_curl; /** * Construct * * Initializes a new YouTrack API object. Expects the web address * of the YouTrack installation including http or https prefix. */ public function __construct($address) { $this->_address = str::slash($address) . 'rest/'; } private function _throw_error($format, $params = null) { $args = func_get_args(); $format = array_shift($args); if (count($args) > 0) { $message = str::formatv($format, $args); } else { $message = $format; } throw new YouTrackException($message); } private function _send_command($method, $command, $data = null) { $url = $this->_address . $command; return $this->_send_request($method, $url, $data); } private function _send_request($method, $url, $data = null) { $options['data'] = $data; if ($this->_cookies) { $options['cookies'] = $this->_cookies; } if (!$this->_curl) { // Initialize the cURL handle. We re-use this handle to // make use of Keep-Alive, if possible. $this->_curl = http::open(); } $response = http::request_ex( $this->_curl, $method, $url, $options ); // In case debug logging is enabled, we append the data // we've sent and the entire request/response to the log. if (logger::is_on(GI_LOG_LEVEL_DEBUG)) { logger::debugr('$data', $data); logger::debugr('$response', $response); } if ($response->code == 501) { $this->_throw_error( 'The YouTrack REST API is not enabled ({0})', $response->code ); } // Parse the DOM before checking the HTTP code in order // to make use of the tag which describes any // errors in detail. $dom = $this->_parse_response($response->content); if ($response->code != 200) { $this->_throw_error( 'Invalid HTTP code ({0})', $response->code ); } if (!$this->_cookies) { // Save the cookies of the first request which serve // as login token. $this->_cookies = $response->cookies; } return $dom; } private function _parse_response($response) { if (!str::starts_with($response, '<?xml')) { // Some commands do not contain an XML header which is // needed by our XML parser for detecting the encoding // etc. $response = '<?xml version="1.0" encoding="UTF-8" ?>' .
            $response;
    }
    
    // YouTrack does not wrap its result in a <response> tag or
    // something similar. Since it is easier to work with our
    // XML api if we have such a tag, we add it here.
    $response = preg_replace(
        '/(<\?xml[^>]+\?>)(.*)/su',
        '\1<response>\2</response>',
        $response
    );
    
    $dom = xml::parse_string($response);
    
    if (isset($dom->error))
    {
        $this->_throw_error((string) $dom->error);
    }
    
    return $dom;
}

/**
 * Login
 *
 * Logs in to the YouTrack installation using the provided user
 * and password.
 */    
public function login($user, $password)
{
    $data['login'] = $user;
    $data['password'] = $password;
    $this->_send_command('POST', 'user/login', $data);
}

/**
 * Logout
 *
 * Logs the user out. You can use login() to log in again.
 */    
public function logout()
{
    // PLEASE NOTE: YouTrack's API command for logging out is
    // not documented or does not exist at all.
    $this->_cookies = null;
}

/**
 * Get Types
 *
 * Returns a list of types for the YouTrack installation. Types
 * are returned as array of objects, each with its ID and name.
 */    
public function get_types()
{
    $response = $this->_send_command('GET', 'project/types');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();        
    
    $types = $response->types;
    foreach ($types->type as $type)
    {
        $t = obj::create();
        $t->name = (string) $type['name'];
        $t->id = $t->name;
        $result[] = $t;
    }
    
    return $result;
}

/**
 * Get Projects
 *
 * Returns a list of projects for the YouTrack installation.
 * Projects are returned as array of objects, each with its ID
 * and name.
 */        
public function get_projects()
{
    $response = 
        $this->_send_command('GET', 'project/all?verbose=true');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();
    
    $projects = $response->projects;
    foreach ($projects->project as $project)
    {
        $p = obj::create();
        $p->name = (string) $project['name'];
        $p->id = (string) $project['shortName'];
        
        $p->subsystems = array();            
        if (isset($project->subsystems->sub))
        {
            foreach ($project->subsystems->sub as $sub)
            {
                $s = obj::create();
                $s->name = (string) $sub['value'];
                $s->id = $s->name;
                $p->subsystems[] = $s;
            }
        }

        $p->raised_in_versions = array();            
        if (isset($project->raised_in_versions->raised))
        {
            foreach ($project->raised_in_versions->raised as $raised)
            {
                $r = obj::create();
                $r->name = (string) $raised['value'];
                $r->id = $r->name;
                $r->raised_in_versions[] = $r;
            }
        }
        
        $result[] = $p;
    }
    
    return $result;
}


/**
 * Get Subsystems
 *
 * Returns a list of subsystems for the given project for the
 * YouTrack installation. Subsystems are returned as array of
 * objects, each with its ID and name.
 */
public function get_subsystems($project_id)
{
    $project = arr::get(
        obj::get_lookup(
            $this->get_projects()
        ),
        $project_id
    );
    
    if (!$project)
    {
        return array();
    }
    
    return $project->subsystems;
}

public function get_raised_in_version($project_id)
{
    $project = arr::get(
        obj::get_lookup(
            $this->get_projects()
        ),
        $project_id
    );
    
    if (!$project)
    {
        return array();
    }
    
    return $project->raised_in_versions;
}

/**
 * Get States
 *
 * Returns a list of states for the YouTrack installation.
 * States are returned as array of objects, each with its ID,
 * name and a resolved property.
 */    
public function get_states()
{
    $response = $this->_send_command('GET', 'project/states');
    
    if (!$response)
    {
        return array();
    }
    
    $result = array();        
    
    $states = $response->states;
    foreach ($states->state as $state)
    {
        $s = obj::create();
        $s->name = (string) $state['name'];
        $s->id = $s->name;
        $s->resolved = $state['resolved'] == 'true';
        $result[] = $s;
    }
    
    return $result;
}

/**
 * Get Issue
 *
 * Gets an existing case from the YouTrack installation and
 * returns it. The resulting issue object has various properties
 * such as the summary, description, project etc.
 */     
public function get_issue($issue_id)
{
    $response = $this->_send_command(
        'GET', 'issue/' . urlencode($issue_id)
    );
    
    $issue = $response->issue;        
    $mappings = array(
        'summary' => 'summary',
        'type' => 'type',
        'projectshortname' => 'project',
        'state' => 'state',
        'subsystem' => 'subsystem',
        'description' => 'description'
    );
    
    $result = array();
    foreach ($issue->field as $field)
    {
        $name = str::to_lower((string) $field['name']);
        if (!isset($mappings[$name]))
        {
            continue;
        }
        
        $value = (string) $field->value;
        $result[$mappings[$name]] = $value;
    }
    
    return $result;
}
    
/**
 * Add Issue
 *
 * Adds a new issue to the YouTrack installation with the given
 * parameters (title, project etc.) and returns its ID.
 *
 * summary:     The summary of the new issue
 * type:        The ID of the type of the new issue (bug,
 *              feature request etc.)
 * project:     The ID of the project the issue should be added
 *              to
 * subsystem:   The ID of the subsystem the issue is added to
 * description: The description of the new issue
 */    

public function add_issue($options)
{
    $response = $this->_send_command('POST', 'issue', $options);
    
    $issue = $response->issue;
    if (!isset($issue['id']))
    {
        $this->_throw_error('No issue ID received');
    }
    
    return (string) $issue['id'];
}

}

class YouTrackException extends Exception
{
}

[/code]


#6

Hello Ned,

Thanks for your reply. Do you have a developer on your team who could help with this? I’m asking as you need programming experience and experience with APIs in order to make the changes. The documentation for customizing the defect plugins only includes a few examples and the customizations look different for each defect plugin and targeted issue tracker.

Regards,
Tobias


#7

Thanks tgurock,

Is there any chance you would have a copy of the YouTrack Defect Plugin? - it looks like I edited the original file as well as the copy I was working from…

Thanks


#8

Actually, there is no need afterall