├── .github └── workflows │ └── pythonpublish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── setup.py └── youtrackutils ├── __init__.py ├── bugzilla ├── __init__.py ├── bzClient.py └── defaultBzMapping.py ├── bugzilla2youtrack.py ├── csv2youtrack.py ├── csvClient ├── __init__.py ├── asanaMapping.py ├── client.py ├── example │ └── test_asana.csv └── youtrackMapping.py ├── fb2youtrack.py ├── fbugz ├── __init__.py ├── defaultFBugz.py ├── fbSOAPClient.py └── fogbugz.py ├── github2youtrack.py ├── mantis ├── __init__.py ├── defaultMantis.py └── mantisClient.py ├── mantis2youtrack.py ├── moveIssue.py ├── redmine ├── __init__.py ├── client.py └── mapping.py ├── redmine2youtrack.py ├── trac2youtrack.py ├── tracLib ├── __init__.py ├── client.py ├── defaultTrac.py └── timetracking.py ├── userTool.py ├── utils ├── __init__.py └── mapfile.py ├── youtrack2youtrack.py ├── zendesk ├── __init__.py └── zendeskClient.py └── zendesk2youtrack.py /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 JetBrains s.r.o 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include version 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![official JetBrains project](http://jb.gg/badges/official-flat-square.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | 3 | # Deprecation Notice 4 | This package is deprecated and will not work with YouTrack 2021.3 and later. 5 | 6 | YouTrack provides built-in imports from Jira, GitHub, Mantis, and Redmine. It also lets you set up a migration from one YouTrack instance to another. If you want to import issues from one of these sources, we strongly encourage you to use the built-in import feature instead of the scripts in this repository. 7 | 8 | The package will also receive little support and maintenance, if any. 9 | 10 | # YouTrack Python Scripts 11 | This repository contains a collection of command-line tools for interacting with the YouTrack REST API client library for Python. At present, the package only contains scripts for importing issues from other issue trackers. The scripts reference the YouTrack REST API client library for Python that is published in a [separate repository](https://github.com/JetBrains/youtrack-rest-python-library). 12 | 13 | ## Compatibility 14 | These scripts are compatible with Python 2.7+. Python 3 releases are not supported. 15 | 16 | The scripts are compatible with YouTrack Standalone versions from 5.x till 2021.2. 17 | 18 | ## Getting Started 19 | This package has been published to PyPI and can be installed with pip. 20 | `pip install youtrack-scripts` 21 | 22 | The YouTrack REST API client library for Python is installed automatically as a dependency. 23 | 24 | Once installed, you can build your mapping files and use the import scripts to migrate issues from other issue trackers to YouTrack. Specific instructions vary by import source. 25 | - For import to an installation on your own server, please refer to the documentation for [YouTrack Standalone](https://www.jetbrains.com/help/youtrack/standalone/2020.2/Migrating-to-YouTrack.html) 26 | - Import to an instance that is hosted in the cloud by JetBrains is not supported 27 | 28 | ## Import Scripts 29 | This package includes dedicated scripts for importing issues from Bugzilla, FogBugz, Mantis Bug Tracker (MantisBT), Redmine, and Trac. 30 | - Import from Jira to YouTrack is supported as a standard integration that is configured directly in YouTrack. 31 | - For other issue trackers, it may be possible to import issues from a CSV file. 32 | 33 | This package also includes scripts for importing single issues or entire projects from one YouTrack installation to another. 34 | 35 | ## YouTrack Support 36 | Your feedback is always appreciated. 37 | - To report bugs and request updates, please [create an issue](http://youtrack.jetbrains.com/issues/JT#newissue=yes). 38 | - If you experience problems with an import script, please [submit a support request](https://youtrack-support.jetbrains.com/hc/en-us). 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | # Try to convert markdown readme file to rst format 7 | try: 8 | import pypandoc 9 | md_file = path.join(here, 'README.md') 10 | rst_file = path.join(here, 'README.rst') 11 | pypandoc.convert_file(source_file=md_file, outputfile=rst_file, to='rst') 12 | except (ImportError, OSError, IOError, RuntimeError): 13 | pass 14 | 15 | # Get the long description from the relevant file 16 | with open(path.join(here, 'README.rst')) as f: 17 | long_description = f.read() 18 | 19 | # Get version from file 20 | with open(path.join(here, 'version')) as f: 21 | version = f.read().strip() 22 | 23 | 24 | setup( 25 | name='youtrack-scripts', 26 | version=version, 27 | python_requires='>=2.6, <3', 28 | packages=['youtrackutils', 29 | 'youtrackutils.bugzilla', 30 | 'youtrackutils.csvClient', 31 | 'youtrackutils.fbugz', 32 | 'youtrackutils.mantis', 33 | 'youtrackutils.redmine', 34 | 'youtrackutils.tracLib', 35 | 'youtrackutils.utils', 36 | 'youtrackutils.zendesk'], 37 | url='https://github.com/JetBrains/youtrack-python-scripts', 38 | license='Apache 2.0', 39 | maintainer='Alexander Buturlinov', 40 | maintainer_email='imboot85@gmail.com', 41 | description='YouTrack import and utility scripts', 42 | long_description=long_description, 43 | entry_points={ 44 | 'console_scripts': [ 45 | 'bugzilla2youtrack=youtrackutils.bugzilla2youtrack:main', 46 | 'csv2youtrack=youtrackutils.csv2youtrack:main', 47 | 'fb2youtrack=youtrackutils.fb2youtrack:main', 48 | 'github2youtrack=youtrackutils.github2youtrack:main', 49 | 'mantis2youtrack=youtrackutils.mantis2youtrack:main', 50 | 'redmine2youtrack=youtrackutils.redmine2youtrack:main', 51 | 'trac2youtrack=youtrackutils.trac2youtrack:main', 52 | 'youtrack2youtrack=youtrackutils.youtrack2youtrack:main', 53 | 'zendesk2youtrack=youtrackutils.zendesk2youtrack:main', 54 | 'yt-move-issue=youtrackutils.moveIssue:main' 55 | ], 56 | }, 57 | install_requires=[ 58 | "python-dateutil", 59 | "youtrack == 0.1.12", 60 | "pyactiveresource", # for Redmine import script 61 | # Commented out because the package installation can fail in case 62 | # if mysql is not installed on local machine 63 | # "MySQL-python", # for BugZilla and Mantis import scripts 64 | "BeautifulSoup >= 3.2.0", # for FogBugz import script 65 | "Trac >= 1.0.1", # for Track import script 66 | "requests" # for github import script, 67 | ] 68 | ) 69 | -------------------------------------------------------------------------------- /youtrackutils/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) -------------------------------------------------------------------------------- /youtrackutils/bugzilla/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import warnings 3 | 4 | warnings.warn("deprecated", DeprecationWarning) 5 | 6 | FIELD_TYPES = dict() 7 | FIELD_NAMES = dict() 8 | 9 | DEFAULT_EMAIL = "example@example.com" 10 | STATUS = dict([]) 11 | RESOLUTION = dict([]) 12 | PRIORITY = dict([]) 13 | CF_TYPES = dict([]) 14 | ACCEPT_EMPTY_COMMENTS = True 15 | BZ_DB_CHARSET = '' 16 | 17 | USE_STATE_MAP = False 18 | STATE_MAP = dict([]) 19 | STATE_STATUS = "bug_status" 20 | STATE_RESOLUTION = "resolution" 21 | 22 | 23 | class BzUser(object): 24 | def __init__(self, id): 25 | self.user_id = id 26 | self.login = "" 27 | self.full_name = None 28 | self.email = None 29 | 30 | 31 | class BzComponent(object): 32 | def __init__(self, id): 33 | self.id = id 34 | self.description = None 35 | self.initial_owner = None 36 | self.name = str(id) 37 | 38 | 39 | class BzVersion(object): 40 | def __init__(self, id): 41 | self.id = id 42 | self.value = str(id) 43 | 44 | 45 | class BzCustomField(object): 46 | def __init__(self, name): 47 | self.name = name 48 | self.type = "FIELD_TYPE_FREETEXT" 49 | self.values = list([]) 50 | 51 | 52 | class BzIssue(object): 53 | def __init__(self, id): 54 | self.id = id 55 | self.assignee = None 56 | self.severity = None 57 | self.status = None 58 | self.component = None 59 | self.created = time.time() 60 | self.keywords = set([]) 61 | self.op_sys = None 62 | self.priority = None 63 | self.platform = None 64 | self.reporter = None 65 | self.resolution = None 66 | self.summary = None 67 | self.version = None 68 | self.voters = set([]) 69 | self.cc = list([]) 70 | self.cf = dict([]) 71 | self.comments = list([]) 72 | self.attachments = list([]) 73 | self.flags = set([]) 74 | 75 | 76 | class BzComment(object): 77 | def __init__(self, time): 78 | self.time = time 79 | self.content = "" 80 | self.reporter = None 81 | 82 | 83 | class BzAttachment(object): 84 | def __init__(self, name): 85 | self.created = None 86 | self.reporter = "" 87 | self.name = name 88 | self.content = None 89 | 90 | 91 | class BzIssueLink(object): 92 | def __init__(self, name, source, target): 93 | self.name = name 94 | self.target_product_id = None 95 | self.source_product_id = None 96 | self.source = source 97 | self.target = target 98 | 99 | 100 | class BzIssueLinkType(object): 101 | def __init__(self, name): 102 | self.name = name 103 | self.description = "" 104 | -------------------------------------------------------------------------------- /youtrackutils/bugzilla/bzClient.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | import MySQLdb.cursors 3 | from youtrackutils.bugzilla import * 4 | import time 5 | from youtrackutils import bugzilla 6 | 7 | 8 | class Client(object): 9 | def __init__(self, host, port, login, password, db_name="bugs"): 10 | self.sql_cnx = MySQLdb.connect(host=host, port=port, user=login, passwd=password, 11 | db=db_name, cursorclass=MySQLdb.cursors.DictCursor, charset=bugzilla.BZ_DB_CHARSET) 12 | self.db_host = "%s:%s/" % (host, str(port)) 13 | 14 | def get_project_description(self, product_id): 15 | cursor = self.sql_cnx.cursor() 16 | description_row = "description" 17 | request = "SELECT %s FROM products WHERE id = %s" % (description_row, product_id) 18 | cursor.execute(request) 19 | desc = cursor.fetchone()[description_row].encode('utf8') 20 | return desc 21 | 22 | def get_components(self, product_id): 23 | cursor = self.sql_cnx.cursor() 24 | request = "SELECT * FROM components WHERE product_id = %s" % product_id 25 | cursor.execute(request) 26 | result = list([]) 27 | for row in cursor : 28 | cmp = BzComponent(row["id"]) 29 | cmp.description = row["description"] 30 | cmp.initial_owner = self.get_user_by_id(row["initialowner"]) 31 | cmp.name = row["name"] 32 | result.append(cmp) 33 | return result 34 | 35 | def get_versions(self, product_id): 36 | cursor = self.sql_cnx.cursor() 37 | id_row = 'id' 38 | value_row = 'value' 39 | request = "SELECT %s, %s FROM versions WHERE product_id = %s" % (id_row, value_row, product_id) 40 | cursor.execute(request) 41 | result = list([]) 42 | for row in cursor: 43 | version = BzVersion(row[id_row]) 44 | version.value = row[value_row] 45 | result.append(version) 46 | return result 47 | 48 | def get_custom_fields(self): 49 | cursor = self.sql_cnx.cursor() 50 | name_row = 'name' 51 | type_row = 'type' 52 | request = "SELECT %s, %s FROM fielddefs WHERE (custom = 1) AND NOT (type = 6)" % (name_row, type_row) 53 | cursor.execute(request) 54 | result = list([]) 55 | for row in cursor: 56 | cf = BzCustomField(row[name_row][3:]) 57 | cf.type = str(row[type_row]) 58 | if cf.type in ["2", "3"]: 59 | values_cursor = self.sql_cnx.cursor() 60 | value_row = 'value' 61 | request = "SELECT %s FROM %s" % (value_row, row[name_row]) 62 | values_cursor.execute(request) 63 | for v in values_cursor: 64 | value = v[value_row] 65 | if value != "---": 66 | cf.values.append(value) 67 | result.append(cf) 68 | return result 69 | 70 | def get_issue_link_types(self): 71 | cursor = self.sql_cnx.cursor() 72 | name_row = 'name' 73 | description_row = 'description' 74 | request = "SELECT %s, %s FROM fielddefs WHERE (custom = 1) AND (type = 6)" % (name_row, description_row) 75 | cursor.execute(request) 76 | result = list([]) 77 | for row in cursor: 78 | link_type = BzIssueLinkType(row[name_row][3:]) 79 | link_type.description = row[description_row].encode('utf8') 80 | result.append(link_type) 81 | return result 82 | 83 | def get_issue_links(self): 84 | link_types = self.get_issue_link_types() 85 | result = set([]) 86 | if not len(link_types): 87 | return result 88 | request = "SELECT bug_id, product_id, " 89 | for elem in link_types: 90 | request = request + "cf_" + elem.name + ", " 91 | request = request[:-2] 92 | request += " FROM bugs" 93 | cursor = self.sql_cnx.cursor() 94 | cursor.execute(request) 95 | for row in cursor: 96 | bug_id = row['bug_id'] 97 | for type in link_types: 98 | target = row["cf_" + type.name] 99 | if target is not None: 100 | link = BzIssueLink(type.name, str(bug_id), str(target)) 101 | link.source_product_id = str(row["product_id"]) 102 | link.target_product_id = str(self._get_product_id_by_bug_id(target)) 103 | result.add(link) 104 | return result 105 | 106 | def _get_component_by_id(self, component_id): 107 | cursor = self.sql_cnx.cursor() 108 | name_row = 'name' 109 | request = "SELECT %s FROM components WHERE id = %s" % (name_row, component_id) 110 | cursor.execute(request) 111 | result = cursor.fetchone() 112 | if result is None: 113 | return "No subsystem" 114 | else: 115 | return result[name_row] 116 | 117 | def get_issues(self, product_id, from_id, to_id): 118 | component_row = "component_id" 119 | user_rows = ["assigned_to", "qa_contact", "reporter"] 120 | 121 | if self.check_column_exists('bugs', 'keywords'): 122 | query = ''' 123 | SELECT 124 | * 125 | FROM 126 | bugs 127 | WHERE 128 | product_id=%s 129 | AND 130 | bug_id BETWEEN %d AND %d 131 | ''' % (product_id, from_id, to_id - 1) 132 | else: 133 | query = ''' 134 | SELECT 135 | b.*, 136 | ifnull(group_concat(d.name), '') keywords 137 | FROM 138 | bugs b 139 | LEFT JOIN 140 | keywords k 141 | ON b.bug_id = k.bug_id 142 | LEFT JOIN 143 | keyworddefs d 144 | ON k.keywordid = d.id 145 | WHERE 146 | b.product_id=%s 147 | AND 148 | b.bug_id BETWEEN %d AND %d 149 | GROUP BY 150 | b.bug_id 151 | ''' % (product_id, from_id, to_id - 1) 152 | 153 | cursor = self.sql_cnx.cursor() 154 | cursor.execute(query) 155 | 156 | result = [] 157 | for row in cursor: 158 | if component_row in row: 159 | row["component"] = self._get_component_by_id(row[component_row]) 160 | for user_row in user_rows: 161 | if user_row in row: 162 | user_row_value = row[user_row] 163 | if user_row_value is not None: 164 | row[user_row] = self.get_user_by_id(user_row_value) 165 | id = row["bug_id"] 166 | row["flags"] = self.get_flags_by_id(id) 167 | row["voters"] = self.get_voters_by_id(id) 168 | row.update(self.get_cf_values_by_id(id)) 169 | row["comments"] = self.get_comments_by_id(id) 170 | row["attachments"] = self.get_attachments_by_id(id) 171 | row["cc"] = self._get_cc_by_id(id) 172 | row["estimated_time"] = int(row["estimated_time"]) 173 | row["keywords"] = set([kw.strip() for kw in row["keywords"].split(",") if len(kw.strip())]) 174 | for key in row.keys(): 175 | if row[key] == "---": 176 | row[key] = None 177 | result.append(row) 178 | return result 179 | 180 | def get_issues_count(self, project_id): 181 | cursor = self.sql_cnx.cursor() 182 | cursor.execute("SELECT COUNT(*) FROM bugs WHERE product_id = %s" % project_id) 183 | return int(cursor.fetchone()["COUNT(*)"]) 184 | 185 | def _get_cc_by_id(self, id): 186 | cc_cursor = self.sql_cnx.cursor() 187 | who_row = 'who' 188 | request = "SELECT %s FROM cc WHERE bug_id = %s" % (who_row, id) 189 | cc_cursor.execute(request) 190 | result = [] 191 | for cc in cc_cursor : 192 | result.append(self.get_user_by_id(cc[who_row])) 193 | return result 194 | 195 | def get_duplicate_links(self): 196 | cursor = self.sql_cnx.cursor() 197 | dupe_row = 'dupe' 198 | dupe_of_row = "dupe_of" 199 | request = "SELECT %s, %s FROM duplicates" % (dupe_row, dupe_of_row) 200 | cursor.execute(request) 201 | result = set([]) 202 | for row in cursor: 203 | link = BzIssueLink("Duplicate", str(row[dupe_row]), str(row[dupe_of_row])) 204 | link.source_product_id = self._get_product_id_by_bug_id(row[dupe_row]) 205 | link.target_product_id = self._get_product_id_by_bug_id(row[dupe_of_row]) 206 | result.add(link) 207 | return result 208 | 209 | def get_dependencies_link(self): 210 | cursor = self.sql_cnx.cursor() 211 | blocked_row = 'blocked' 212 | depends_on_row = "dependson" 213 | request = "SELECT %s, %s FROM dependencies" % (blocked_row, depends_on_row) 214 | cursor.execute(request) 215 | result = set([]) 216 | for row in cursor: 217 | link = BzIssueLink("Depend", str(row[blocked_row]), str(row[depends_on_row])) 218 | link.source_product_id = self._get_product_id_by_bug_id(row[blocked_row]) 219 | link.target_product_id = self._get_product_id_by_bug_id(row[depends_on_row]) 220 | result.add(link) 221 | return result 222 | 223 | def get_user_by_id(self, id): 224 | cursor = self.sql_cnx.cursor() 225 | login_name = 'login_name' 226 | real_name = "realname" 227 | user_id = "userid" 228 | request = "SELECT %s, %s, %s FROM profiles WHERE userid = %s" % (login_name, real_name, user_id, id) 229 | cursor.execute(request) 230 | result = cursor.fetchone() 231 | user = BzUser(result[user_id]) 232 | user.login = result[login_name] 233 | user.email = result[login_name] 234 | user.full_name = result[real_name] 235 | return user 236 | 237 | def get_cf_values_by_id(self, bug_id): 238 | existing_custom_fields = self.get_custom_fields() 239 | 240 | single_fields = list([]) 241 | multiple_fields = list([]) 242 | for cf in existing_custom_fields: 243 | if cf.type == '3' : 244 | multiple_fields.append(cf.name) 245 | else: 246 | single_fields.append(cf.name) 247 | 248 | result = dict([]) 249 | sing_cursor = self.sql_cnx.cursor() 250 | if len(single_fields): 251 | request = "SELECT " 252 | for elem in single_fields: 253 | request = request + "cf_" + elem + ", " 254 | request = request[:-2] 255 | request += " FROM bugs WHERE bug_id = %s" % (str(bug_id)) 256 | sing_cursor.execute(request) 257 | for row in sing_cursor: 258 | for elem in single_fields: 259 | elem_row = "cf_" + elem 260 | if (row[elem_row] != "---") and (row[elem_row] is not None): 261 | result[elem] = row[elem_row] 262 | for cf in multiple_fields: 263 | mult_cursor = self.sql_cnx.cursor() 264 | mult_cursor.execute("SELECT value FROM bug_cf_" + cf + " WHERE bug_id = %s" % str(bug_id)) 265 | result[cf] = list([]) 266 | for row in mult_cursor: 267 | if row['value'] != '---': 268 | result[cf].append(row['value']) 269 | return result 270 | 271 | def get_comments_by_id(self, bug_id): 272 | result = list([]) 273 | cursor = self.sql_cnx.cursor() 274 | when_row = 'bug_when' 275 | who_row = 'who' 276 | text_row = 'thetext' 277 | request = "SELECT %s, %s, %s FROM longdescs WHERE bug_id = %s" % (when_row, who_row, text_row, str(bug_id)) 278 | cursor.execute(request) 279 | for row in cursor: 280 | comment = BzComment(time.mktime(row[when_row].timetuple()) + 1e-6 * row[when_row].microsecond) 281 | comment.reporter = self.get_user_by_id(row[who_row]) 282 | comment.content = row[text_row] 283 | result.append(comment) 284 | return result 285 | 286 | def get_attachments_by_id(self, bug_id): 287 | def get_attach_data_table(): 288 | cursor = self.sql_cnx.cursor() 289 | cursor.execute("show tables like 'attach_data'") 290 | if cursor.fetchone(): 291 | return 'attach_data' 292 | return 'attachments' 293 | result = list([]) 294 | cursor = self.sql_cnx.cursor() 295 | id_row = 'attach_id' 296 | created_row = 'creation_ts' 297 | filename_row = 'filename' 298 | submitter_row = 'submitter_id' 299 | attach_data_table = get_attach_data_table() 300 | attach_data_table_id_row = 'id' if attach_data_table == 'attach_data' else 'attach_id' 301 | request = "SELECT %s, %s, %s, %s " % (id_row, created_row, filename_row, submitter_row) 302 | request += "FROM attachments WHERE bug_id = %s" % str(bug_id) 303 | cursor.execute(request) 304 | for row in cursor: 305 | file_cursor = self.sql_cnx.cursor() 306 | data_row = 'thedata' 307 | file_request = "SELECT %s FROM %s WHERE %s = %s" % (data_row, attach_data_table, attach_data_table_id_row, str(row[id_row])) 308 | file_cursor.execute(file_request) 309 | attach_row = file_cursor.fetchone() 310 | if attach_row is None: 311 | continue 312 | attach = BzAttachment(row[filename_row]) 313 | attach.content = attach_row[data_row] 314 | attach.reporter = self.get_user_by_id(row[submitter_row]) 315 | attach.created = time.mktime(row[created_row].timetuple()) + 1e-6 * row[created_row].microsecond 316 | result.append(attach) 317 | return result 318 | 319 | def get_flags_by_id(self, bug_id): 320 | result = set([]) 321 | cursor = self.sql_cnx.cursor() 322 | type_row = 'type_id' 323 | request = "SELECT %s FROM flags WHERE (bug_id = %s) AND (status = '+')" % (type_row, str(bug_id)) 324 | cursor.execute(request) 325 | for row in cursor: 326 | flag_cursor = self.sql_cnx.cursor() 327 | name_row = 'name' 328 | flag_request = "SELECT %s FROM flagtypes WHERE id = %s LIMIT 1" % (name_row, str(row[type_row])) 329 | flag_cursor.execute(flag_request) 330 | result.add(flag_cursor.fetchone()[name_row].encode('utf8')) 331 | return result 332 | 333 | def get_voters_by_id(self, bug_id): 334 | result = list([]) 335 | if self.check_table_exists('votes'): 336 | cursor = self.sql_cnx.cursor() 337 | who_row = 'who' 338 | request = "SELECT %s FROM votes WHERE bug_id=%s" % (who_row, str(bug_id)) 339 | cursor.execute(request) 340 | for row in cursor: 341 | result.append(self.get_user_by_id(row[who_row])) 342 | return result 343 | 344 | def _get_product_id_by_bug_id(self, bug_id): 345 | cursor = self.sql_cnx.cursor() 346 | id_row = "product_id" 347 | request = "SELECT %s FROM bugs WHERE bug_id=%s LIMIT 1" % (id_row, str(bug_id)) 348 | cursor.execute(request) 349 | return cursor.fetchone()[id_row] 350 | 351 | def get_product_id_by_name(self, name): 352 | cursor = self.sql_cnx.cursor() 353 | id_row = "id" 354 | name_row = "name" 355 | request = "SELECT %s FROM products WHERE products.name='%s'" % (id_row, name) 356 | cursor.execute(request) 357 | result = cursor.fetchone() 358 | return result[id_row] if result is not None else None 359 | 360 | def get_product_names(self): 361 | cursor = self.sql_cnx.cursor() 362 | name_row = "name" 363 | request = "SELECT %s FROM products" % name_row 364 | cursor.execute(request) 365 | result = [] 366 | for row in cursor: 367 | result.append(row[name_row].encode('utf8')) 368 | return result 369 | 370 | def check_table_exists(self, table_name): 371 | cursor = self.sql_cnx.cursor() 372 | request = "SHOW TABLES LIKE '%s'" % table_name 373 | return cursor.execute(request) > 0 374 | 375 | def check_column_exists(self, table_name, column_name): 376 | cursor = self.sql_cnx.cursor() 377 | request = "SHOW COLUMNS FROM %s LIKE '%s'" % (table_name, column_name) 378 | return cursor.execute(request) > 0 379 | 380 | -------------------------------------------------------------------------------- /youtrackutils/bugzilla/defaultBzMapping.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import bugzilla 2 | 3 | bugzilla.FIELD_TYPES = { 4 | "created": "date", 5 | "updated": "date", 6 | "numberInProject": "integer", 7 | "reporterName": "user[1]", 8 | "Assignee": "user[1]", 9 | "Subsystem": "ownedField[1]", 10 | "Affected versions": "version[*]", 11 | "Severity": "enum[1]", 12 | "State": "state[1]", 13 | "Status": "state[1]", 14 | "Resolution": "state[1]", 15 | "OS": "enum[1]", 16 | "Platform": "enum[1]", 17 | "watcherName": "user[*]", 18 | "voterName": "user[*]", 19 | "Deadline": "date", 20 | "Estimate": "integer", 21 | "QA contact": "user[1]", 22 | "Milestone": "version[*]" 23 | } 24 | 25 | bugzilla.FIELD_NAMES = { 26 | "bug_id": "numberInProject", 27 | "reporter": "reporterName", 28 | "version": "Affected versions", 29 | "voters": "voterName", 30 | "assigned_to": "Assignee", 31 | "bug_severity": "Severity", 32 | "resolution": "Resolution", 33 | "bug_status": "Status", 34 | "creation_ts": "created", 35 | "op_sys": "OS", 36 | "rep_platform": "Platform", 37 | "short_desc": "summary", 38 | "cc": "watcherName", 39 | "delta_ts": "updated", 40 | "qa_contact": "QA contact", 41 | "estimated_time": "Estimate", 42 | "target_milestone": "Milestone", 43 | "component": "Subsystem", 44 | } 45 | 46 | # Mapping between cf types in bz and util 47 | bugzilla.CF_TYPES = { 48 | "1": "string", # FIELD_TYPE_FREETEXT 49 | "2": "enum[1]", # FIELD_TYPE_SINGLE_SELECT 50 | "3": "enum[*]", # FIELD_TYPE_MULTY_SELECT 51 | "4": "string", # FIELD_TYPE_TEXTAREA 52 | "5": "date", # FIELD_TYPE_DATETIME 53 | "7": "string" # FIELD_TYPE_BUG_URLS 54 | } 55 | 56 | # If we need to import empty comments 57 | bugzilla.ACCEPT_EMPTY_COMMENTS = False 58 | bugzilla.BZ_DB_CHARSET = 'utf8' 59 | 60 | bugzilla.USE_STATE_MAP = True 61 | bugzilla.STATE_STATUS = "bug_status" 62 | bugzilla.STATE_RESOLUTION = "resolution" 63 | 64 | 65 | bugzilla.STATE_MAP = { 66 | "UNCONFIRMED": "Open", 67 | "CONFIRMED": "Submitted", 68 | "NEW": "Submitted", 69 | "ASSIGNED": "Submitted", 70 | "REOPENED": "Reopened", 71 | "FIXED": "Fixed", 72 | "INVALID": "Won't fix", 73 | "WONTFIX": "Won't fix", 74 | "MOVED": "Won't fix", 75 | "LATER": "Won't fix", 76 | "IN_PROGRESS": "In Progress", 77 | "DUPLICATE": "Duplicate", 78 | "VERIFIED": "Verified", 79 | "RESOLVED": {"FIXED": "Fixed", "*": "Won\'t fix", "DUPLICATE": "Duplicate"}, 80 | "CLOSED": {"FIXED": "Fixed", "*": "Won\'t fix", "DUPLICATE": "Duplicate"} 81 | } 82 | -------------------------------------------------------------------------------- /youtrackutils/csvClient/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) 4 | 5 | # represents the format of the string (see http://docs.python.org/library/datetime.html#strftime-strptime-behavior) 6 | # format symbol "z" doesn't wok sometimes, maybe you will need to change csv2youtrack.to_unix_date(time_string) 7 | DATE_FORMAT_STRING = "" 8 | 9 | 10 | FIELD_NAMES = { 11 | "Project" : "project", 12 | "Summary" : "summary", 13 | "Reporter" : "reporterName", 14 | "Created" : "created", 15 | "Updated" : "updated", 16 | "Description" : "description" 17 | } 18 | FIELD_TYPES = { 19 | "Fix versions" : "version[*]", 20 | "State" : "state[1]", 21 | "Assignee" : "user[1]", 22 | "Affected versions" : "version[*]", 23 | "Fixed in build" : "build[1]", 24 | "Priority" : "enum[1]", 25 | "Subsystem" : "ownedField[1]", 26 | "Browser" : "enum[1]", 27 | "OS" : "enum[1]", 28 | "Verified in build" : "build[1]", 29 | "Verified by" : "user[1]", 30 | "Affected builds" : "build[*]", 31 | "Fixed in builds" : "build[*]", 32 | "Reviewed by" : "user[1]", 33 | "Story points" : "integer", 34 | "Value" : "integer", 35 | "Marketing value" : "integer" 36 | } 37 | 38 | CSV_DELIMITER = "," 39 | -------------------------------------------------------------------------------- /youtrackutils/csvClient/asanaMapping.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import csvClient 2 | 3 | csvClient.FIELD_NAMES = { 4 | #default fields (always available) 5 | #required fields 6 | "Projects" : "project_name", #make sure that all the fields are filled out for each task 7 | #asana leaves them blank for entries with a Parent Task 8 | "Project ID" : "project_id", #needs to be added to the csv from asana 9 | "Number" : "numberInProject", #needs to be added 10 | "Name" : "summary", 11 | "Created At" : "created", 12 | "Reporter" : "reporterName", #needs to be added 13 | #optional 14 | "Notes" : "description", 15 | "Last Modified" : "updated", 16 | #"Modified By" : "updaterName", 17 | "Completed At" : "resolved", 18 | #"Liked By" : "voterName", 19 | #"Watched By" : "watcherName", 20 | #"Group" : "permittedGroup", 21 | #not listed but found in code 22 | #"Tags" : "Tags", #can't get this to work properly 23 | #"Project Short" : "projectShortName", #the same as project_id? 24 | 25 | #extra fields defined in project 26 | "Assignee" : "Assignee", 27 | "Due Date" : "Due Date", 28 | #"Parent Task" : "Affected versions", #set this to something appropriate 29 | "State" : "State", #needs to be added 30 | } 31 | csvClient.FIELD_TYPES = { 32 | "Assignee" : "user[1]", 33 | "Due Date" : "date", 34 | "Affected versions" : "version[*]", 35 | "State" : "state[1]", 36 | } 37 | 38 | 39 | csvClient.CSV_DELIMITER = "," 40 | csvClient.DATE_FORMAT_STRING = "%Y-%m-%d" 41 | csvClient.GENERATE_ID_FOR_ISSUES = True 42 | 43 | -------------------------------------------------------------------------------- /youtrackutils/csvClient/client.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import sys 4 | import csv 5 | from youtrackutils import csvClient 6 | 7 | maxInt = sys.maxint 8 | while True: 9 | # decrease the maxInt value by factor 10 10 | # as long as the OverflowError occurs. 11 | try: 12 | csv.field_size_limit(maxInt) 13 | break 14 | except OverflowError: 15 | maxInt = int(maxInt/10) 16 | 17 | 18 | class Client(object): 19 | def __init__(self, file_path): 20 | self._file_path = file_path 21 | self._header = self._read_header() 22 | self._issues_reader = None 23 | 24 | def _read_header(self): 25 | header = self._get_reader().next() 26 | return [field_name 27 | for field_name in [h.strip() for h in header] 28 | if len(field_name)] 29 | 30 | def has_bom(self): 31 | b_cnt = min(32, os.path.getsize(self._file_path)) 32 | raw = open(self._file_path, 'rb').read(b_cnt) 33 | return raw.startswith(codecs.BOM_UTF8) 34 | 35 | def _get_reader(self): 36 | fh = open(self._file_path, "rU") 37 | if self.has_bom(): 38 | fh.read(len(codecs.BOM_UTF8)) 39 | return csv.reader(fh, delimiter=csvClient.CSV_DELIMITER) 40 | 41 | def get_rows(self): 42 | reader = self._get_reader() 43 | for row in reader: 44 | yield row 45 | 46 | def get_issues(self): 47 | reader = self._get_reader() 48 | reader.next() 49 | header_len = len(self._header) 50 | for row in reader: 51 | if not row: 52 | continue 53 | issue = {"comments": []} 54 | for i in range(len(row)): 55 | value = row[i].strip() 56 | if len(value): 57 | if i < header_len: 58 | issue[self._header[i]] = value 59 | else: 60 | issue["comments"].append(value) 61 | yield issue 62 | 63 | def get_header(self): 64 | return self._header 65 | 66 | def reset(self): 67 | self._issues_reader = self._get_reader() 68 | self._read_header() 69 | -------------------------------------------------------------------------------- /youtrackutils/csvClient/example/test_asana.csv: -------------------------------------------------------------------------------- 1 | Task ID,Created At,Completed At,Last Modified,Name,Assignee,Due Date,Notes,Projects,Parent Task,Project ID,Number,Reporter,State, 2 | XXXXXXXXXX,2014-09-15,2014-10-27,2014-10-27,Task 1,John Doe,,Some task,whatever,,bug,1,import,Fixed,"#can use '=IF(C2="""",""Open"",""Fixed"")' for the N collumn" 3 | XXXXXXXXXX,2014-08-15,,2014-10-24,Incomplete task 2,John Doe,,Some other task,whatever,,bug,2,import,Open, 4 | XXXXXXXXXX,2014-10-24,2014-10-24,2014-10-24,Task 3,John Doe,,This is yet another task,whatever,,bug,3,import,Fixed, 5 | XXXXXXXXXX,2014-10-24,2014-10-24,2014-10-24,Task 4,John Doe,,"And the final task 6 | With some multi-line comment and, commas, in, it",whatever,,bug,4,import,Fixed, 7 | -------------------------------------------------------------------------------- /youtrackutils/csvClient/youtrackMapping.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import csvClient 2 | 3 | csvClient.FIELD_NAMES = { 4 | "Project" : "project_name", 5 | "Project Id" : "project_id", 6 | "Summary" : "summary", 7 | "Reporter" : "reporterName", 8 | "Created" : "created", 9 | "Updated" : "updated", 10 | "Description" : "description", 11 | "Issue Id" : "numberInProject" 12 | } 13 | csvClient.FIELD_TYPES = { 14 | "Fix versions" : "version[*]", 15 | "State" : "state[1]", 16 | "Assignee" : "user[1]", 17 | "Affected versions" : "version[*]", 18 | "Fixed in build" : "build[1]", 19 | "Priority" : "enum[1]", 20 | "Subsystem" : "ownedField[1]", 21 | "Browser" : "enum[1]", 22 | "OS" : "enum[1]", 23 | "Verified in build" : "build[1]", 24 | "Verified by" : "user[1]", 25 | "Affected builds" : "build[*]", 26 | "Fixed in builds" : "build[*]", 27 | "Reviewed by" : "user[1]", 28 | "Story points" : "integer", 29 | "Value" : "integer", 30 | "Marketing value" : "integer" 31 | } 32 | 33 | 34 | csvClient.CSV_DELIMITER = "," 35 | csvClient.DATE_FORMAT_STRING = "%A, %B %d, %Y %I:%M:%S %p %z" 36 | -------------------------------------------------------------------------------- /youtrackutils/fb2youtrack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | import getopt 10 | import os 11 | import youtrack 12 | from youtrackutils.fbugz.fbSOAPClient import FBClient 13 | from youtrack.connection import Connection 14 | from youtrack import Group, User, Issue, Comment, Link 15 | import youtrackutils.fbugz 16 | import youtrackutils.fbugz.defaultFBugz 17 | from youtrack.importHelper import * 18 | from utils.mapfile import load_map_file, dump_map_file 19 | 20 | 21 | help_url = "\ 22 | https://www.jetbrains.com/help/youtrack/standalone/Import-from-FogBugz.html" 23 | 24 | 25 | def usage(): 26 | basename = os.path.basename(sys.argv[0]) 27 | 28 | print(""" 29 | Usage: 30 | %s [OPTIONS] yt_url fb_url fb_login fb_password fb_max_issue_id 31 | 32 | yt_url YouTrack base URL 33 | 34 | fb_url FogBugz URL 35 | 36 | fb_login The username to log in to the FogBugz server 37 | 38 | fb_password The password to log in to the FogBugz server 39 | 40 | fb_max_issue_id Max issue id to import from FugBugz 41 | 42 | The script uses default mapping settings to import data from source 43 | tracker, like how fields from source tracker should be imported to YouTrack. 44 | If you wish to modify the settings you can run the script with -g option 45 | to generate mapping file. Then you'll be able to modify the file to feet 46 | your needs and re-run the script with the mapping file using -m option. 47 | 48 | For instructions, see: 49 | %s 50 | 51 | Options: 52 | -h, Show this help and exit 53 | -g, Generate mapping file from the defaults 54 | -T TOKEN_FILE, 55 | Path to file with permanent token 56 | -t TOKEN, 57 | Value for permanent token as text 58 | -u LOGIN, 59 | YouTrack user login to perform import on behalf of 60 | -p PASSWORD, 61 | YouTrack user password 62 | -m MAPPING_FILE, 63 | Path to mapping file that maps columns from csv to YouTrack fields 64 | -d PROJECT_LEAD_LOGIN 65 | YouTrack user to set as project lead for imported projects 66 | 67 | Examples: 68 | 69 | Generate mapping file (can be customized and used for further import) 70 | 71 | $ %s -g -m mapping.json 72 | 73 | 74 | Import issues using the mapping file: 75 | 76 | $ %s -T token -m mapping.json https://youtrack.company.com \ 77 | https://fogbugz.company.com fb fb 1000 78 | 79 | 80 | """ % (basename, help_url, basename, basename)) 81 | 82 | 83 | def main(): 84 | try: 85 | params = {} 86 | opts, args = getopt.getopt(sys.argv[1:], 'hgu:p:m:t:T:d:') 87 | for opt, val in opts: 88 | if opt == '-h': 89 | usage() 90 | sys.exit(0) 91 | elif opt == '-g': 92 | params['generate_mapping'] = True 93 | elif opt == '-u': 94 | params['yt_login'] = val 95 | elif opt == '-p': 96 | params['yt_password'] = val 97 | elif opt == '-m': 98 | check_file_and_save(val, params, 'mapping_file') 99 | elif opt == '-t': 100 | params['token'] = val 101 | elif opt == '-T': 102 | check_file_and_save(val, params, 'token_file') 103 | elif opt == '-d': 104 | params['project_lead_login'] = val 105 | except getopt.GetoptError as e: 106 | print(e) 107 | usage() 108 | sys.exit(1) 109 | 110 | if params.get('generate_mapping', False): 111 | return dump_map_file(get_mappings(), params.get('mapping_file')) 112 | 113 | try: 114 | for k in ('yt_url', 'fb_url', 'fb_login', 'fb_password'): 115 | params[k] = args.pop(0) 116 | params['fb_max_issue_id'] = int(args.pop(0)) 117 | except (ValueError, KeyError, IndexError): 118 | print("Bad arguments") 119 | usage() 120 | sys.exit(1) 121 | 122 | if 'mapping_file' in params: 123 | update_mappings(load_map_file(params['mapping_file'])) 124 | 125 | if not params['fb_url'].endswith("/"): 126 | params['fb_url'] += "/" 127 | 128 | fb2youtrack(params) 129 | 130 | 131 | def check_file_and_save(filename, params, key): 132 | try: 133 | params[key] = os.path.abspath(filename) 134 | except (OSError, IOError) as e: 135 | print("Data file is not accessible: " + str(e)) 136 | print(filename) 137 | sys.exit(1) 138 | 139 | 140 | def get_mappings(): 141 | return dict( 142 | __help__=u"For instructions, see: " + help_url + 143 | u"#customize-mapping-file", 144 | cf_types=youtrackutils.fbugz.CF_TYPES, 145 | cf_names=youtrackutils.fbugz.CF_NAMES, 146 | projects_to_import=youtrackutils.fbugz.PROJECTS_TO_IMPORT 147 | ) 148 | 149 | 150 | def update_mappings(mapping_data): 151 | if 'cf_types' in mapping_data: 152 | youtrackutils.fbugz.CF_TYPES = mapping_data['cf_types'] 153 | if 'cf_names' in mapping_data: 154 | youtrackutils.fbugz.CF_NAMES = mapping_data['cf_names'] 155 | if 'projects_to_import' in mapping_data: 156 | youtrackutils.fbugz.PROJECTS_TO_IMPORT = \ 157 | mapping_data['projects_to_import'] 158 | 159 | 160 | def _to_yt_user(fb_user): 161 | user = User() 162 | user.login = fb_user.login.replace(' ', '_') 163 | 164 | user.fullName = fb_user.login 165 | user.email = fb_user.email 166 | user.group = fb_user.user_type 167 | return user 168 | 169 | 170 | def _to_yt_subsystem(bundle, area): 171 | subsystem = bundle.createElement(area.name) 172 | assignee = area.person_owner 173 | if assignee is not None: 174 | subsystem.owner = assignee.replace(' ', "_") 175 | return subsystem 176 | 177 | 178 | def _to_yt_version(bundle, milestone): 179 | version = bundle.createElement(milestone.name) 180 | version.released = milestone.inactive 181 | version.archived = False 182 | version.releaseDate = milestone.release_date 183 | return version 184 | 185 | 186 | def _to_yt_comment(fb_comment): 187 | comment = Comment() 188 | comment.author = fb_comment.author.replace(" ", "_") 189 | comment.text = fb_comment.text 190 | comment.created = fb_comment.date 191 | return comment 192 | 193 | 194 | def _to_yt_issue(fb_issue, value_sets): 195 | issue = Issue() 196 | issue.numberInProject = str(fb_issue.ix_bug) 197 | issue.summary = fb_issue.title 198 | issue.created = fb_issue.opened 199 | issue.reporterName = fb_issue.reporter.replace(' ', "_") 200 | 201 | for field_name in fb_issue.field_values.keys(): 202 | value_set = None 203 | if field_name in value_sets: 204 | value_set = value_sets[field_name] 205 | yt_field_name = get_yt_field_name(field_name) 206 | field_value = fb_issue.field_values[field_name] 207 | if value_set is not None and field_value not in value_set : 208 | field_value = None 209 | value = to_yt_field_value(yt_field_name, field_value) 210 | if value is not None: 211 | issue[yt_field_name] = value 212 | 213 | issue.comments = [] 214 | is_description = True 215 | for c in fb_issue.comments : 216 | if is_description: 217 | issue.description = c.text 218 | is_description = False 219 | else : 220 | issue.comments.append(_to_yt_comment(c)) 221 | return issue 222 | 223 | 224 | def add_field_values_to_bundle(connection, bundle, field_values): 225 | missing_names = calculate_missing_value_names(bundle, [value.name for value in field_values]) 226 | values_to_add = [] 227 | for value in field_values: 228 | if value.name in missing_names: 229 | values_to_add.append(value) 230 | add_values_to_bundle_safe(connection, bundle, values_to_add) 231 | 232 | 233 | def _do_import_users(target, users_to_import): 234 | target.importUsers(users_to_import) 235 | for u in users_to_import: 236 | target.setUserGroup(u.login, u.group) 237 | 238 | 239 | def get_yt_field_name(fb_name): 240 | if fb_name in youtrackutils.fbugz.CF_NAMES: 241 | return youtrackutils.fbugz.CF_NAMES[fb_name] 242 | return fb_name.decode('utf-8') 243 | 244 | 245 | def create_bundle_with_values(connection, bundle_type, bundle_name, values, value_converter): 246 | bundle = create_bundle_safe(connection, bundle_name, bundle_type) 247 | values = set(values) 248 | values_to_add = [value_converter(bundle, value) for value in values if value is not None] 249 | add_field_values_to_bundle(connection, bundle, values_to_add) 250 | 251 | 252 | def add_values_to_field(connection, field_name, project_id, values, create_value): 253 | field = connection.getProjectCustomField(project_id, field_name) 254 | values = set(values) 255 | if hasattr(field, 'bundle'): 256 | bundle = connection.getBundle(field.type, field.bundle) 257 | yt_values = [create_value(bundle, to_yt_field_value(field_name, value)) for value in values] 258 | add_field_values_to_bundle(connection, bundle, yt_values) 259 | 260 | 261 | def to_yt_status(bundle, fb_status): 262 | status_name, resolved = fb_status 263 | status = bundle.createElement(status_name) 264 | status.is_resolved = str(resolved) 265 | return status 266 | 267 | 268 | def to_yt_field_value(field_name, value): 269 | if field_name not in youtrackutils.fbugz.CF_VALUES: 270 | return value 271 | if value not in youtrackutils.fbugz.CF_VALUES[field_name]: 272 | return value 273 | return youtrackutils.fbugz.CF_VALUES[field_name][value] 274 | 275 | 276 | def fb2youtrack(params): 277 | # Connection to FogBugz 278 | source = FBClient(params['fb_url'], 279 | params['fb_login'], 280 | params['fb_password']) 281 | 282 | # Connecting to YouTrack 283 | token = params.get('token') 284 | if not token and 'token_file' in params: 285 | try: 286 | with open(params['token_file'], 'r') as f: 287 | token = f.read().strip() 288 | except (OSError, IOError) as e: 289 | print("Cannot load token from file: " + str(e)) 290 | sys.exit(1) 291 | if token: 292 | target = Connection(params['yt_url'], token=token) 293 | elif 'yt_login' in params: 294 | target = Connection(params['yt_url'], 295 | params.get('yt_login'), 296 | params.get('yt_password')) 297 | else: 298 | print("You have to provide token or login/password to import data") 299 | sys.exit(1) 300 | 301 | if not params.get('project_lead_login'): 302 | project_lead = params.get('yt_login') 303 | if not project_lead: 304 | for login in ('root', 'admin', 'administrator', 'guest'): 305 | try: 306 | project_lead = target.getUser(login).login 307 | break 308 | except youtrack.YouTrackException: 309 | continue 310 | params['project_lead_login'] = project_lead 311 | 312 | max_issue_id = params['fb_max_issue_id'] 313 | 314 | project_names = youtrackutils.fbugz.PROJECTS_TO_IMPORT 315 | accessible_projects = source.list_project_names() 316 | for p_name in project_names: 317 | if not (p_name in accessible_projects.keys()): 318 | print('Unknown project names. Exiting...') 319 | sys.exit() 320 | 321 | # for p_name in accessible_projects : 322 | # if (p_name.encode('utf-8') in project_names_str) : 323 | # project_names_str.remove(p_name.encode('utf-8')) 324 | # project_names.append(p_name) 325 | # 326 | # if (len(project_names_str) != 0) : 327 | # print 'Unknown project names!' 328 | 329 | print('Creating custom fields') 330 | # 331 | # for field_name in ['Category', 'Priority', 'Status']: 332 | # field_name = get_yt_name_from_fb__field_name(field_name) 333 | # create_custom_field(target, fbugz.CF_TYPES[field_name], field_name, False) 334 | 335 | fb_category_bundle_name = u'FB Categories' 336 | fb_priorities_bundle_name = u'FB Priorities' 337 | fb_statuses_bundle_name = u'FB Statuses' 338 | 339 | common_fields = { 340 | u'category' : fb_category_bundle_name, 341 | u'priority' : fb_priorities_bundle_name, 342 | u'status' : fb_statuses_bundle_name 343 | } 344 | 345 | field_name = u'category' 346 | create_bundle_with_values( 347 | target, 348 | youtrackutils.fbugz.CF_TYPES[get_yt_field_name(field_name)], 349 | common_fields[field_name], 350 | source.list_categories(), 351 | lambda bundle, value: 352 | bundle.createElement(to_yt_field_value(field_name, value))) 353 | field_name = u'priority' 354 | create_bundle_with_values(target, youtrackutils.fbugz.CF_TYPES[get_yt_field_name(field_name)], 355 | common_fields[field_name], 356 | [elem[0] + '-' + elem[1] for elem in source.list_priorities()], 357 | lambda bundle, value : bundle.createElement(to_yt_field_value(field_name, value))) 358 | 359 | field_name = u'status' 360 | statuses = [(to_yt_field_value(field_name, value), resolved) for (value, resolved) in source.list_statuses()] 361 | create_bundle_with_values(target, youtrackutils.fbugz.CF_TYPES[get_yt_field_name(field_name)], 362 | common_fields[field_name], 363 | statuses, lambda bundle, value : to_yt_status(bundle, value)) 364 | 365 | simple_fields = [u'original_title', u'version', u'computer', u'due', u'estimate'] 366 | 367 | for name in simple_fields: 368 | name = get_yt_field_name(name) 369 | create_custom_field(target, youtrackutils.fbugz.CF_TYPES[name], name, False) 370 | 371 | print('Importing users') 372 | for name in ['Normal', 'Deleted', 'Community', 'Virtual'] : 373 | group = Group() 374 | group.name = name 375 | try : 376 | target.createGroup(group) 377 | print('Group with name [ %s ] successfully created' % name) 378 | except: 379 | print("Can't create group with name [ %s ] (maybe because it already exists)" % name) 380 | 381 | users_to_import = [] 382 | max = 100 383 | for user in source.get_users() : 384 | yt_user = _to_yt_user(user) 385 | print('Importing user [ %s ]' % yt_user.login) 386 | users_to_import.append(yt_user) 387 | if len(users_to_import) >= max: 388 | _do_import_users(target, users_to_import) 389 | users_to_import = [] 390 | _do_import_users(target, users_to_import) 391 | print('Importing users finished') 392 | 393 | # to handle linked issues 394 | try : 395 | target.createIssueLinkTypeDetailed('parent-child', 'child of', 'parent of', True) 396 | except YouTrackException: 397 | print("Can't create issue link type [ parent-child ] (maybe because it already exists)") 398 | links_to_import = [] 399 | 400 | for project_name in project_names: 401 | value_sets = dict([]) 402 | 403 | project_id = accessible_projects[project_name] 404 | print('Importing project [ %s ]' % project_name) 405 | target.createProjectDetailed(project_id, 406 | project_name.encode('utf-8'), 407 | '', 408 | params['project_lead_login']) 409 | 410 | print('Creating custom fields in project [ %s ]' % project_name) 411 | 412 | for cf_name in common_fields: 413 | bundle_name = common_fields[cf_name] 414 | cf_name = get_yt_field_name(cf_name) 415 | target.deleteProjectCustomField(project_id, cf_name) 416 | target.createProjectCustomFieldDetailed(project_id, cf_name, 'No ' + cf_name.lower(), 417 | {'bundle' : bundle_name}) 418 | 419 | for cf_name in simple_fields: 420 | cf_name = get_yt_field_name(cf_name) 421 | try: 422 | target.createProjectCustomFieldDetailed(project_id, cf_name, 'No ' + cf_name.lower()) 423 | except YouTrackException: 424 | print("Can't create custom field with name [%s]" % cf_name) 425 | cf_name = get_yt_field_name('fix_for') 426 | milestones = source.get_milestones(project_id) 427 | value_sets["fix_for"] = [] 428 | for milestone in milestones: 429 | value_sets["fix_for"].append(milestone.name) 430 | milestone.name = to_yt_field_value('fix_for', milestone.name) 431 | add_values_to_field(target, cf_name, project_id, milestones, 432 | lambda bundle, value: _to_yt_version(bundle, value)) 433 | 434 | cf_name = get_yt_field_name('area') 435 | areas = source.get_areas(project_id) 436 | value_sets["area"] = [] 437 | for area in areas: 438 | value_sets["area"].append(area.name) 439 | area.name = to_yt_field_value('area', area.name) 440 | add_values_to_field(target, cf_name, project_id, areas, 441 | lambda bundle, value: _to_yt_subsystem(bundle, value)) 442 | 443 | print('Importing issues for project [ %s ]' % project_name) 444 | start = 0 445 | issues_to_import = [] 446 | # create dictionary with child : parent pairs 447 | while start <= max_issue_id: 448 | fb_issues = source.get_issues(project_name, start, 30) 449 | for issue in fb_issues : 450 | add_values_to_field(target, get_yt_field_name('area'), project_id, 451 | [issue.field_values['area']], lambda bundle, value: bundle.createElement(value)) 452 | issues_to_import.append(_to_yt_issue(issue, value_sets)) 453 | target.importIssues(project_id, project_name.encode('utf-8') + " assignees", issues_to_import) 454 | for issue in fb_issues : 455 | full_issue_id = '%s-%s' % (project_id, issue.ix_bug) 456 | for attach in issue.attachments : 457 | target.createAttachmentFromAttachment(full_issue_id, attach) 458 | for tag in issue.tags : 459 | target.executeCommand(full_issue_id, 'tag ' + tag) 460 | if issue.bug_parent is not None: 461 | parent_issue_id = '%s-%s' % (source.get_issue_project_id(issue.bug_parent), issue.bug_parent) 462 | link = Link() 463 | link.typeName = 'parent-child' 464 | link.source = full_issue_id 465 | link.target = parent_issue_id 466 | links_to_import.append(link) 467 | issues_to_import = [] 468 | start += 30 469 | print('Importing issues for project [ %s ] finished' % project_name) 470 | 471 | print('Importing issue links') 472 | print(target.importLinks(links_to_import)) 473 | print('Importing issue links finished') 474 | 475 | 476 | if __name__ == '__main__': 477 | main() 478 | -------------------------------------------------------------------------------- /youtrackutils/fbugz/__init__.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | from time import time 3 | import urllib2 4 | import warnings 5 | 6 | warnings.warn("deprecated", DeprecationWarning) 7 | 8 | CATEGORY = dict([]) 9 | PRIORITY = dict([]) 10 | STATUS = dict([]) 11 | CF_NAMES = dict([]) 12 | CF_TYPES = dict([]) 13 | CF_VALUES = dict([]) 14 | PROJECTS_TO_IMPORT = list([]) 15 | 16 | class FBArea(object) : 17 | def __init__(self, name) : 18 | self.name = name 19 | self.person_owner = "" 20 | self.n_type = None 21 | 22 | class FBUser(object) : 23 | def __init__(self, login): 24 | self.login = login 25 | self.email = '' 26 | self.user_type = 'Normal' 27 | self.id = "" 28 | 29 | class FBMilestone(object) : 30 | def __init__(self, name) : 31 | self.name = name 32 | self.deleted = False 33 | self.inactive = False 34 | self.release_date = str(int(time())) 35 | 36 | class FBCustomField(object) : 37 | def __init__(self, name, column_name) : 38 | self.name = name 39 | self.column_name = column_name 40 | self.type = 0 41 | self.possible_values = list([]) 42 | 43 | class FBIssue(object) : 44 | def __init__(self, ix_bug) : 45 | self.ix_bug = ix_bug 46 | self.bug_parent = None 47 | self.tags = [] 48 | self.open = True 49 | self.title = 'title' 50 | self.field_values = dict([]) 51 | self.original_title = None 52 | self.latest_text_summary = 'None' 53 | self.area = 'No subsystem' 54 | self.assignee = '' 55 | self.status = None 56 | self.priority = None 57 | self.fix_for = None 58 | self.first_field = '' 59 | self.second_field = '' 60 | self.category = '' 61 | self.opened = int(time()) 62 | self.resolved = int(time()) 63 | self.closed = int(time()) 64 | self.due = int(time()) 65 | self.reporter = 'guest' 66 | self.attachments = [] 67 | self.comments = [] 68 | self.version = None 69 | self.computer = None 70 | 71 | 72 | class FBAttachment(object) : 73 | def __init__(self, base_url, url) : 74 | self._url = base_url + url 75 | parsed_url = urllib2.urlparse.urlparse(self._url) 76 | parse_qs = cgi.parse_qs(parsed_url.query) 77 | file_name_arg = 'sFileName' 78 | if file_name_arg in parse_qs.keys(): 79 | attachment_name = parse_qs[file_name_arg][0] 80 | if isinstance(attachment_name, unicode): 81 | attachment_name = attachment_name.encode('utf-8') 82 | self.name = attachment_name 83 | else: 84 | self.name = 'Attachment' 85 | self.authorLogin = 'guest' 86 | self.token = 'no_token_available' 87 | 88 | def getContent(self) : 89 | url = self._url.replace('&', '&') + '&token=' + self.token 90 | f = urllib2.urlopen(urllib2.Request(url)) 91 | return f 92 | 93 | class FBComment(object) : 94 | def __init__(self) : 95 | self.author = 'guest' 96 | self.date = time() 97 | self.text = '' 98 | 99 | -------------------------------------------------------------------------------- /youtrackutils/fbugz/defaultFBugz.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import fbugz 2 | 3 | fbugz.CF_NAMES = { 4 | u'assignee' : u'Assignee', 5 | u'area' : u'Subsystem', 6 | u'category' : u'Type', 7 | u'fix_for' : u'Fix versions', 8 | u'priority' : u'Priority', 9 | u'status' : u'State', 10 | u'due' : u'Due date', 11 | u'original_title' : u'Original title', 12 | u'version' : u'Version', 13 | u'computer' : u'Computer', 14 | u'estimate' : u'Estimate' 15 | } 16 | 17 | fbugz.CF_TYPES = { 18 | u'Assignee' : 'user[1]', 19 | u'Subsystem' : 'ownedField[1]', 20 | u'Fix versions' : 'version[*]', 21 | u'Priority' : 'enum[1]', 22 | u'State' : 'state[1]', 23 | u'Due date' : 'date', 24 | u'Original title' : 'string', 25 | u'Version' : 'string', 26 | u'Computer' : 'string', 27 | u'Estimate' : 'float', 28 | u'Type' : 'enum[1]' 29 | } 30 | 31 | fbugz.PROJECTS_TO_IMPORT = ["Inbox"] 32 | 33 | #fbugz.CF_NAMES = { 34 | # 'ix_bug' : 'numberInProject', 35 | # 'title' : 'summary', 36 | # 'opened' : 'created', 37 | # 'reporter' : 'reporter', 38 | # 'assignee' : 'Assignee', 39 | # 'area' : 'Subsystem', 40 | # 'category' : 'Type', 41 | # 'fix_for' : 'Fix versions', 42 | # 'priority' : 'Priority', 43 | # 'status' : 'State', 44 | # 'due' : 'Due date', 45 | # 'original_title' : 'Original title', 46 | # 'version' : 'Version', 47 | # 'computer' : 'Computer', 48 | # 'estimate' : 'Estimate' 49 | #} 50 | # 51 | #fbugz.CF_TYPES = { 52 | # 'Assignee' : 'user[1]', 53 | # 'Subsystem' : 'ownedField[1]', 54 | # 'Fix versions' : 'version[*]', 55 | # 'Priority' : 'enum[1]', 56 | # 'State' : 'state[1]', 57 | # 'Due date' : 'date', 58 | # 'Original title' : 'string', 59 | # 'Version' : 'string', 60 | # 'Computer' : 'string', 61 | # 'Estimate' : 'int' 62 | #} 63 | # 64 | #CATEGORY = { 65 | # 'Feature' : 'Feature', 66 | # 'Bug' : 'Bug', 67 | # 'Inquiry' : 'Feature', 68 | # 'Schedule Item' : 'Task' 69 | #} 70 | # 71 | #PRIORITY = { 72 | # 1 : 'Show-stopper', 73 | # 2 : 'Critical', 74 | # 3 : 'Critical', 75 | # 4 : 'Major', 76 | # 5 : 'Normal', 77 | # 6 : 'Normal', 78 | # 7 : 'Minor' 79 | #} 80 | # 81 | #STATUS = { 82 | # "Active" : "Open", 83 | # "Fixed" : "Fixed", 84 | # "Implemented" : "Fixed", 85 | # "Responded" : "Fixed", 86 | # "Completed" : "Fixed", 87 | # "Not Reproducible" : "Can't reproduce", 88 | # "Duplicate" : "Duplicate", 89 | # "Already Exists" : "Duplicate", 90 | # "Postponed" : "Submitted", 91 | # "Won't Fix" : "Won't Fix", 92 | # "By Design" : "Won't Fix", 93 | # "Won't Implement" : "Won't Fix", 94 | # "Won't Respond" : "Won't Fix", 95 | # "SPAM" : "Won't Fix", 96 | # "Canceled" : "Won't fix", 97 | # "Waiting For Info" : "Incomplete" 98 | #} 99 | # 100 | #fbugz.CF_VALUES = { 101 | # 'Type' : CATEGORY, 102 | # 'Priority' : PRIORITY, 103 | # 'State' : STATUS 104 | #} 105 | # 106 | -------------------------------------------------------------------------------- /youtrackutils/fbugz/fbSOAPClient.py: -------------------------------------------------------------------------------- 1 | import re 2 | from youtrackutils.fbugz.fogbugz import FogBugz 3 | from youtrackutils.fbugz import FBUser, FBArea, FBMilestone, FBIssue, FBComment, FBAttachment 4 | from datetime import datetime 5 | import calendar 6 | 7 | 8 | class FBClient(object): 9 | def __init__(self, source_url, source_login, source_password): 10 | self._source_url = source_url 11 | self._client = FogBugz(source_url) 12 | self._client.logon(source_login, source_password) 13 | self._case_ids = [] 14 | 15 | def list_project_names(self): 16 | projects = dict([]) 17 | for p in self._client.listProjects().findAll('project'): 18 | projects[p.sproject.string.strip()] = p.ixproject.string.strip() 19 | return projects 20 | 21 | def get_users(self): 22 | self._users = [] 23 | try: 24 | for p in self._client.listPeople(fIncludeNormal=1).findAll('person'): 25 | self._users.append(self._create_user(p, 'Normal')) 26 | except: 27 | print "Can't get Normal users" 28 | 29 | try: 30 | for p in self._client.listPeople(fIncludeNormal=0, fIncludeDeleted=1).findAll('person'): 31 | self._users.append(self._create_user(p, 'Deleted')) 32 | except: 33 | print "Can't get Deleted users" 34 | 35 | try: 36 | for p in self._client.listPeople(fIncludeNormal=0, fIncludeVirtual=1).findAll('person'): 37 | self._users.append(self._create_user(p, 'Virtual')) 38 | except: 39 | print "Can't get Virtual users" 40 | 41 | try: 42 | for p in self._client.listPeople(fIncludeNormal=0, fIncludeCommunity=1).findAll('person'): 43 | self._users.append(self._create_user(p, 'Community')) 44 | except: 45 | print "Can't get Community users" 46 | 47 | if 'FogBugz' not in [u.login for u in self._users]: 48 | self._users.append(FBUser('FogBugz')) 49 | 50 | return self._users 51 | 52 | def get_areas(self, ix_project): 53 | result = [] 54 | for a in self._client.listAreas(ixproject=ix_project).findAll('area'): 55 | area = FBArea(a.sarea.string.encode('utf-8').decode('utf-8')) 56 | owner = a.spersonowner.string 57 | if owner is None: 58 | area.person_owner = None 59 | else: 60 | area.person_owner = self.convert_login(owner.encode('utf-8').decode('utf-8')) 61 | result.append(area) 62 | return result 63 | 64 | def get_milestones(self, ix_project): 65 | result = [] 66 | for m in self._client.listFixFors(ixProject=ix_project, fIncludeDeleted=1, fIncludeReallyDeleted=1).findAll( 67 | "fixfor"): 68 | milestone = FBMilestone(m.sfixfor.string.encode('utf-8').decode('utf-8')) 69 | dt = m.dt.string 70 | milestone.release_date = self._to_unix_date(dt) 71 | milestone.inactive = bool(m.fdeleted.string) 72 | result.append(milestone) 73 | return result 74 | 75 | def get_issues(self, project_name, start_id, num): 76 | result = [] 77 | cols_string = "ixBug,sArea,sPersonAssignedTo,ixBugParent,sCategory,dtClosed,sComputer,sVersion,dtDue,sFixFor," 78 | cols_string += "sLatestTextSummary,fOpen,dtOpened,dtResolved,ixPriority,sPriority,sStatus,sOriginalTitle,sTitle,tags," 79 | cols_string += "ixPersonOpenedBy,ixPersonResolvedBy,hrsCurrEst" 80 | 81 | for i in self._client.search(cols=cols_string, q=( 82 | 'case:%d..%d project:"%s"' % (start_id, start_id + num - 1, project_name))).findAll('case'): 83 | ix_bug = i.ixbug.string 84 | self._case_ids.append(ix_bug) 85 | issue = FBIssue(ix_bug) 86 | issue.field_values['area'] = i.sarea.string.encode('utf-8').decode('utf-8') 87 | assignee = i.spersonassignedto.string 88 | if assignee is not None: 89 | if assignee == 'CLOSED': 90 | issue.field_values[u'assignee'] = self.convert_login( 91 | self._find_user_login(i.ixpersonresolvedby.string)) 92 | else: 93 | issue.field_values[u'assignee'] = self.convert_login(assignee.encode('utf-8')) 94 | parent_id = i.ixbugparent.string 95 | if parent_id != '0': 96 | issue.bug_parent = parent_id 97 | issue.field_values[u'category'] = i.scategory.string 98 | issue.field_values[u'estimate'] = i.hrscurrest.string 99 | issue.closed = self._to_unix_date(i.dtclosed.string) 100 | computer = i.scomputer.string 101 | if computer is not None: 102 | issue.field_values[u'computer'] = computer.encode('utf-8') 103 | sversion_string = i.sversion.string 104 | if sversion_string is not None: 105 | issue.field_values['version'] = sversion_string.encode('utf-8') 106 | due_dt = i.dtdue.string 107 | if (due_dt is None) or (due_dt.strip() == ''): 108 | issue.field_values[u'due'] = None 109 | else: 110 | issue.field_values[u'due'] = self._to_unix_date(due_dt) 111 | issue.field_values[u'fix_for'] = i.sfixfor.string.encode('utf-8').decode('utf-8') 112 | latest_summary = i.slatesttextsummary.string 113 | if latest_summary is not None: 114 | issue.latest_text_summary = latest_summary.encode('utf-8') 115 | issue.open = bool(i.fopen.string) 116 | issue.opened = self._to_unix_date(i.dtopened.string) 117 | issue.resolved = self._to_unix_date(i.dtresolved.string) 118 | issue.field_values[u'priority'] = i.ixpriority.string.encode('utf-8').decode( 119 | 'utf-8') + u'-' + i.spriority.string.encode('utf-8').decode('utf-8') 120 | issue.field_values[u'status'] = i.sstatus.string 121 | original = i.soriginaltitle.string 122 | if original is not None: 123 | issue.field_values[u'original_title'] = original.encode('utf-8').decode('utf-8') 124 | else: 125 | issue.field_values[u'original_title'] = None 126 | issue.title = i.stitle.string.encode('utf-8').decode('utf-8') 127 | events = self._client.search(q=ix_bug, cols='events').findAll('event') 128 | issue.attachments = self._get_attachments_from_events(events) 129 | issue.comments = self._get_comments_from_events(events) 130 | for tag in i.tags.findAll('tag'): 131 | issue.tags.append(tag.string) 132 | issue.reporter = self.convert_login(self._find_user_login(i.ixpersonopenedby.string)) 133 | result.append(issue) 134 | 135 | return result 136 | 137 | def get_issue_project_id(self, ix_bug): 138 | return self._client.search(q=ix_bug, cols='ixProject').case.ixproject.string 139 | 140 | def list_priorities(self): 141 | return [(elem.ixpriority.string.encode('utf-8').decode('utf-8'), 142 | elem.spriority.string.encode('utf-8').decode('utf-8')) for elem in 143 | self._client.listPriorities().findAll( 144 | 'priority')] 145 | 146 | def list_categories(self): 147 | return [elem.scategory.string.encode('utf-8').decode('utf-8') for elem in 148 | self._client.listCategories().findAll('category')] 149 | 150 | def list_statuses(self): 151 | statuses = [ 152 | (elem.sstatus.string.encode('utf-8').decode('utf-8'), elem.fresolved.string.encode('utf-8') == 'true') for 153 | elem 154 | in 155 | self._client.listStatuses().findAll( 156 | 'status')] 157 | resulting_statuses = [] 158 | for status in statuses: 159 | resulting_statuses.append(status) 160 | if status[1]: 161 | match_result = re.match("Resolved \((.*)\)", status[0]) 162 | if match_result is not None: 163 | resulting_statuses.append(("Closed (%s)" % match_result.groups()[0], True)) 164 | else: 165 | resulting_statuses.append(("Closed (%s)" % status[0], True)) 166 | return resulting_statuses 167 | 168 | def _get_comments_from_events(self, events): 169 | comments = [] 170 | for event in events: 171 | if hasattr(event, 's'): 172 | comment = FBComment() 173 | content = event.s.string 174 | if (content is None) or (content.strip() == ''): 175 | continue 176 | comment.text = content.encode('utf-8').decode('utf-8') 177 | comment.author = self.convert_login(event.sperson.string.encode('utf-8')) 178 | comment.date = self._to_unix_date(event.dt.string) 179 | comments.append(comment) 180 | return comments 181 | 182 | def _get_attachments_from_events(self, events): 183 | attachments = [] 184 | for event in events: 185 | for a in event.findAll('attachment'): 186 | attach = FBAttachment(self._source_url, a.surl.string) 187 | attach.authorLogin = self.convert_login(event.sperson.string) 188 | attach.token = self._client.get_token() 189 | attachments.append(attach) 190 | return attachments 191 | 192 | 193 | def _create_user(self, p, type): 194 | person = FBUser(self.convert_login(p.sfullname.string)) 195 | if len(p.semail.string): 196 | person.email = p.semail.string 197 | person.user_type = type 198 | person.id = p.ixperson.string 199 | return person 200 | 201 | def _find_user_login(self, id): 202 | if self._users is None: 203 | self.get_users() 204 | for user in self._users: 205 | if user.id == id: 206 | return user.login.encode('utf-8') 207 | return None 208 | 209 | def _to_unix_date(self, date_string): 210 | if date_string is None: 211 | dt = datetime.now() 212 | else: 213 | dt = datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') 214 | return str(calendar.timegm(dt.timetuple()) * 1000) 215 | 216 | def convert_login(self, old_login): 217 | if old_login is None: 218 | return None 219 | return old_login.replace(' ', '_') 220 | -------------------------------------------------------------------------------- /youtrackutils/fbugz/fogbugz.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import urllib2 3 | 4 | from BeautifulSoup import BeautifulSoup, CData 5 | 6 | class FogBugzAPIError(Exception): 7 | pass 8 | 9 | class FogBugzLogonError(FogBugzAPIError): 10 | pass 11 | 12 | class FogBugzConnectionError(FogBugzAPIError): 13 | pass 14 | 15 | class FogBugz: 16 | def __init__(self, url, token=None): 17 | self.__handlerCache = {} 18 | if not url.endswith('/'): 19 | url += '/' 20 | 21 | if token: 22 | self._token = token.encode('utf-8') 23 | else: 24 | self_token = None 25 | 26 | self._opener = urllib2.build_opener() 27 | try: 28 | soup = BeautifulSoup(self._opener.open(url + 'api.xml')) 29 | except urllib2.URLError: 30 | raise FogBugzConnectionError("Library could not connect to the FogBugz API. Either this installation of FogBugz does not support the API, or the url, %s, is incorrect." % (self._url,)) 31 | self._url = url + soup.response.url.string 32 | self.currentFilter = None 33 | 34 | def logon(self, username, password): 35 | """ 36 | Logs the user on to FogBugz. 37 | 38 | Returns None for a successful login. 39 | """ 40 | if self._token: 41 | self.logoff() 42 | try: 43 | response = self.__makerequest('logon', email=username, password=password) 44 | except FogBugzAPIError, e: 45 | raise FogBugzLogonError(e) 46 | 47 | self._token = response.token.string 48 | if type(self._token) == CData: 49 | self._token = self._token.encode('utf-8') 50 | 51 | def logoff(self): 52 | """ 53 | Logs off the current user. 54 | """ 55 | self.__makerequest('logoff') 56 | self._token = None 57 | 58 | def token(self,token): 59 | """ 60 | Set the token without actually logging on. More secure. 61 | """ 62 | self._token = token.encode('utf-8') 63 | 64 | def __makerequest(self, cmd, **kwargs): 65 | kwargs["cmd"] = cmd 66 | if self._token: 67 | kwargs["token"] = self._token 68 | 69 | try: 70 | response = BeautifulSoup(self._opener.open(self._url+urllib.urlencode(dict([k, v.encode('utf-8') if isinstance(v,basestring) else v ] for k, v in kwargs.items())))).response 71 | except urllib2.URLError, e: 72 | raise FogBugzConnectionError(e) 73 | except UnicodeDecodeError, e: 74 | print kwargs 75 | raise 76 | 77 | if response.error: 78 | raise FogBugzAPIError('Error Code %s: %s' % (response.error['code'], response.error.string,)) 79 | return response 80 | 81 | def __getattr__(self, name): 82 | """ 83 | Handle all FogBugz API calls. 84 | 85 | >>> fb.logon(email@example.com, password) 86 | >>> response = fb.search(q="assignedto:email") 87 | """ 88 | 89 | # Let's leave the private stuff to Python 90 | if name.startswith("__"): 91 | raise AttributeError("No such attribute '%s'" % name) 92 | 93 | if not self.__handlerCache.has_key(name): 94 | def handler(**kwargs): 95 | return self.__makerequest(name, **kwargs) 96 | self.__handlerCache[name] = handler 97 | return self.__handlerCache[name] 98 | 99 | 100 | def get_token(self) : 101 | return self._token -------------------------------------------------------------------------------- /youtrackutils/github2youtrack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import getopt 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | import os 10 | import re 11 | import requests 12 | import csv 13 | import youtrackutils.csvClient 14 | import csv2youtrack 15 | from youtrack.importHelper import utf8encode 16 | 17 | youtrackutils.csvClient.FIELD_NAMES = { 18 | "Project Name" : "project_name", 19 | "Project Id" : "project_id", 20 | "Summary" : "summary", 21 | "State" : "State", 22 | "Id" : "numberInProject", 23 | "Created" : "created", 24 | "Updated" : "updated", 25 | "Resolved" : "resolved", 26 | "Assignee" : "Assignee", 27 | "Description" : "description", 28 | "Labels" : "Labels", 29 | "Author" : "reporterName", 30 | "Milestone" : "Fix versions" 31 | } 32 | 33 | youtrackutils.csvClient.FIELD_TYPES = { 34 | "State" : "state[1]", 35 | "Assignee" : "user[1]", 36 | "Labels" : "enum[*]", 37 | "Fix versions" : "version[*]", 38 | "Type" : "enum[1]" 39 | } 40 | 41 | youtrackutils.csvClient.DATE_FORMAT_STRING = "%Y-%m-%dT%H:%M:%SZ" 42 | youtrackutils.csvClient.VALUE_DELIMITER = "|" 43 | youtrackutils.csvClient.USE_MARKDOWN = True 44 | 45 | CSV_FILE = "github2youtrack-{repo}-{data}.csv" 46 | 47 | help_url = "\ 48 | https://www.jetbrains.com/help/youtrack/standalone/import-from-github.html" 49 | 50 | 51 | def usage(): 52 | basename = os.path.basename(sys.argv[0]) 53 | 54 | print(""" 55 | Usage: 56 | %s [OPTIONS] yt_url gh_login gh_password gh_repo 57 | 58 | yt_url YouTrack base URL 59 | 60 | gh_login The username to log in to GitHub 61 | 62 | gh_password The password to log in to GitHub 63 | 64 | gh_repo The name of the GitHub repository to import issues from 65 | 66 | For instructions, see: 67 | %s 68 | 69 | Options: 70 | -h, Show this help and exit 71 | -T TOKEN_FILE, 72 | Path to file with permanent token 73 | -t TOKEN, 74 | Value for permanent token as text 75 | -u LOGIN, 76 | YouTrack user login to perform import on behalf of 77 | -p PASSWORD, 78 | YouTrack user password 79 | 80 | Examples: 81 | 82 | $ %s -T token https://youtrack.company.com gh-user gh-pass test-repo 83 | 84 | 85 | """ % (basename, help_url, basename)) 86 | 87 | 88 | def main(): 89 | try: 90 | params = {} 91 | opts, args = getopt.getopt(sys.argv[1:], 'hu:p:t:T:') 92 | for opt, val in opts: 93 | if opt == '-h': 94 | usage() 95 | sys.exit(0) 96 | elif opt == '-u': 97 | params['login'] = val 98 | elif opt == '-p': 99 | params['password'] = val 100 | elif opt == '-t': 101 | params['token'] = val 102 | elif opt == '-T': 103 | check_file_and_save(val, params, 'token_file') 104 | except getopt.GetoptError as e: 105 | print(e) 106 | usage() 107 | sys.exit(1) 108 | 109 | try: 110 | params['target_url'], github_user, github_password, github_repo = args 111 | except (ValueError, KeyError, IndexError): 112 | print("Bad arguments") 113 | usage() 114 | sys.exit(1) 115 | 116 | if github_repo.find('/') > -1: 117 | github_repo_owner, github_repo = github_repo.split('/') 118 | else: 119 | github_repo_owner = github_user 120 | 121 | params['issues_file'] = CSV_FILE.format(repo=github_repo, data='issues') 122 | params['comments_file'] = CSV_FILE.format(repo=github_repo, data='comments') 123 | 124 | github2csv(params['issues_file'], 125 | params['comments_file'], 126 | github_user, 127 | github_password, 128 | github_repo, 129 | github_repo_owner) 130 | 131 | csv2youtrack.csv2youtrack(params) 132 | 133 | 134 | def check_file_and_save(filename, params, key): 135 | try: 136 | params[key] = os.path.abspath(filename) 137 | except (OSError, IOError) as e: 138 | print("Data file is not accessible: " + str(e)) 139 | print(filename) 140 | sys.exit(1) 141 | 142 | 143 | def get_last_part_of_url(url_string): 144 | return url_string.split('/').pop() 145 | 146 | 147 | # based on https://gist.github.com/unbracketed/3380407 148 | def write_issues(r, issues_csvout, comments_csvout, repo, auth): 149 | """output a list of issues to csv""" 150 | if not r.status_code == 200: 151 | raise Exception(r.status_code) 152 | for issue in r.json(): 153 | labels = [] 154 | labels_lowercase = [] 155 | for label in issue['labels']: 156 | label_name = label.get('name') 157 | if not label_name: 158 | continue 159 | labels.append(label_name) 160 | labels_lowercase.append(label_name) 161 | 162 | # TODO: Join writerow 163 | #labels = csvClient.VALUE_DELIMITER.join([str(x) for x in labels]) 164 | 165 | assignee = issue['assignee'] 166 | if assignee: 167 | assignee = assignee.get('login') 168 | else: 169 | assignee = "" 170 | 171 | created = issue['created_at'] 172 | updated = issue.get('updated_at', '') 173 | resolved = issue.get('closed_at', '') 174 | 175 | author = issue['user'].get('login') 176 | if not author: 177 | author = get_last_part_of_url(issue['user'].get('url')) 178 | 179 | project = re.sub(r'[^\w]', '_', get_last_part_of_url(repo)) 180 | 181 | milestone = issue.get('milestone') 182 | if milestone: 183 | milestone = milestone['title'] 184 | else: 185 | milestone = '' 186 | 187 | state = issue['state'].lower() 188 | if state == 'closed': 189 | if 'wontfix' in labels_lowercase or 'invalid' in labels_lowercase: 190 | state = "Won't fix" 191 | else: 192 | state = "Fixed" 193 | 194 | issue_type = 'Task' 195 | if 'bug' in labels_lowercase: 196 | issue_type = 'Bug' 197 | 198 | issue_row = [project, project, issue['number'], state, issue['title'], 199 | issue['body'], created, updated, resolved, author or 'guest', 200 | assignee, youtrackutils.csvClient.VALUE_DELIMITER.join(labels), 201 | issue_type, milestone] 202 | issues_csvout.writerow([utf8encode(e) for e in issue_row]) 203 | 204 | if int(issue.get('comments', 0)) > 0 and 'comments_url' in issue: 205 | rc = requests.get(issue['comments_url'], auth=auth) 206 | if not rc.status_code == 200: 207 | raise Exception(r.status_code) 208 | for comment in rc.json(): 209 | author = comment['user'].get('login') 210 | if not author: 211 | author = get_last_part_of_url(comment['user'].get(u'url')) 212 | comment_row = [project, issue['number'], author or 'guest', 213 | comment['created_at'], comment['body']] 214 | comments_csvout.writerow([utf8encode(e) for e in comment_row]) 215 | 216 | 217 | def github2csv(issues_csv_file, comments_csv_file, github_user, github_password, github_repo, github_repo_owner): 218 | issues_url = 'https://api.github.com/repos/%s/%s/issues?state=all' % (github_repo_owner, github_repo) 219 | AUTH = (github_user, github_password) 220 | 221 | r = requests.get(issues_url, auth=AUTH) 222 | issues_csvout = csv.writer(open(issues_csv_file, 'wb')) 223 | issues_csvout.writerow( 224 | ('Project Name', 'Project Id', 'Id', 'State', 'Summary', 'Description', 225 | 'Created', 'Updated', 'Resolved', 'Author', 'Assignee', 'Labels', 226 | 'Type', 'Milestone')) 227 | comments_csvout = csv.writer(open(comments_csv_file, 'wb')) 228 | comments_csvout.writerow( 229 | ('Project Id', 'Id', 'Author', 'Created', 'Text')) 230 | write_issues(r, issues_csvout, comments_csvout, github_repo, AUTH) 231 | 232 | #more pages? examine the 'link' header returned 233 | if 'link' in r.headers: 234 | pages = dict( 235 | [(rel[6:-1], url[url.index('<')+1:-1]) for url, rel in 236 | [link.split(';') for link in 237 | r.headers['link'].split(',')]]) 238 | while 'last' in pages and 'next' in pages: 239 | r = requests.get(pages['next'], auth=AUTH) 240 | write_issues(r, issues_csvout, comments_csvout, github_repo, AUTH) 241 | pages = dict( 242 | [(rel[6:-1], url[url.index('<') + 1:-1]) for url, rel in 243 | [link.split(';') for link in 244 | r.headers['link'].split(',')]]) 245 | 246 | 247 | if __name__ == "__main__": 248 | main() 249 | -------------------------------------------------------------------------------- /youtrackutils/mantis/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) 4 | 5 | CF_TYPES = dict([]) 6 | LINK_TYPES = dict([]) 7 | CREATE_CF_FOR_SUBPROJECT = True 8 | CHARSET = "cp866" 9 | FIELD_NAMES = dict([]) 10 | FIELD_TYPES = dict([]) 11 | FIELD_VALUES = dict([]) 12 | 13 | class MantisUser(object) : 14 | 15 | def __init__(self, name) : 16 | self.user_name = name 17 | self.real_name = "" 18 | self.email = "" 19 | 20 | class MantisCategory(object) : 21 | def __init__(self, name) : 22 | self.name = name 23 | self.assignee = None 24 | 25 | class MantisVersion(object) : 26 | def __init__(self, name) : 27 | self.name = name 28 | self.is_released = True 29 | self.is_obsolete = False 30 | 31 | class MantisCustomFieldDef(object) : 32 | def __init__(self, id) : 33 | self.name = None 34 | self.type = None 35 | self.values = None 36 | self.field_id = id 37 | self.default_value = None 38 | 39 | class MantisComment(object) : 40 | def __init__(self) : 41 | self.reporter = "" 42 | self.date_submitted = None 43 | self.text = "" 44 | 45 | class MantisIssueLink(object) : 46 | def __init__(self, source, target, type): 47 | self.source = source 48 | self.target = target 49 | self.source_project_id = None 50 | self.target_project_id = None 51 | self.type = type 52 | 53 | class MantisAttachment(object): 54 | def __init__(self, id): 55 | self.id = id 56 | self.title = "" 57 | self.filename = "" 58 | self.file_type = "" 59 | self.content = None 60 | self.user_id = "" 61 | self.date_added = "" 62 | -------------------------------------------------------------------------------- /youtrackutils/mantis/defaultMantis.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import mantis 2 | 3 | # maps the cf type in mantis with the cf type in yt 4 | mantis.CF_TYPES = { 5 | 0 : "string", #String 6 | 1 : "integer", #Nimeric 7 | 2 : "string", #Float 8 | 3 : "enum[1]", #Enumeration 9 | 4 : "string", #Email 10 | 5 : "enum[*]", #Checkbox 11 | 6 : "enum[1]", #List 12 | 7 : "enum[*]", #Multiselection list 13 | 8 : "date", #Date 14 | 9 : "enum[1]" # Radio 15 | } 16 | 17 | 18 | PRIORITY_VALUES = { 19 | 10 : "none", 20 | 20 : "low", 21 | 30 : "normal", 22 | 40 : "high", 23 | 50 : "urgent", 24 | 60 : "immediate" 25 | } 26 | 27 | SEVERITY_VALUES = { 28 | 10 : "Feature", 29 | 20 : "Trivial", 30 | 30 : "Text", 31 | 40 : "Tweak", 32 | 50 : "Minor", 33 | 60 : "Major", 34 | 70 : "Crash", 35 | 80 : "Block", 36 | 90 : "Super Blocker" 37 | } 38 | 39 | REPRODUCIBILITY_VALUES = { 40 | 10 : "Always", 41 | 30 : "Sometimes", 42 | 50 : "Random", 43 | 70 : "Have not tried", 44 | 90 : "Unable to reproduce", 45 | 100 : "N/A" 46 | } 47 | 48 | STATUS_VALUES = { 49 | 10 : "new", 50 | 20 : "feedback", 51 | 30 : "acknowledged", 52 | 40 : "confirmed", 53 | 50 : "assigned", 54 | 60 : "resolved", 55 | 70 : "closed", 56 | 75 : "some_status_3", 57 | 80 : "some_status_1", 58 | 90 : "some_status_2" 59 | } 60 | 61 | RESOLUTION_VALUES = { 62 | 10 : "open", 63 | 20 : "fixed", 64 | 30 : "reopened", 65 | 40 : "unable to reproduce", 66 | 50 : "not fixable", 67 | 60 : "duplicate", 68 | 70 : "no change required", 69 | 80 : "suspended", 70 | 90 : "won't fix" 71 | } 72 | 73 | #maps mantis link types with yt link types 74 | mantis.LINK_TYPES = { 75 | 0 : "Duplicate", #duplicate of 76 | 1 : "Relates", #related to 77 | 2 : "Depend" #parent of 78 | } 79 | 80 | mantis.FIELD_NAMES = { 81 | u"severity" : u"Severity", 82 | u"handler_id" : u"Assignee", 83 | u"status" : u"State", 84 | u"resolution" : u"Resolution", 85 | u"category_id" : u"Subsystem", 86 | u"version" : u"Affected versions", 87 | u"fixed_in_version" : u"Fix versions", 88 | u"build" : u"Fixed in build", 89 | u"os_build" : u"OS version", 90 | u"os" : u"OS", 91 | u"due_date" : u"Mantis Due date", 92 | u"target_version" : u"Target version", # it's better to import this fields with version type 93 | u"priority" : u"Priority", 94 | u"platform" : u"Platform", 95 | u"last_updated" : u"updated", 96 | u"date_submitted" : u"created", 97 | u"reporter_id" : u"reporterName", 98 | u"id" : u"numberInProject", 99 | u'project_id' : u'Subproject' 100 | } 101 | 102 | mantis.FIELD_VALUES = { 103 | u"State" : STATUS_VALUES, 104 | u"Reproducibility" : REPRODUCIBILITY_VALUES, 105 | u"Priority" : PRIORITY_VALUES, 106 | u"Severity" : SEVERITY_VALUES, 107 | u"Resolution" : RESOLUTION_VALUES 108 | } 109 | 110 | mantis.FIELD_TYPES = { 111 | u"Priority" : "enum[1]", 112 | u"State" : "state[1]", 113 | u"Resolution" : "state[1]", 114 | u"Fix versions" : "version[*]", 115 | u"Affected versions" : "version[*]", 116 | u"Assignee" : "user[1]", 117 | u"Fixed in build" : "build[1]", 118 | u"Subsystem" : "ownedField[1]", 119 | u"Subproject" : "ownedField[1]", 120 | u"Severity" : "enum[1]", 121 | u"Platform" : "string", 122 | u"OS" : "string", 123 | u"OS version" : "string", 124 | u"Reproducibility" : "enum[1]", 125 | u"Mantis Due date" : "date", 126 | u"Target version" : "version[1]", 127 | } 128 | 129 | # charset of your mantis database 130 | mantis.CHARSET = "utf8" 131 | 132 | # If True then issues to import to YouTrack will be collected from a project 133 | # and all it's subprojects. 134 | # If False then subprojects' issues won't be taken in account. 135 | mantis.BATCH_SUBPROJECTS = True 136 | 137 | -------------------------------------------------------------------------------- /youtrackutils/mantis/mantisClient.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | import MySQLdb.cursors 3 | from youtrackutils.mantis import * 4 | 5 | 6 | class MantisClient(object): 7 | def __init__(self, host, port, login, password, db_name, charset_name, batch_subprojects): 8 | self.batch_subprojects = batch_subprojects 9 | self.sql_cnx = MySQLdb.connect(host=host, port=port, user=login, passwd=password, 10 | db=db_name, cursorclass=MySQLdb.cursors.DictCursor, charset=charset_name) 11 | 12 | 13 | def get_project_id_by_name(self, project_name): 14 | cursor = self.sql_cnx.cursor() 15 | id_row = "id" 16 | name_row = "name" 17 | request = "SELECT %s, %s FROM mantis_project_table" % (id_row, name_row,) 18 | cursor.execute(request) 19 | for row in cursor: 20 | if row[name_row].encode('utf8').strip() == project_name: 21 | return row[id_row] 22 | 23 | def _to_user(self, row): 24 | user = MantisUser(row["username"].replace(" ", "_")) 25 | user.real_name = row["realname"] 26 | user.email = row["email"] 27 | return user 28 | 29 | def get_mantis_categories(self, project_id): 30 | cursor = self.sql_cnx.cursor() 31 | project_ids_string = repr(self._calculate_project_ids(project_id)).replace('[', '(').replace(']', ')') 32 | name_row = "name" 33 | user_id_row = "user_id" 34 | request = "SELECT %s, %s FROM mantis_category_table WHERE project_id IN %s" % ( 35 | user_id_row, name_row, project_ids_string) 36 | cursor.execute(request) 37 | result = [] 38 | for row in cursor: 39 | category = MantisCategory(row[name_row]) 40 | user_id = row[user_id_row] 41 | if user_id: 42 | category.assignee = self.get_user_by_id(user_id) 43 | result.append(category) 44 | return result 45 | 46 | def get_mantis_versions(self, project_id): 47 | cursor = self.sql_cnx.cursor() 48 | project_ids_string = repr(self._calculate_project_ids(project_id)).replace('[', '(').replace(']', ')') 49 | version_row = "version" 50 | released_row = "released" 51 | obsolete_row = "obsolete" 52 | date_order = "date_order" 53 | request = "SELECT %s, %s, %s, %s FROM mantis_project_version_table " % ( 54 | version_row, released_row, obsolete_row, date_order) 55 | request += "WHERE project_id IN %s" % project_ids_string 56 | cursor.execute(request) 57 | result = [] 58 | for row in cursor: 59 | version = MantisVersion(row[version_row]) 60 | version.is_released = (row[released_row] > 0) 61 | version.is_obsolete = (row[obsolete_row] > 0) 62 | version.release_date = self._to_epoch_time(row[date_order]) 63 | result.append(version) 64 | return result 65 | 66 | def get_mantis_custom_fields(self, project_ids): 67 | cursor = self.sql_cnx.cursor() 68 | ids = set([]) 69 | for project_id in project_ids: 70 | ids = ids | set(self._calculate_project_ids(project_id)) 71 | project_ids_string = repr(list(ids)).replace('[', '(').replace(']', ')') 72 | cf_ids_request = "SELECT DISTINCT field_id FROM mantis_custom_field_project_table WHERE project_id IN " + project_ids_string 73 | id_row = "id" 74 | type_row = "type" 75 | name_row = "name" 76 | default_value_row = "default_value" 77 | possible_values_row = "possible_values" 78 | request = "SELECT %s, %s, %s, %s, %s " % (id_row, name_row, type_row, possible_values_row, default_value_row) 79 | request += "FROM mantis_custom_field_table WHERE %s IN (%s)" % (id_row, cf_ids_request) 80 | cursor.execute(request) 81 | result = [] 82 | for row in cursor: 83 | cf = MantisCustomFieldDef(row[id_row]) 84 | cf.type = row[type_row] 85 | cf.name = row[name_row] 86 | cf.default_value = row[default_value_row] 87 | if row[type_row] in [3, 6, 7, 9, 5]: 88 | # possible values 89 | values = row[possible_values_row].split("|") 90 | cf.values = [] 91 | for v in values: 92 | v = v.strip() 93 | if v != "": 94 | cf.values.append(v) 95 | result.append(cf) 96 | return result 97 | 98 | def get_custom_fields_attached_to_project(self, project_id): 99 | cursor = self.sql_cnx.cursor() 100 | project_ids = (repr(self._calculate_project_ids(project_id)).replace('[', '(').replace(']', ')')) 101 | field_id_row = "field_id" 102 | request = "SELECT DISTINCT %s FROM mantis_custom_field_project_table WHERE project_id IN %s" % ( 103 | field_id_row, project_ids) 104 | cursor.execute(request) 105 | result = [] 106 | for row in cursor: 107 | result.append(row[field_id_row]) 108 | return result 109 | 110 | 111 | def get_mantis_issues(self, project_id, after, max): 112 | cursor = self.sql_cnx.cursor() 113 | project_ids = (repr(self._calculate_project_ids(project_id)).replace('[', '(').replace(']', ')')) 114 | id_row = "id" 115 | project_id_row = "project_id" 116 | reporter_id_row = "reporter_id" 117 | handler_id_row = "handler_id" 118 | bug_text_id_row = "bug_text_id" 119 | category_id_row = "category_id" 120 | date_submitted_row = "date_submitted" 121 | due_date_row = "due_date" 122 | last_updated_row = "last_updated" 123 | 124 | rows_to_retrieve = [id_row, project_id_row, reporter_id_row, handler_id_row, bug_text_id_row, "summary", 125 | category_id_row, date_submitted_row, due_date_row, last_updated_row, "priority", "severity", 126 | "reproducibility", "status", "resolution", "os_build", "os", "platform", "version", 127 | "fixed_in_version", "build", "target_version"] 128 | 129 | 130 | 131 | request = "SELECT %s FROM mantis_bug_table WHERE project_id IN %s LIMIT %d OFFSET %d" % ( 132 | ", ".join(rows_to_retrieve), project_ids, max, after) 133 | cursor.execute(request) 134 | result = [] 135 | for row in cursor: 136 | row[id_row] = str(row[id_row]) 137 | row[reporter_id_row] = self.get_user_by_id(row[reporter_id_row]) 138 | row[handler_id_row] = self.get_user_by_id(row[handler_id_row]) 139 | 140 | row.update(self._get_text_fields(row[bug_text_id_row])) 141 | row[bug_text_id_row] = None 142 | 143 | row[project_id_row] = self._get_project_name_by_id(row[project_id_row]) 144 | row[category_id_row] = self._get_category_by_id(row[category_id_row]) 145 | 146 | row[date_submitted_row] = self._to_epoch_time(row[date_submitted_row]) 147 | row[due_date_row] = self._to_epoch_time(row[due_date_row]) 148 | row[last_updated_row] = self._to_epoch_time(row[last_updated_row]) 149 | 150 | row.update(self._get_cf_values(row[id_row])) 151 | 152 | row["comments"] = self._get_comments_by_id(row[id_row]) 153 | 154 | result.append(row) 155 | return result 156 | 157 | def get_mantis_subprojects(self, project_id): 158 | cursor = self.sql_cnx.cursor() 159 | project_ids = (repr(self._calculate_project_ids(project_id)).replace('[', '(').replace(']', ')')) 160 | name_row = "name" 161 | request = "SELECT %s FROM mantis_project_table WHERE id IN %s" % (name_row, project_ids) 162 | cursor.execute(request) 163 | result = [] 164 | for row in cursor: 165 | result.append(row[name_row]) 166 | return result 167 | 168 | def _get_cf_values(self, bug_id): 169 | result = {} 170 | cf_cursor = self.sql_cnx.cursor() 171 | cf_cursor.execute("SELECT field_id, value FROM mantis_custom_field_string_table WHERE bug_id=%s", 172 | (bug_id,)) 173 | for row in cf_cursor: 174 | issue_cf = self._get_cf_name_by_id(row["field_id"]) 175 | value = row["value"] 176 | cf_name = issue_cf["name"] 177 | if issue_cf["type"] in [3, 6, 7, 9, 5]: 178 | values = value.split("|") 179 | result[cf_name] = [] 180 | for v in values: 181 | v = v.strip() 182 | if v != "": 183 | result[cf_name].append(v) 184 | elif issue_cf["type"] == 8: 185 | result[cf_name] = self._to_epoch_time(value) if len(value) else "" 186 | else: 187 | result[cf_name] = value 188 | return result 189 | 190 | def get_issue_links(self, after, max): 191 | cursor = self.sql_cnx.cursor() 192 | result = [] 193 | cursor.execute("SELECT * FROM mantis_bug_relationship_table LIMIT %d OFFSET %d" % (max, after)) 194 | for row in cursor: 195 | source_bug_id = row["source_bug_id"] 196 | target_bug_id = row["destination_bug_id"] 197 | link = MantisIssueLink(source_bug_id, target_bug_id, row["relationship_type"]) 198 | link.source_project_id = self._get_project_id_by_bug_id(source_bug_id) 199 | link.target_project_id = self._get_project_id_by_bug_id(target_bug_id) 200 | result.append(link) 201 | return result 202 | 203 | def get_attachments(self, bug_id): 204 | cursor = self.sql_cnx.cursor() 205 | id_row = "id" 206 | title_row = "title" 207 | filename_row = "filename" 208 | file_type_row = "file_type" 209 | content_row = "content" 210 | user_id_row = "user_id" 211 | date_added_row = "date_added" 212 | diskfile_row = "diskfile" 213 | folder_row = "folder" 214 | rows_to_get = [id_row, title_row, diskfile_row, folder_row, filename_row, file_type_row, content_row, user_id_row, date_added_row] 215 | request = "SELECT %s FROM mantis_bug_file_table WHERE bug_id=%s" % (", ".join(rows_to_get), bug_id) 216 | cursor.execute(request) 217 | result = [] 218 | for row in cursor: 219 | try: 220 | attachment = MantisAttachment(row[id_row]) 221 | attachment.title = row[title_row] 222 | attachment.filename = row[filename_row] 223 | attachment.file_type = row[file_type_row] 224 | attachment.author = self.get_user_by_id(row[user_id_row]) 225 | attachment.date_added = self._to_epoch_time(row[date_added_row]) 226 | if row[content_row]: 227 | attachment.content = row[content_row] 228 | else: 229 | file_path = row[folder_row].rstrip("/") + "/" + row[diskfile_row] 230 | with open(file_path.encode('utf-8')) as f: 231 | attachment.content = f.read() 232 | except: 233 | print "Skip attach" 234 | result.append(attachment) 235 | return result 236 | 237 | def get_project_description(self, project_id): 238 | cursor = self.sql_cnx.cursor() 239 | description_row = "description" 240 | cursor.execute("SELECT %s FROM mantis_project_table WHERE id=%s LIMIT 1", (description_row, project_id)) 241 | description = cursor.fetchone()[description_row] 242 | if description is None: 243 | return "empty description" 244 | return description.encode('utf8') 245 | 246 | def get_user_by_id(self, id): 247 | if id: 248 | cursor = self.sql_cnx.cursor() 249 | request = "SELECT * FROM mantis_user_table WHERE id=%s LIMIT 1" % str(id) 250 | cursor.execute(request) 251 | element = cursor.fetchone() 252 | if element is not None: 253 | return self._to_user(element) 254 | return None 255 | 256 | def _calculate_project_ids(self, project_id): 257 | result = [int(project_id)] 258 | if self.batch_subprojects: 259 | result.extend(self._get_child_projects_by_project_id(project_id)) 260 | # TODO: Why do we add projectid=0? Invesigate it! 261 | #result.append(int(0)) 262 | return result 263 | 264 | def _get_child_projects_by_project_id(self, id): 265 | cursor = self.sql_cnx.cursor() 266 | child_id_row = "child_id" 267 | request = "SELECT %s FROM mantis_project_hierarchy_table h\ 268 | WHERE parent_id = %s AND EXISTS (\ 269 | SELECT * FROM mantis_project_table p WHERE p.id = h.%s)" % (child_id_row, id, child_id_row) 270 | cursor.execute(request) 271 | result = [] 272 | for row in cursor: 273 | result.append(int(row[child_id_row])) 274 | result.extend(self._get_child_projects_by_project_id(row[child_id_row])) 275 | return result 276 | 277 | 278 | def _get_text_fields(self, text_id): 279 | cursor = self.sql_cnx.cursor() 280 | description_row = "description" 281 | steps_row = "steps_to_reproduce" 282 | additional_row = "additional_information" 283 | request = "SELECT %s, %s, %s " % (description_row, steps_row, additional_row) 284 | request += "FROM mantis_bug_text_table WHERE id=%s LIMIT 1" % str(text_id) 285 | cursor.execute(request) 286 | row = cursor.fetchone() 287 | description = row[description_row] 288 | if (row[steps_row] is not None) and len(row[steps_row]): 289 | description += "\n Steps to reproduce : \n" + row[steps_row] 290 | if (row[additional_row] is not None) and len(row[additional_row]): 291 | description += "\n Steps to reproduce : \n" + row[additional_row] 292 | return {"description" : description} 293 | 294 | def _get_category_by_id(self, id): 295 | cursor = self.sql_cnx.cursor() 296 | name_row = "name" 297 | request = "SELECT %s FROM mantis_category_table WHERE id=%s LIMIT 1" % (name_row, str(id)) 298 | cursor.execute(request) 299 | category = cursor.fetchone() 300 | if category is None: 301 | return None 302 | else: 303 | return category[name_row] 304 | 305 | def _get_comments_by_id(self, id): 306 | cursor = self.sql_cnx.cursor() 307 | reporter_id_row = "reporter_id" 308 | bugnote_row = "bugnote_text_id" 309 | date_submitted_row = "date_submitted" 310 | request = "SELECT %s, %s, %s" % (reporter_id_row, bugnote_row, date_submitted_row) 311 | request += " FROM mantis_bugnote_table WHERE bug_id=%s" % str(id) 312 | cursor.execute(request) 313 | result = [] 314 | for row in cursor: 315 | text_cursor = self.sql_cnx.cursor() 316 | note_row = "note" 317 | req = "SELECT %s FROM mantis_bugnote_text_table WHERE id=%s LIMIT 1" % (note_row, str(row[bugnote_row])) 318 | text_cursor.execute(req) 319 | comment = MantisComment() 320 | comment.reporter = self.get_user_by_id(row[reporter_id_row]) 321 | comment.date_submitted = self._to_epoch_time(row[date_submitted_row]) 322 | comment.text = text_cursor.fetchone()[note_row] 323 | result.append(comment) 324 | return result 325 | 326 | def _get_project_id_by_bug_id(self, bug_id): 327 | cursor = self.sql_cnx.cursor() 328 | project_id_row = "project_id" 329 | request = "SELECT %s FROM mantis_bug_table WHERE id=%s LIMIT 1" % (project_id_row, bug_id) 330 | cursor.execute(request) 331 | return cursor.fetchone()[project_id_row] 332 | 333 | 334 | def _get_cf_name_by_id(self, id): 335 | cursor = self.sql_cnx.cursor() 336 | cursor.execute("SELECT name, type FROM mantis_custom_field_table WHERE id=%s LIMIT 1", (str(id),)) 337 | return cursor.fetchone() 338 | 339 | def _get_project_name_by_id(self, id): 340 | cursor = self.sql_cnx.cursor() 341 | name_row = "name" 342 | request = "SELECT %s FROM mantis_project_table WHERE id=%s LIMIT 1" % (name_row, str(id)) 343 | cursor.execute(request) 344 | return cursor.fetchone()[name_row] 345 | 346 | def get_issue_tags_by_id(self, id): 347 | cursor = self.sql_cnx.cursor() 348 | name_row = "name" 349 | request = "SELECT %s FROM mantis_tag_table WHERE id IN (SELECT tag_id FROM mantis_bug_tag_table WHERE bug_id = %s) LIMIT 1" % ( 350 | name_row, str(id)) 351 | cursor.execute(request) 352 | return [row[name_row] for row in cursor] 353 | 354 | def _to_epoch_time(self, time): 355 | if time is None: 356 | return "" 357 | if isinstance(time, long): 358 | return str(time * 1000) 359 | if len(time): 360 | return str(int(time) * 1000) 361 | return "" 362 | -------------------------------------------------------------------------------- /youtrackutils/mantis2youtrack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | import urllib2 10 | from youtrack.connection import Connection 11 | from youtrackutils.mantis.mantisClient import MantisClient 12 | from youtrack import * 13 | import youtrackutils.mantis 14 | import youtrackutils.mantis.defaultMantis 15 | from StringIO import StringIO 16 | from youtrack.importHelper import * 17 | import youtrack.importHelper 18 | 19 | 20 | def main(): 21 | target_url, target_login, target_pass, mantis_db, mantis_host, mantis_port, mantis_login, mantis_pass = sys.argv[1:9] 22 | 23 | youtrackutils.mantis.FIELD_TYPES.update(youtrack.EXISTING_FIELD_TYPES) 24 | mantis_product_names = [p.strip() for p in sys.argv[9:]] 25 | mantis2youtrack(target_url, target_login, target_pass, mantis_db, mantis_host, 26 | mantis_port, mantis_login, mantis_pass, mantis_product_names) 27 | 28 | 29 | def to_yt_user(mantis_user): 30 | yt_user = User() 31 | yt_user.login = mantis_user.user_name 32 | yt_user.fullName = mantis_user.real_name 33 | yt_user.email = mantis_user.email if (mantis_user.email is not None) and len(mantis_user.email) else "" 34 | return yt_user 35 | 36 | 37 | def to_yt_subsystem(mantis_cat, bundle, value_mapping): 38 | name = mantis_cat.name 39 | if name in value_mapping: 40 | name = value_mapping[name] 41 | name = name.replace("/", " ") 42 | subsys = bundle.createElement(name) 43 | subsys.isDefault = False 44 | assignee = mantis_cat.assignee 45 | if assignee is not None: 46 | subsys.owner = assignee 47 | else: 48 | subsys.defaultAssignee = "" 49 | return subsys 50 | 51 | 52 | def to_yt_version(mantis_version, bundle, value_mapping): 53 | name = mantis_version.name 54 | if name in value_mapping: 55 | name = value_mapping[name] 56 | yt_version = bundle.createElement(name) 57 | yt_version.isReleased = mantis_version.is_released 58 | yt_version.isArchived = mantis_version.is_obsolete 59 | yt_version.releaseDate = mantis_version.release_date 60 | return yt_version 61 | 62 | 63 | def to_yt_comment(mantis_comment, target): 64 | yt_comment = Comment() 65 | if mantis_comment.reporter is not None: 66 | reporter = to_yt_user(mantis_comment.reporter) 67 | target.importUsers([reporter]) 68 | yt_comment.author = reporter.login 69 | else: 70 | yt_comment.author = "guest" 71 | if (mantis_comment.text is not None) and len(mantis_comment.text.lstrip()): 72 | yt_comment.text = mantis_comment.text 73 | else: 74 | yt_comment.text = "no text" 75 | yt_comment.created = mantis_comment.date_submitted 76 | return yt_comment 77 | 78 | 79 | def get_yt_field_value(yt_field_name, field_type, mantis_value): 80 | if mantis_value is None: 81 | return None 82 | values_map = {} 83 | if yt_field_name in youtrackutils.mantis.FIELD_VALUES: 84 | values_map = youtrackutils.mantis.FIELD_VALUES[yt_field_name] 85 | 86 | if isinstance(mantis_value, str) or isinstance(mantis_value, unicode): 87 | if mantis_value in values_map: 88 | mantis_value = values_map[mantis_value] 89 | if yt_field_name not in ('summary', 'description'): 90 | mantis_value = mantis_value.replace("/", " ") 91 | return mantis_value 92 | if isinstance(mantis_value, int) or isinstance(mantis_value, long): 93 | if mantis_value in values_map: 94 | mantis_value = values_map[mantis_value] 95 | return str(mantis_value) 96 | if isinstance(mantis_value, list): 97 | return [value.replace("/", " ") for value in [values_map[v] if v in values_map else v for v in mantis_value]] 98 | if isinstance(mantis_value, youtrackutils.mantis.MantisUser): 99 | return to_yt_user(mantis_value) 100 | return mantis_value 101 | 102 | 103 | def get_yt_field_name(mantis_field_name, target, project_id=None): 104 | result = None 105 | if mantis_field_name in youtrackutils.mantis.FIELD_NAMES: 106 | result = youtrackutils.mantis.FIELD_NAMES[mantis_field_name] 107 | elif mantis_field_name in youtrack.EXISTING_FIELDS: 108 | result = mantis_field_name 109 | else: 110 | try: 111 | target.getCustomField(mantis_field_name) 112 | result = mantis_field_name 113 | except YouTrackException: 114 | pass 115 | if result is None or project_id is None: 116 | return result 117 | if result in youtrack.EXISTING_FIELDS: 118 | return result 119 | try: 120 | target.getProjectCustomField(project_id, result) 121 | return result 122 | except YouTrackException: 123 | return None 124 | 125 | 126 | def get_yt_field_type(field_name, target): 127 | if field_name in youtrackutils.mantis.FIELD_TYPES: 128 | return youtrackutils.mantis.FIELD_TYPES[field_name] 129 | try: 130 | return target.getCustomField(field_name).type 131 | except YouTrackException: 132 | return None 133 | 134 | 135 | def add_value_to_field(field_name, field_type, value, project_id, target): 136 | if (field_type is not None) and field_type.startswith("user"): 137 | target.importUsers([value]) 138 | value = value.login 139 | if field_name in youtrack.EXISTING_FIELDS: 140 | return 141 | custom_field = target.getProjectCustomField(project_id, field_name) 142 | if hasattr(custom_field, "bundle"): 143 | bundle = target.getBundle(field_type, custom_field.bundle) 144 | try: 145 | target.addValueToBundle(bundle, value) 146 | except YouTrackException: 147 | pass 148 | 149 | 150 | def to_yt_issue(mantis_issue, project_id, target): 151 | issue = Issue() 152 | issue.comments = [to_yt_comment(comment, target) for comment in mantis_issue["comments"]] 153 | for key in mantis_issue.keys(): 154 | field_name = get_yt_field_name(key, target, project_id) 155 | if field_name is None: 156 | continue 157 | field_type = get_yt_field_type(field_name, target) 158 | if field_type is None and field_name not in youtrack.EXISTING_FIELDS: 159 | continue 160 | value = mantis_issue[key] 161 | if value is None: 162 | continue 163 | if isinstance(value, list): 164 | if not len(value): 165 | continue 166 | elif not len(unicode(value)): 167 | continue 168 | value = get_yt_field_value(field_name, field_type, value) 169 | if isinstance(value, list): 170 | for v in value: 171 | add_value_to_field(field_name, field_type, v, project_id, target) 172 | else: 173 | add_value_to_field(field_name, field_type, value, project_id, target) 174 | 175 | if (field_type is not None) and field_type.startswith("user"): 176 | if isinstance(value, list): 177 | value = [v.login for v in value] 178 | else: 179 | value = value.login 180 | if not isinstance(value, list): 181 | value = unicode(value) 182 | 183 | issue[field_name] = value 184 | if "reporterName" not in issue: 185 | issue["reporterName"] = "guest" 186 | return issue 187 | 188 | 189 | def to_yt_link(mantis_link): 190 | link = Link() 191 | link.source = "%s-%s" % (mantis_link.source_project_id, mantis_link.source) 192 | link.target = "%s-%s" % (mantis_link.target_project_id, mantis_link.target) 193 | link.typeName = youtrackutils.mantis.LINK_TYPES[mantis_link.type] 194 | return link 195 | 196 | 197 | def create_yt_custom_field(connection, mantis_field_name, 198 | attach_bundle_policy="0", auto_attach=True): 199 | """ 200 | Converts mantis_field_name to yt field name and creates 201 | auto attached field with such names and values. 202 | 203 | Args: 204 | connection: Opened Connection instance. 205 | mantis_field_name: Name of custom field in mantis. 206 | attach_bundle_policy: Should be "0" if bundle must be attached as is and "1" if it should be cloned. 207 | auto_attach: 208 | 209 | Returns: 210 | new field name 211 | """ 212 | print("Processing custom field with name [ %s ]" % mantis_field_name.encode('utf-8')) 213 | field_name = youtrackutils.mantis.FIELD_NAMES[mantis_field_name] if mantis_field_name in youtrackutils.mantis.FIELD_NAMES else mantis_field_name 214 | create_custom_field(connection, youtrackutils.mantis.FIELD_TYPES[field_name], field_name, auto_attach, 215 | bundle_policy=attach_bundle_policy) 216 | return field_name 217 | 218 | 219 | def process_mantis_custom_field(connection, mantis_cf_def): 220 | """ 221 | Converts mantis cf to yt cf. 222 | 223 | Args: 224 | connection: Opened Connection instance. 225 | mantis_cf_def: definition of cf in mantis. 226 | 227 | """ 228 | # get names of custom fields in util that are mapped with this prototype 229 | # calculate type of custom field in util 230 | yt_cf_type = youtrackutils.mantis.CF_TYPES[mantis_cf_def.type] 231 | yt_name = youtrackutils.mantis.FIELD_NAMES[mantis_cf_def.name] if mantis_cf_def.name in youtrackutils.mantis.FIELD_NAMES else mantis_cf_def.name 232 | if yt_name in youtrackutils.mantis.FIELD_TYPES: 233 | yt_cf_type = youtrackutils.mantis.FIELD_TYPES[yt_name] 234 | create_custom_field(connection, yt_cf_type, yt_name, False) 235 | 236 | 237 | def attach_field_to_project(connection, project_id, mantis_field_name): 238 | name = get_yt_field_name(mantis_field_name, connection) 239 | project_field = connection.getCustomField(name) 240 | params = dict([]) 241 | if hasattr(project_field, "defaultBundle"): 242 | params["bundle"] = project_field.defaultBundle 243 | try: 244 | connection.createProjectCustomFieldDetailed(str(project_id), name, u"No " + name, params) 245 | except YouTrackException: 246 | pass 247 | 248 | 249 | def add_values_to_fields(connection, project_id, mantis_field_name, values, mantis_value_to_yt_value): 250 | """ 251 | Adds values to custom fields, which are mapped with mantis_field_name field. 252 | 253 | Args: 254 | connection: Opened Connection instance. 255 | project_id: Id of project to add values to. 256 | mantis_field_name: name of cf in Mantis. 257 | values: Values to add to field in Mantis. 258 | 259 | """ 260 | field_name = get_yt_field_name(mantis_field_name, connection) 261 | pcf = connection.getProjectCustomField(str(project_id), field_name) 262 | if hasattr(pcf, "bundle"): 263 | value_mapping = youtrackutils.mantis.FIELD_VALUES[field_name] if field_name in youtrackutils.mantis.FIELD_VALUES else {} 264 | bundle = connection.getBundle(pcf.type[0:-3], pcf.bundle) 265 | yt_values = [v for v in [mantis_value_to_yt_value(value, bundle, value_mapping) for value in values] if 266 | len(v.name)] 267 | add_values_to_bundle_safe(connection, bundle, yt_values) 268 | 269 | 270 | def import_attachments(issue_attachments, issue_id, target): 271 | for attachment in issue_attachments: 272 | print("Processing issue attachment [ %s ]" % str(attachment.id)) 273 | content = StringIO(attachment.content) 274 | author_login = "guest" 275 | if attachment.author is not None: 276 | author = to_yt_user(attachment.author) 277 | target.importUsers([author]) 278 | author_login = author.login 279 | try: 280 | target.importAttachment( 281 | issue_id, 282 | attachment.filename, 283 | content, 284 | author_login, 285 | attachment.file_type, 286 | None, 287 | attachment.date_added) 288 | except YouTrackException: 289 | print("Failed to import attachment") 290 | except urllib2.HTTPError as e: 291 | msg = 'Failed to import attachment [%s] for issue [%s]. Exception: [%s]' % (attachment.filename, issue_id, str(e)) 292 | if isinstance(msg, unicode): 293 | msg = msg.encode('utf-8') 294 | print(msg) 295 | 296 | 297 | def is_prefix_of_any_other_tag(tag, other_tags): 298 | for t in other_tags: 299 | if t.startswith(tag) and (t != tag): 300 | return True 301 | return False 302 | 303 | 304 | def import_tags(source, target, project_ids, collected_tags): 305 | tags_to_import_now = set([]) 306 | tags_to_import_after = set([]) 307 | for tag in collected_tags: 308 | if is_prefix_of_any_other_tag(tag, collected_tags): 309 | tags_to_import_after.add(tag) 310 | else: 311 | tags_to_import_now.add(tag) 312 | max = 100 313 | for project_id in project_ids: 314 | go_on = True 315 | after = 0 316 | while go_on: 317 | issues = source.get_mantis_issues(project_id, after, max) 318 | go_on = False 319 | for issue in issues: 320 | go_on = True 321 | issue_id = issue['id'] 322 | issue_tags = source.get_issue_tags_by_id(issue_id) 323 | for tag in issue_tags: 324 | if tag in tags_to_import_now: 325 | try: 326 | target.executeCommand("%s-%s" % (project_id, issue_id), "tag " + tag) 327 | except YouTrackException: 328 | pass 329 | after += max 330 | if len(tags_to_import_after): 331 | import_tags(source, target, project_ids, tags_to_import_after) 332 | 333 | 334 | def mantis2youtrack(target_url, target_login, target_pass, mantis_db_name, mantis_db_host, mantis_db_port, 335 | mantis_db_login, mantis_db_pass, mantis_project_names): 336 | print("target_url : " + target_url) 337 | print("target_login : " + target_login) 338 | print("target_pass : " + target_pass) 339 | print("mantis_db_name : " + mantis_db_name) 340 | print("mantis_db_host : " + mantis_db_host) 341 | print("mantis_db_port : " + mantis_db_port) 342 | print("mantis_db_login : " + mantis_db_login) 343 | print("mantis_db_pass : " + mantis_db_pass) 344 | print("mantis_project_names : " + repr(mantis_project_names)) 345 | 346 | #connacting to yt 347 | target = Connection(target_url, target_login, target_pass) 348 | #connacting to mantis 349 | client = MantisClient(mantis_db_host, int(mantis_db_port), mantis_db_login, 350 | mantis_db_pass, mantis_db_name, youtrackutils.mantis.CHARSET, youtrackutils.mantis.BATCH_SUBPROJECTS) 351 | if not len(mantis_project_names): 352 | print("You should declarer at least one project to import") 353 | sys.exit() 354 | 355 | print("Creating custom fields definitions") 356 | create_yt_custom_field(target, u"priority") 357 | create_yt_custom_field(target, u"severity") 358 | create_yt_custom_field(target, u"category_id") 359 | create_yt_custom_field(target, u"version", "1") 360 | create_yt_custom_field(target, u"fixed_in_version", "1") 361 | create_yt_custom_field(target, u"build", "1") 362 | create_yt_custom_field(target, u"platform") 363 | create_yt_custom_field(target, u"os") 364 | create_yt_custom_field(target, u"os_build") 365 | create_yt_custom_field(target, u"due_date") 366 | create_yt_custom_field(target, u"Reproducibility") 367 | create_yt_custom_field(target, u"target_version", u'1') 368 | create_yt_custom_field(target, u"status") 369 | create_yt_custom_field(target, u"resolution") 370 | create_yt_custom_field(target, u'project_id', u'1') 371 | 372 | # adding some custom fields that are predefined in mantis 373 | project_ids = [] 374 | for name in mantis_project_names: 375 | pid = client.get_project_id_by_name(name) 376 | if pid is None: 377 | raise Exception("Cannot find project with name '%s'" % name) 378 | project_ids.append(pid) 379 | 380 | custom_fields = client.get_mantis_custom_fields(project_ids) 381 | 382 | for cf_def in custom_fields: 383 | print("Processing custom field [ %s ]" % cf_def.name.encode('utf-8')) 384 | process_mantis_custom_field(target, cf_def) 385 | 386 | print("Creating custom fields definitions finished") 387 | 388 | issue_tags = set([]) 389 | for name in mantis_project_names: 390 | project_id = str(client.get_project_id_by_name(name)) 391 | name = name.replace("/", " ") 392 | print("Creating project [ %s ] with name [ %s ]" % (project_id, name)) 393 | try: 394 | target.getProject(project_id) 395 | except YouTrackException: 396 | target.createProjectDetailed(project_id, name, client.get_project_description(project_id), 397 | target_login) 398 | 399 | print("Importing components to project [ %s ]" % project_id) 400 | add_values_to_fields(target, project_id, u"category_id", 401 | client.get_mantis_categories(project_id), 402 | lambda component, yt_bundle, value_mapping: 403 | to_yt_subsystem(component, yt_bundle, value_mapping)) 404 | print("Importing components to project [ %s ] finished" % project_id) 405 | 406 | print("Importing versions to project [ %s ]" % project_id) 407 | mantis_versions = client.get_mantis_versions(project_id) 408 | add_values_to_fields(target, project_id, u"version", mantis_versions, 409 | lambda version, yt_bundle, value_mapping: 410 | to_yt_version(version, yt_bundle, value_mapping)) 411 | 412 | add_values_to_fields(target, project_id, u"fixed_in_version", 413 | mantis_versions, 414 | lambda version, yt_bundle, value_mapping: 415 | to_yt_version(version, yt_bundle, value_mapping)) 416 | 417 | print("Importing versions to project [ %s ] finished" % project_id) 418 | 419 | print("Attaching custom fields to project [ %s ]" % project_id) 420 | cf_ids = client.get_custom_fields_attached_to_project(project_id) 421 | 422 | for cf in custom_fields: 423 | if cf.field_id in cf_ids: 424 | attach_field_to_project(target, project_id, cf.name) 425 | 426 | print("Attaching custom fields to project [ %s ] finished" % project_id) 427 | 428 | print("Importing issues to project [ %s ]" % project_id) 429 | max_count = 100 430 | after = 0 431 | go_on = True 432 | while go_on: 433 | go_on = False 434 | mantis_issues = client.get_mantis_issues(project_id, after, max_count) 435 | after += max_count 436 | if len(mantis_issues): 437 | go_on = True 438 | target.importIssues(project_id, name + " Assignees", 439 | [to_yt_issue(issue, project_id, target) for issue in mantis_issues]) 440 | 441 | # import attachments 442 | for issue in mantis_issues: 443 | issue_attachments = client.get_attachments(issue['id']) 444 | issue_id = "%s-%s" % (project_id, issue['id']) 445 | import_attachments(issue_attachments, issue_id, target) 446 | issue_tags |= set(client.get_issue_tags_by_id(issue['id'])) 447 | 448 | print("Importing issues to project [ %s ] finished" % project_id) 449 | 450 | import_tags(client, target, project_ids, issue_tags) 451 | 452 | print("Importing issue links") 453 | go_on = True 454 | after = 0 455 | max_count = 200 456 | while go_on: 457 | go_on = False 458 | mantis_issue_links = client.get_issue_links(after, max_count) 459 | yt_issue_links = [] 460 | for link in mantis_issue_links: 461 | go_on = True 462 | print("Processing issue link for source issue [ %s ]" % str(link.source)) 463 | yt_issue_links.append(to_yt_link(link)) 464 | after += max_count 465 | print(target.importLinks(yt_issue_links)) 466 | 467 | print("Importing issue links finished") 468 | 469 | if __name__ == "__main__": 470 | main() 471 | -------------------------------------------------------------------------------- /youtrackutils/moveIssue.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | from youtrack import Issue, YouTrackException 10 | from youtrack.connection import Connection 11 | from youtrack.sync.links import LinkImporter 12 | 13 | PREDEFINED_FIELDS = ["summary", "description", "created", "updated", 14 | "updaterName", "resolved", "reporterName", 15 | "assigneeName", "priority", "state"] 16 | 17 | 18 | def main(): 19 | try: 20 | (source_url, source_login, source_password, 21 | target_url, target_login, target_password) = sys.argv[1:7] 22 | source_id, target = sys.argv[7:] 23 | except (ValueError, IndexError): 24 | print("Usage: moveIssue source_url source_login source_password " + 25 | "target_url target_login target_password source_id " + 26 | "target_project_id_or_issue_id") 27 | sys.exit(1) 28 | do_move(source_url, source_login, source_password, 29 | target_url, target_login, target_password, source_id, target) 30 | 31 | 32 | def check_user(user_login, source, target): 33 | if (user_login == "guest") or (user_login is None): 34 | return 35 | try: 36 | target.getUser(user_login) 37 | except YouTrackException: 38 | source_user = source.getUser(user_login) 39 | if not("email" in source_user): 40 | source_user.email = "no_email" 41 | print(target.importUsers([source_user])) 42 | print("User %s was created" % user_login) 43 | return user_login 44 | 45 | 46 | def get_new_issue_id(project_id, target): 47 | max_id = 0 48 | max_number = 100 49 | while True: 50 | issues = target.getIssues(project_id, "", max_id, max_number) 51 | if len(issues) == 0: 52 | return max_id + 1 53 | for issue in issues: 54 | issue_id = int(issue.numberInProject) 55 | if issue_id >= max_id: 56 | max_id = issue_id 57 | 58 | 59 | def get_time_tracking_state(source, target, 60 | source_project_id, target_project_id): 61 | source_tt = source.getProjectTimeTrackingSettings(source_project_id) 62 | target_tt = target.getProjectTimeTrackingSettings(target_project_id) 63 | return source_tt and target_tt and source_tt.Enabled and target_tt.Enabled 64 | 65 | 66 | def do_move(source_url, source_login, source_password, 67 | target_url, target_login, target_password, source_issue_id, target): 68 | print("source_url : " + source_url) 69 | print("source_login : " + source_login) 70 | print("source_password : " + source_password) 71 | print("target_url : " + target_url) 72 | print("target_login : " + target_login) 73 | print("target_password : " + target_password) 74 | print("source_id : " + source_issue_id) 75 | 76 | if target.find('-') > -1: 77 | print("target_id : " + target) 78 | target_project_id, target_issue_number = target.split('-') 79 | else: 80 | print("target_project_id: " + target) 81 | target_project_id = target 82 | target_issue_number = None 83 | 84 | # connecting 85 | try: 86 | target = Connection(target_url, target_login, target_password) 87 | print("Connected to target url [%s]" % target_url) 88 | except Exception as ex: 89 | print("Failed to connect to target url [%s] with login/password [%s/%s]" 90 | % (target_url, target_login, target_password)) 91 | raise ex 92 | 93 | try: 94 | source = Connection(source_url, source_login, source_password) 95 | print("Connected to source url [%s]" % source_url) 96 | except Exception as ex: 97 | print("Failed to connect to source url [%s] with login/password [%s/%s]" 98 | % (source_url, source_login, source_password)) 99 | raise ex 100 | 101 | try: 102 | target.getProject(target_project_id) 103 | except Exception as ex: 104 | print("Can't connect to target project [%s]" % target_project_id) 105 | raise ex 106 | 107 | # twin issues 108 | try: 109 | source_issue = source.getIssue(source_issue_id) 110 | except Exception as ex: 111 | print("Failed to get issue [%s]" % source_issue_id) 112 | raise ex 113 | 114 | target_issue = Issue() 115 | 116 | # import users if needed 117 | name_fields = ["reporterName", "assigneeName", "updaterName"] 118 | for field in name_fields: 119 | if field in source_issue: 120 | check_user(source_issue[field], source, target) 121 | 122 | if not target_issue_number: 123 | target_issue_number = str(get_new_issue_id(target_project_id, target)) 124 | target_issue.numberInProject = target_issue_number 125 | 126 | # check subsystem 127 | target_subsystem = None 128 | try: 129 | target.getSubsystem(target_project_id, source_issue.subsystem) 130 | target_subsystem = source_issue.subsystem 131 | except (YouTrackException, AttributeError): 132 | pass 133 | target_issue.subsystem = target_subsystem 134 | for field in PREDEFINED_FIELDS: 135 | if field in source_issue: 136 | target_issue[field] = source_issue[field] 137 | 138 | if "Type" in source_issue: 139 | target_issue.type = source_issue["Type"] 140 | elif "type" in source_issue: 141 | target_issue.type = source_issue["type"] 142 | else: 143 | target_issue.type = "Bug" 144 | 145 | # convert custom field 146 | target_cfs = target.getProjectCustomFields(target_project_id) 147 | for cf in target_cfs: 148 | cf_name = cf.name 149 | if cf_name in source_issue: 150 | target_issue[cf_name] = source_issue[cf_name] 151 | 152 | # comments 153 | target_issue.comments = source_issue.getComments() 154 | for comment in target_issue.comments: 155 | check_user(comment.author, source, target) 156 | 157 | # import issue 158 | print(target.importIssues( 159 | target_project_id, 160 | "", 161 | [target_issue])) 162 | 163 | # attachments 164 | for attachment in source_issue.getAttachments(): 165 | check_user(attachment.authorLogin, source, target) 166 | attachment.url = attachment.url.replace(source_url, "") 167 | target.createAttachmentFromAttachment( 168 | "%s-%s" % (target_project_id, target_issue.numberInProject), 169 | attachment) 170 | 171 | # work items 172 | if get_time_tracking_state( 173 | source, target, source_issue_id.split('-')[0], 174 | target_project_id): 175 | workitems = source.getWorkItems(source_issue_id) 176 | if workitems: 177 | existing_workitems = dict() 178 | target_workitems = target.getWorkItems( 179 | target_project_id + '-' + target_issue_number) 180 | if target_workitems: 181 | for w in target_workitems: 182 | _id = '%s\n%s\n%s' % (w.date, w.authorLogin, w.duration) 183 | if hasattr(w, 'description'): 184 | _id += '\n%s' % w.description 185 | existing_workitems[_id] = w 186 | new_workitems = [] 187 | for w in workitems: 188 | _id = '%s\n%s\n%s' % (w.date, w.authorLogin, w.duration) 189 | if hasattr(w, 'description'): 190 | _id += '\n%s' % w.description 191 | if _id not in existing_workitems: 192 | new_workitems.append(w) 193 | if new_workitems: 194 | print("Process workitems for issue [ " + source_issue_id + "]") 195 | try: 196 | for w in new_workitems: 197 | check_user(w.authorLogin, source, target) 198 | target.importWorkItems( 199 | target_project_id + '-' + target_issue_number, 200 | new_workitems) 201 | except YouTrackException as e: 202 | print("Failed to import workitems: " + str(e)) 203 | 204 | # links 205 | link_importer = LinkImporter(target) 206 | links2import = source_issue.getLinks() 207 | link_importer.collectLinks(links2import) 208 | link_importer.addAvailableIssue(source_issue) 209 | for l in links2import: 210 | link_importer.addAvailableIssue(source.getIssue(l.source)) 211 | link_importer.addAvailableIssue(source.getIssue(l.target)) 212 | link_importer.importCollectedLinks() 213 | 214 | 215 | if __name__ == "__main__": 216 | main() 217 | -------------------------------------------------------------------------------- /youtrackutils/redmine/__init__.py: -------------------------------------------------------------------------------- 1 | from client import RedmineClient 2 | from client import RedmineException 3 | from mapping import Mapping 4 | 5 | import warnings 6 | 7 | warnings.warn("deprecated", DeprecationWarning) 8 | 9 | -------------------------------------------------------------------------------- /youtrackutils/redmine/client.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | from pyactiveresource.activeresource import ActiveResource 4 | from pyactiveresource.connection import ResourceNotFound, MethodNotAllowed, ServerError 5 | 6 | 7 | class RedmineResource(ActiveResource): 8 | root_element = None 9 | 10 | def __init__(self, attributes=None, prefix_options=None): 11 | if isinstance(attributes, basestring): 12 | self.name = attributes 13 | super(RedmineResource, self).__init__(attributes, prefix_options) 14 | 15 | def __repr__(self): 16 | return pprint.pformat(self.attributes, indent=2, width=140) 17 | 18 | @classmethod 19 | def _build_list(cls, elements, prefix_options=None): 20 | if cls.root_element is not None and \ 21 | isinstance(elements, dict) and cls.root_element in elements: 22 | elements = elements[cls.root_element] 23 | return super(RedmineResource, cls)._build_list(elements, prefix_options) 24 | 25 | 26 | class Project(RedmineResource): 27 | pass 28 | 29 | 30 | class Issue(RedmineResource): 31 | root_element = 'issues' 32 | 33 | 34 | class Group(RedmineResource): 35 | pass 36 | 37 | 38 | class User(RedmineResource): 39 | root_element = 'users' 40 | 41 | 42 | class Membership(RedmineResource): 43 | root_element = 'memberships' 44 | 45 | 46 | class Role(RedmineResource): 47 | pass 48 | 49 | 50 | class Version(RedmineResource): 51 | pass 52 | 53 | 54 | class IssueCategory(RedmineResource): 55 | pass 56 | 57 | 58 | class TimeEntry(RedmineResource): 59 | pass 60 | 61 | 62 | class Permission(RedmineResource): 63 | pass 64 | 65 | 66 | class CustomField(RedmineResource): 67 | pass 68 | 69 | 70 | class RedmineException(Exception): 71 | pass 72 | 73 | 74 | class RedmineClient(object): 75 | 76 | def __init__(self, api_key, url, login=None, password=None): 77 | RedmineResource.site = url 78 | if api_key is not None: 79 | RedmineResource.headers = {'X-Redmine-API-Key': api_key} 80 | else: 81 | if login is None: 82 | login = '' 83 | if password is None: 84 | password = '' 85 | self._user = login 86 | self._password = password 87 | RedmineResource.user = login 88 | RedmineResource.password = password 89 | if not Membership.prefix_source.endswith('/'): 90 | Membership.prefix_source += '/' 91 | Membership.prefix_source += 'projects/$project_id' 92 | 93 | @property 94 | def headers(self): 95 | headers = {} 96 | if RedmineResource.headers: 97 | for name, value in RedmineResource.headers.items(): 98 | headers[name] = value 99 | if RedmineResource.connection.auth: 100 | headers['Authorization'] = 'Basic ' + RedmineResource.connection.auth 101 | return headers 102 | 103 | def get_project(self, project_id): 104 | try: 105 | return Project.find(project_id) 106 | except ResourceNotFound: 107 | raise RedmineException( 108 | "Project '%s' doesn't exist in Redmine" % project_id) 109 | 110 | def get_projects(self, project_ids=None): 111 | if project_ids: 112 | return [self.get_project(id_) for id_ in project_ids] 113 | return Project.find() 114 | 115 | def get_issues(self, project_id=None): 116 | return Issue.find(None, None, project_id=project_id) 117 | 118 | def get_issue_details(self, issue_id): 119 | return Issue.find(issue_id, 120 | include='journals,assigned_to_id,attachments,children,relations') 121 | 122 | def get_project_issues(self, _id, _limit=None, _offset=None, _skip_on_error=False): 123 | return_data = [] 124 | if _limit: 125 | if _offset is None: 126 | _offset = 0 127 | issues = Issue.find(None, None, project_id=_id, 128 | limit=_limit, offset=_offset, sort='id', status_id='*') 129 | elif _offset: 130 | issues = Issue.find(None, None, project_id=_id, 131 | offset=_offset, sort='id', status_id='*') 132 | else: 133 | issues = Issue.find(None, None, project_id=_id, sort='id', status_id='*') 134 | for issue in issues: 135 | if (hasattr(issue, 'id')): 136 | try: 137 | details = self.get_issue_details(issue.id) 138 | return_data.append(details) 139 | except ServerError, se: 140 | print "Wasn't able to process issue " + issue.id 141 | if not _skip_on_error: 142 | raise se 143 | else: 144 | for packed_issue in issue['issues']: 145 | try: 146 | details = self.get_issue_details(packed_issue['id']) 147 | return_data.append(details) 148 | except ServerError, se: 149 | print "Wasn't able to process issue " + packed_issue['id'] 150 | if not _skip_on_error: 151 | raise se 152 | return return_data 153 | 154 | def get_user(self, user_id): 155 | return User.find(user_id, None, include='groups') 156 | 157 | def get_users(self, user_ids=None): 158 | if user_ids: 159 | return [self.get_user(uid) for uid in user_ids] 160 | return User.find(None, None, include='groups') 161 | 162 | def get_groups(self): 163 | return Group.find() 164 | 165 | def get_project_members(self, id_): 166 | return Membership.find(None, None, project_id=id_) 167 | 168 | def get_roles(self): 169 | roles = Role.find() 170 | for role in roles: 171 | try: 172 | role.attributes['permissions'] = Role().find(role.id).permissions 173 | except (ResourceNotFound, MethodNotAllowed): 174 | print "WARN: Can't get permissions for roles." 175 | print "WARN: This Redmine version doesn't support this feature." 176 | break 177 | return roles 178 | 179 | def get_category(self, id_): 180 | return IssueCategory.find(id_) 181 | 182 | def get_version(self, id_): 183 | return Version.find(id_) 184 | 185 | def get_time_entries(self, issue_id): 186 | return TimeEntry.find(None, None, issue_id=issue_id) 187 | -------------------------------------------------------------------------------- /youtrackutils/redmine/mapping.py: -------------------------------------------------------------------------------- 1 | class Mapping(object): 2 | FIELD_NAMES = { 3 | 'id' : 'numberInProject', 4 | 'subject' : 'summary', 5 | 'author' : 'reporterName', 6 | 'status' : 'State', 7 | 'priority' : 'Priority', 8 | 'created_on' : 'created', 9 | 'updated_on' : 'updated', 10 | 'tracker' : 'Type', 11 | 'assigned_to' : 'Assignee', 12 | 'due_date' : 'Due Date', 13 | 'estimated_hours' : 'Estimation', 14 | 'category' : 'Subsystem', 15 | 'fixed_version' : 'Fix versions', 16 | 'redmine_id' : 'Redmine ID' 17 | } 18 | 19 | FIELD_TYPES = { 20 | 'Type' : 'enum[1]', 21 | 'State' : 'state[1]', 22 | 'Priority' : 'enum[1]', 23 | 'Assignee' : 'user[1]', 24 | 'Due Date' : 'date', 25 | 'Estimation' : 'period', 26 | 'Subsystem' : 'ownedField[1]', 27 | 'Fix versions' : 'version[*]', 28 | 'Redmine ID' : 'integer' 29 | } 30 | 31 | CONVERSION = { 32 | 'State': { 33 | 'Resolved' : 'Fixed', 34 | 'Closed' : 'Fixed', 35 | 'Rejected' : "Won't fix" 36 | }, 37 | 'Priority': { 38 | 'High' : 'Major', 39 | 'Low' : 'Minor', 40 | 'Urgent' : 'Critical', 41 | 'Immediate' : 'Show-stopper' 42 | } 43 | } 44 | 45 | # RESOLVED_STATES = [ 46 | # "Can't Reproduce", 47 | # "Duplicate", 48 | # "Fixed", 49 | # "Won't fix", 50 | # "Incomplete", 51 | # "Obsolete", 52 | # "Verified" 53 | # ] 54 | 55 | PERMISSIONS = { 56 | 'add_project' : 'CREATE_PROJECT', 57 | 'edit_project' : 'UPDATE_PROJECT', 58 | 'close_project' : 'DELETE_PROJECT', 59 | 'manage_members' : [ 'CREATE_USER', 'READ_USER', 'UPDATE_USER', 60 | 'CREATE_USERGROUP', 'READ_USERGROUP', 61 | 'UPDATE_USERGROUP' ], 62 | 'add_messages' : 'CREATE_COMMENT', 63 | 'edit_messages' : 'UPDATE_NOT_OWN_COMMENT', 64 | 'edit_own_messages' : 'UPDATE_COMMENT', 65 | 'delete_messages' : 'DELETE_NOT_OWN_COMMENT', 66 | 'delete_own_messages' : 'DELETE_COMMENT', 67 | 'view_issues' : 'READ_ISSUE', 68 | 'add_issues' : 'CREATE_ISSUE', 69 | 'edit_issues' : 'UPDATE_ISSUE', 70 | 'delete_issues' : 'DELETE_ISSUE', 71 | 'view_issue_watchers' : 'VIEW_WATCHERS', 72 | 'add_issue_watchers' : 'UPDATE_WATCHERS', 73 | 'delete_issue_watchers' : 'UPDATE_WATCHERS', 74 | 'log_time' : 'UPDATE_WORK_ITEM', 75 | 'view_time_entries' : 'READ_WORK_ITEM', 76 | 'edit_time_entries' : 'UPDATE_WORK_ITEM', 77 | 'edit_own_time_entries' : 'UPDATE_NOT_OWN_WORK_ITEM', 78 | # 'select_project_modules' : None, 79 | # 'manage_versions' : None, 80 | # 'add_subprojects' : None, 81 | # 'manage_boards' : None, 82 | # 'view_calendar' : None, 83 | # 'manage_documents' : None, 84 | # 'view_documents' : None, 85 | # 'manage_files' : None, 86 | # 'view_files' : None, 87 | # 'view_gantt' : None, 88 | # 'manage_categories' : None, 89 | # 'manage_issue_relations' : None, 90 | # 'manage_subtasks' : None, 91 | # 'set_issues_private' : None, 92 | # 'set_own_issues_private' : None, 93 | # 'add_issue_notes' : None, 94 | # 'edit_issue_notes' : None, 95 | # 'edit_own_issue_notes' : None, 96 | # 'view_private_notes' : None, 97 | # 'set_notes_private' : None, 98 | # 'move_issues' : None, 99 | # 'manage_public_queries' : None, 100 | # 'save_queries' : None, 101 | # 'manage_news' : None, 102 | # 'comment_news' : None, 103 | # 'manage_project_activities' : None, 104 | # 'manage_wiki' : None, 105 | # 'rename_wiki_pages' : None, 106 | # 'delete_wiki_pages' : None, 107 | # 'view_wiki_pages' : None, 108 | # 'export_wiki_pages' : None, 109 | # 'view_wiki_edits' : None, 110 | # 'edit_wiki_pages' : None, 111 | # 'delete_wiki_pages_attachments' : None, 112 | # 'protect_wiki_pages' : None 113 | } 114 | -------------------------------------------------------------------------------- /youtrackutils/trac2youtrack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | import urllib 10 | from youtrack.connection import Connection 11 | from youtrackutils.tracLib.client import Client 12 | import youtrack 13 | import re 14 | import youtrackutils.tracLib 15 | import youtrackutils.tracLib.defaultTrac 16 | import youtrack.connection 17 | from youtrack.importHelper import * 18 | 19 | 20 | def main(): 21 | try: 22 | target_url, target_login, target_password, project_ID, project_name, env_path = sys.argv[1:] 23 | print("url : " + target_url) 24 | print("login : " + target_login) 25 | print("pass : " + target_password) 26 | print("id : " + project_ID) 27 | print("name : " + project_name) 28 | print("trac environment at : " + env_path) 29 | except BaseException as e: 30 | print(e) 31 | return 32 | trac2youtrack(target_url, target_login, target_password, project_ID, project_name, env_path) 33 | 34 | 35 | def to_youtrack_user(trac_user) : 36 | """ 37 | Converts Trac user to YT user. 38 | 39 | Args: 40 | trac_user: TracUser instance. 41 | 42 | Returns: 43 | YT user with same email and login as trac user. If trac_user doesn't have email, 44 | tracLib.DEFAULT_EMAIL is set. 45 | """ 46 | user = youtrack.User() 47 | user.login = trac_user.name 48 | user.email = youtrackutils.tracLib.DEFAULT_EMAIL 49 | if not (trac_user.email is None): 50 | if len(trac_user.email): 51 | user.email = trac_user.email 52 | return user 53 | 54 | 55 | def to_non_authorised_youtrack_user(user_name): 56 | """ 57 | This method is for creating YT users for people, who were not authorised in Trac, but left their names, 58 | and probably emails. 59 | 60 | Args: 61 | user_name: String, that represents user. It must have format "login_name ". 62 | 63 | Returns: 64 | If user_name can be parsed, returns YT user with login login_name and email email_address, 65 | else returns None 66 | 67 | """ 68 | if user_name is None: 69 | return None 70 | user = youtrack.User() 71 | # non authorized users in trac are stored like this "name " 72 | start = user_name.find("<") 73 | end = user_name.rfind(">") 74 | # we don't accept users who didn't leave the email 75 | if (start > -1) and (end > start + 1): 76 | if user_name.find("@", start, end) > 0: 77 | user.email = user_name[start + 1 : end].replace(" ", "_") 78 | user.login = user_name[start + 1 : end].replace(" ", "_") 79 | return user 80 | return None 81 | 82 | 83 | def process_non_authorised_user(connection, registered_users, user_name) : 84 | """ 85 | This method tries to create new YT user for trac non-authorised user. 86 | 87 | Args: 88 | connection: youtrack.connection object. 89 | registered_users: list of user logins, that were previously registered in YT. 90 | user_name: String, that represents user. It must have format "login_name ". 91 | 92 | Returns: 93 | New user login and updated list of registered users logins. If it is impossible to create 94 | new YT user, then user login is None. 95 | """ 96 | if youtrackutils.tracLib.ACCEPT_NON_AUTHORISED_USERS: 97 | yt_user = to_non_authorised_youtrack_user(user_name) 98 | if yt_user is None: 99 | return None, registered_users 100 | else: 101 | if not (yt_user.login in registered_users): 102 | connection.importUsers([yt_user]) 103 | registered_users.add(yt_user.login) 104 | return yt_user.login, registered_users 105 | else: 106 | return None, registered_users 107 | 108 | 109 | def to_youtrack_subsystem(trac_component, yt_bundle): 110 | """ 111 | Converts trac component to YT subsystem. 112 | 113 | Args: 114 | trac_component: Trac component to convert. 115 | yt_bundle: YT field bundle to create component in. 116 | 117 | Returns: 118 | YT field, that has same name, owner and description, as trac component has. 119 | 120 | """ 121 | yt_subsystem = yt_bundle.createElement(trac_component.name) 122 | yt_subsystem.owner = trac_component.owner 123 | yt_subsystem.description = trac_component.description 124 | return yt_subsystem 125 | 126 | 127 | def to_youtrack_issue(project_ID, trac_issue, check_box_fields): 128 | issue = youtrack.Issue() 129 | 130 | issue.numberInProject = str(trac_issue.id) 131 | issue.summary = trac_issue.summary 132 | issue.description = trac_issue.description 133 | if trac_issue.time > 0: 134 | issue.created = str(trac_issue.time) 135 | if trac_issue.changetime > 0: 136 | issue.updated = str(trac_issue.changetime) 137 | # anonymous in trac == guest in util 138 | if trac_issue.reporter is None: 139 | issue.reporterName = "guest" 140 | else: 141 | issue.reporterName = trac_issue.reporter 142 | # watchers 143 | issue.watcherName = set([]) 144 | for cc in trac_issue.cc: 145 | issue.watcherName.add(cc) 146 | # adding custom fields to issue 147 | custom_fields = trac_issue.custom_fields 148 | for cf in custom_fields.keys(): 149 | if cf in check_box_fields: 150 | if custom_fields[cf] != "0": 151 | issue[cf] = check_box_fields[cf] 152 | else: 153 | value = custom_fields[cf] 154 | if cf in youtrackutils.tracLib.FIELD_NAMES.keys(): 155 | cf = youtrackutils.tracLib.FIELD_NAMES[cf] 156 | if cf in youtrackutils.tracLib.FIELD_VALUES.keys(): 157 | cf_values_mapping = youtrackutils.tracLib.FIELD_VALUES[cf] 158 | if value in cf_values_mapping: 159 | value = cf_values_mapping[value] 160 | if (value is not None) and (value.strip() != ""): 161 | issue[cf] = value 162 | 163 | # handle special case of status:closed / resolution:fixed 164 | if (custom_fields["Status"] is not None) and (custom_fields["Status"] == "closed"): 165 | if (custom_fields["Resolution"] is not None) and (custom_fields["Resolution"] == "fixed"): 166 | issue["State"] = "Verified" 167 | 168 | unsorted_comments = [] 169 | for comment in trac_issue.comments: 170 | unsorted_comments.append(to_youtrack_comment(project_ID, comment)) 171 | issue.comments = sorted(unsorted_comments, key=lambda it: it.created) 172 | return issue 173 | 174 | 175 | def to_youtrack_version(trac_version, yt_bundle): 176 | """" 177 | This method converts trac version to YT version. 178 | 179 | Args: 180 | trac_version: Trac version to convert. 181 | yt_bundle: YT field bundle to create version in. 182 | 183 | Returns: 184 | YT version that has same name, description and release date as trac version. 185 | New version is released and not archived. 186 | """"" 187 | version = yt_bundle.createElement(trac_version.name) 188 | version.isReleased = (trac_version.time is not None) 189 | version.isArchived = False 190 | version.description = trac_version.description 191 | version.releaseDate = trac_version.time 192 | return version 193 | 194 | 195 | def to_youtrack_comment(project_ID, trac_comment): 196 | """ 197 | This method converts trac comment to youtrack comment 198 | 199 | Args: 200 | trac_comment: Trac comment to convert 201 | 202 | Returns: 203 | YT comment, which has same author as initial comment. If initial comment 204 | author was anonymous, comment is added from behalf of guest user. YT comment 205 | gets same creation date as trac comment. 206 | """ 207 | comment = youtrack.Comment() 208 | if trac_comment.author == "anonymous": 209 | comment.author = "guest" 210 | else: 211 | comment.author = trac_comment.author 212 | 213 | comment.text = trac_comment.content 214 | 215 | # translate Trac wiki ticket link format to YouTrack id format 216 | comment.text = re.sub(r'\#(\d+)', project_ID+'-'+r'\1', comment.text) 217 | 218 | # translate trac preformatted blocks, {{{ and }}} 219 | # opening tag done as two lines for python 2.7 that doesn't really support optional capture group 220 | comment.text = re.sub(r'{{{\s*#!(\w+)', r'```\1', comment.text) 221 | comment.text = re.sub(r'{{{', r'```', comment.text) 222 | comment.text = re.sub(r'}}}', r'```', comment.text) 223 | 224 | comment.created = str(trac_comment.time) 225 | return comment 226 | 227 | 228 | def trac_values_to_youtrack_values(field_name, value_names): 229 | """ 230 | This method converts trac custom field valued values to YT custom field values using 231 | mapping file (FIELD_VALUES dict). If some value is not in this dictionary, it is added 232 | to result as is. 233 | 234 | Args: 235 | field_name: Name of YT custom field to convert values for. 236 | value_names: trac value names. 237 | 238 | Returns: 239 | List of strings, that ahs YT field values. If value_names is None, returns None. 240 | """ 241 | if value_names is None: 242 | return None 243 | field_values = [] 244 | for name in value_names: 245 | if field_name in youtrackutils.tracLib.FIELD_VALUES: 246 | if name in youtrackutils.tracLib.FIELD_VALUES[field_name].keys(): 247 | name = youtrackutils.tracLib.FIELD_VALUES[field_name][name] 248 | if name not in field_values: 249 | field_values.append(name) 250 | return field_values 251 | 252 | 253 | def trac_field_name_to_yt_field_name(trac_field_name): 254 | if trac_field_name in youtrackutils.tracLib.FIELD_NAMES: 255 | return youtrackutils.tracLib.FIELD_NAMES[trac_field_name] 256 | return trac_field_name 257 | 258 | 259 | def create_yt_custom_field(connection, project_Id, field_name, value_names): 260 | """ 261 | Creates YT custom field if needed and attaches it to project with id project_id. Converts Trac value_names 262 | to YT values and sets those values to project custom field. 263 | 264 | Args: 265 | connection: Connection instance. 266 | project_id: Id of project to attach field to. 267 | field_name: Name of trac field. It will be converted to YT cf name. YT field should be mentioned in tracLib.FIELD_TYPES mapping. 268 | value_names: Names of Trac cf values. They will be converted to YT values. 269 | """ 270 | field_values = trac_values_to_youtrack_values(field_name, value_names) 271 | field_name = trac_field_name_to_yt_field_name(field_name) 272 | process_custom_field(connection, project_Id, youtrackutils.tracLib.FIELD_TYPES[field_name], field_name, field_values) 273 | 274 | 275 | def to_youtrack_state(trac_resolution, yt_bundle) : 276 | """ 277 | Creates YT state in yt_bundle with trac_resolution name. 278 | 279 | Args: 280 | track_resolution: Name of new state to add. 281 | yt_bundle: FieldBundle to create new state in. 282 | 283 | Returns: 284 | New resolved YT state with trac_resolution name. 285 | """ 286 | state = yt_bundle.createElement(trac_resolution.name) 287 | state.isResolved = True 288 | return state 289 | 290 | 291 | def to_youtrack_workitem(trac_workitem): 292 | workitem = youtrack.WorkItem() 293 | workitem.date = str(trac_workitem.time) 294 | workitem.duration = str(int(trac_workitem.duration) / 60) 295 | workitem.authorLogin = trac_workitem.author 296 | workitem.description = trac_workitem.comment 297 | return workitem 298 | 299 | 300 | def create_yt_bundle_custom_field(target, project_id, field_name, trac_field_values, trac_field_to_youtrack_field): 301 | """ 302 | Creates YT bundle custom field if needed and attaches it to project with id project_id. If Field is already attached to project, 303 | adds missing values to bundle, attached with this field. If there is no such custom field, attached to this project, creates bundle 304 | to attaches with this field, converts trac_field_values to YT field values and adds them to created bundle. 305 | 306 | Args: 307 | target: Connection instance.. 308 | project_id: Id of project to attach field to. 309 | field_name: Name of custom field in trac. It will be converted to custom field name in YT. 310 | trac_field_values: Field values in Trac. 311 | trac_field_to_yt_field: Methods, that converts trac field value to YT Field in particular bundle 312 | """ 313 | create_yt_custom_field(target, project_id, field_name, []) 314 | field_name = trac_field_name_to_yt_field_name(field_name) 315 | 316 | field_bundle = target.getBundle( 317 | youtrackutils.tracLib.FIELD_TYPES[field_name][0:-3], 318 | target.getProjectCustomField(project_id, field_name).bundle) 319 | values_to_add = [] 320 | for field in trac_field_values: 321 | if field_name in youtrackutils.tracLib.FIELD_VALUES.keys(): 322 | if field.name in youtrackutils.tracLib.FIELD_VALUES[field_name].keys(): 323 | field.name = youtrackutils.tracLib.FIELD_VALUES[field_name][field.name] 324 | values_to_add.append(trac_field_to_youtrack_field(field, field_bundle)) 325 | add_values_to_bundle_safe(target, field_bundle, values_to_add) 326 | 327 | 328 | def trac2youtrack(target_url, target_login, target_password, project_ID, project_name, env_path): 329 | # creating connection to trac to import issues to 330 | client = Client(env_path) 331 | # creating connection to util to import issues in 332 | target = Connection(target_url, target_login, target_password) 333 | 334 | # create project 335 | print("Creating project[%s]" % project_name) 336 | try: 337 | target.getProject(project_ID) 338 | except youtrack.YouTrackException: 339 | target.createProjectDetailed(project_ID, project_name, client.get_project_description(), target_login) 340 | 341 | # importing users 342 | trac_users = client.get_users() 343 | print("Importing users") 344 | yt_users = list([]) 345 | 346 | # converting trac users to yt users 347 | registered_users = set([]) 348 | for user in trac_users : 349 | print("Processing user [ %s ]" % user.name) 350 | registered_users.add(user.name) 351 | yt_users.append(to_youtrack_user(user)) 352 | # adding users to yt project 353 | target.importUsers(yt_users) 354 | print("Importing users finished") 355 | 356 | print("Creating project custom fields") 357 | 358 | create_yt_custom_field(target, project_ID, "Priority", client.get_issue_priorities()) 359 | 360 | create_yt_custom_field(target, project_ID, "Type", client.get_issue_types()) 361 | 362 | trac_resolution_to_yt_state = lambda track_field, yt_bundle : to_youtrack_state(track_field, yt_bundle) 363 | create_yt_bundle_custom_field(target, project_ID, "Resolution", client.get_issue_resolutions(), trac_resolution_to_yt_state) 364 | 365 | trac_version_to_yt_version = lambda trac_field, yt_bundle : to_youtrack_version(trac_field, yt_bundle) 366 | 367 | trac_versions = client.get_versions() 368 | create_yt_bundle_custom_field(target, project_ID, "Affected versions", trac_versions, trac_version_to_yt_version) 369 | 370 | trac_milestones = client.get_milestones() 371 | create_yt_bundle_custom_field(target, project_ID, "Fix versions", trac_milestones, trac_version_to_yt_version) 372 | 373 | trac_components = client.get_components() 374 | for cmp in trac_components : 375 | if cmp.owner not in registered_users : 376 | cmp.owner, registered_users = process_non_authorised_user(target, registered_users, cmp.owner) 377 | trac_component_to_yt_subsystem = lambda trac_field, yt_bundle : to_youtrack_subsystem(trac_field, yt_bundle) 378 | create_yt_bundle_custom_field(target, project_ID, "Component", trac_components, trac_component_to_yt_subsystem) 379 | 380 | create_yt_custom_field(target, project_ID, "Severity", client.get_severities()) 381 | 382 | trac_custom_fields = client.get_custom_fields_declared() 383 | check_box_fields = dict([]) 384 | for elem in trac_custom_fields: 385 | print("Processing custom field [ %s ]" % elem.name) 386 | if elem.type == "checkbox": 387 | if len(elem.label) > 0: 388 | opt = elem.label 389 | else: 390 | opt = elem.name 391 | options = list([opt]) 392 | check_box_fields[elem.name] = opt 393 | else: 394 | options = elem.options 395 | 396 | values = None 397 | if len(options): 398 | values = options 399 | 400 | field_name = elem.name 401 | if field_name in youtrackutils.tracLib.FIELD_NAMES.keys() : 402 | field_name = youtrackutils.tracLib.FIELD_NAMES[field_name] 403 | 404 | field_type = youtrackutils.tracLib.CUSTOM_FIELD_TYPES[elem.type] 405 | if field_name in youtrackutils.tracLib.FIELD_TYPES.keys(): 406 | field_type = youtrackutils.tracLib.FIELD_TYPES[field_name] 407 | 408 | process_custom_field(target, project_ID, field_type, field_name, trac_values_to_youtrack_values(field_name, values)) 409 | print("Creating project custom fields finished") 410 | 411 | print("Importing issues") 412 | trac_issues = client.get_issues() 413 | yt_issues = list([]) 414 | counter = 0 415 | max = 100 416 | for issue in trac_issues: 417 | print("Processing issue [ %s ]" % (str(issue.id))) 418 | counter += 1 419 | if not (issue.reporter in registered_users): 420 | yt_user, registered_users = process_non_authorised_user(target, registered_users, issue.reporter) 421 | if yt_user is None : 422 | issue.reporter = "guest" 423 | else: 424 | issue.reporter = yt_user 425 | if not (issue.owner in registered_users): 426 | yt_user, registered_users = process_non_authorised_user(target, registered_users, issue.owner) 427 | if yt_user is None : 428 | issue.owner = "" 429 | else: 430 | issue.owner = yt_user 431 | legal_cc = set([]) 432 | for cc in issue.cc: 433 | if cc in registered_users: 434 | legal_cc.add(cc) 435 | issue.cc = legal_cc 436 | 437 | yt_issues.append(to_youtrack_issue(project_ID, issue, check_box_fields)) 438 | if counter == max: 439 | counter = 0 440 | print(target.importIssues(project_ID, project_name + ' Assignees', yt_issues)) 441 | yt_issues = list([]) 442 | print(target.importIssues(project_ID, project_name + ' Assignees', yt_issues)) 443 | print('Importing issues finished') 444 | 445 | # importing tags 446 | print("Importing keywords") 447 | for issue in trac_issues: 448 | print("Importing tags from issue [ %s ]" % (str(issue.id))) 449 | tags = issue.keywords 450 | for t in tags: 451 | target.executeCommand(str(project_ID) + "-" + str(issue.id), "tag " + t.encode('utf-8')) 452 | print("Importing keywords finished") 453 | 454 | print("Importing attachments") 455 | for issue in trac_issues: 456 | print("Processing issue [ %s ]" % (str(issue.id))) 457 | issue_attach = issue.attachment 458 | for attach in issue_attach: 459 | print("Processing attachment [ %s ]" % attach.filename.encode('utf-8')) 460 | if not (attach.author_name in registered_users): 461 | yt_user, registered_users = process_non_authorised_user(target, registered_users, attach.author_name) 462 | if yt_user is None: 463 | attach.author_name = "guest" 464 | else: 465 | attach.author_name = yt_user 466 | content = open(urllib.quote(attach.filename.encode('utf-8'))) 467 | target.createAttachment(str(project_ID) + "-" + str(issue.id), attach.name, content, attach.author_name, 468 | created=attach.time) 469 | print("Importing attachments finished") 470 | 471 | print("Importing workitems") 472 | tt_enabled = False 473 | for issue in trac_issues: 474 | if issue.workitems: 475 | if not tt_enabled: 476 | tt_settings = target.getProjectTimeTrackingSettings(str(project_ID)) 477 | if not tt_settings.Enabled: 478 | print("Enabling TimeTracking for the project") 479 | target.setProjectTimeTrackingSettings(str(project_ID), enabled=True) 480 | tt_enabled = True 481 | print("Processing issue [ %s ]" % (str(issue.id))) 482 | workitems = [to_youtrack_workitem(w) for w in issue.workitems] 483 | target.importWorkItems(str(project_ID) + "-" + str(issue.id), workitems) 484 | print("Importing workitems finished") 485 | 486 | if __name__ == "__main__": 487 | main() 488 | -------------------------------------------------------------------------------- /youtrackutils/tracLib/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) 4 | 5 | CUSTOM_FIELD_TYPES = None 6 | PERMISSIONS = None 7 | DEFAULT_EMAIL = " " 8 | ACCEPT_NON_AUTHORISED_USERS = True 9 | FIELD_VALUES = dict([]) 10 | FIELD_TYPES = dict([]) 11 | FIELD_NAMES = dict([]) 12 | 13 | SUPPORT_TIME_TRACKING = 'auto' 14 | 15 | class TracUser(object): 16 | 17 | def __init__(self, name): 18 | self.name = name 19 | self.email = "" 20 | 21 | class TracIssue(object): 22 | 23 | def __init__(self, id): 24 | self.id = id 25 | self.type = None 26 | self.time = None 27 | self.changetime = None 28 | self.component = None 29 | self.severity = None 30 | self.priority = None 31 | self.owner = None 32 | self.reporter = None 33 | self.cc = set([]) 34 | self.version = None 35 | self.milestone = None 36 | self.status = None 37 | self.resolution = None 38 | self.summary = None 39 | self.description = None 40 | self.keywords = set([]) 41 | self.custom_fields = {} 42 | self.attachment = set([]) 43 | self.comments = set([]) 44 | self.workitems = set([]) 45 | 46 | class TracVersion(object): 47 | 48 | def __init__(self, name): 49 | self.name = name 50 | self.time = None 51 | self.description = "" 52 | 53 | class TracMilestone(object): 54 | 55 | def __init__(self, name): 56 | self.name = name 57 | self.time = None 58 | self.description = "" 59 | 60 | class TracComponent(object): 61 | 62 | def __init__(self, name): 63 | self.name = name 64 | self.owner = None 65 | self.description = "" 66 | 67 | class TracCustomFieldDeclaration(object): 68 | 69 | def __init__(self, name): 70 | self.name = name 71 | self.type = "text" 72 | self.label = "" 73 | self.options = list([]) 74 | self.value = "" 75 | 76 | def __str__(self): 77 | result = "name : " + self.name + " type : " + self.type + " label : " + self.label 78 | result = result + " value : " + self.value + " options : " 79 | for elem in self.options: 80 | result = result + elem + ", " 81 | return result 82 | 83 | class TracAttachment(object): 84 | 85 | def __init__(self, filename): 86 | self.filename = filename 87 | self.size = -1 88 | self.time = None 89 | self.description = "" 90 | self.author_name = None 91 | self.name = "" 92 | 93 | class TracComment(object): 94 | 95 | def __init__(self, time): 96 | self.time = time 97 | self.author = "" 98 | self.content = "" 99 | self.id = 0 100 | 101 | def __eq__(self, other): 102 | return self.id == other.id 103 | 104 | class TracWorkItem(object): 105 | def __init__(self, time, duration, author, comment): 106 | self.time = time 107 | self.duration = duration 108 | self.author = author 109 | self.comment = comment.strip() if comment is not None else "" 110 | 111 | class TracResolution(object): 112 | 113 | def __init__(self, name): 114 | self.name = name 115 | 116 | 117 | def to_unix_time(time): 118 | return time / 1000 119 | -------------------------------------------------------------------------------- /youtrackutils/tracLib/client.py: -------------------------------------------------------------------------------- 1 | from trac.env import Environment 2 | from trac.attachment import Attachment 3 | from youtrackutils.tracLib import * 4 | from ConfigParser import ConfigParser 5 | from youtrackutils import tracLib 6 | from youtrackutils.tracLib import timetracking 7 | 8 | 9 | class Client(object): 10 | def __init__(self, env_path): 11 | self.env_path = env_path 12 | self.env = Environment(env_path) 13 | # self.db_cnx = self.env.get_db_cnx() 14 | self._registered_users_logins = [] 15 | self._timetracking_plugins = self._get_timetracking_plugins() 16 | 17 | def _get_timetracking_plugins(self): 18 | plugins = {} 19 | if tracLib.SUPPORT_TIME_TRACKING == 'auto': 20 | for plugin in tracLib.timetracking.plugins: 21 | plugin_name = plugin.get_name() 22 | for com_name, com_enabled in self.env._component_rules.items(): 23 | if com_name.startswith(plugin_name) and com_enabled and plugin_name not in plugins: 24 | plugins[plugin_name] = plugin(self.env) 25 | else: 26 | for plugin in tracLib.timetracking.plugins: 27 | plugin_name = plugin.get_name() 28 | if plugin_name == tracLib.SUPPORT_TIME_TRACKING: 29 | plugins[plugin_name] = plugin(self.env) 30 | break 31 | for plugin_name in plugins.keys(): 32 | print "Plugin '%s' will be used to get workitems." % plugin_name 33 | return plugins.values() 34 | 35 | def get_project_description(self): 36 | return self.env.project_description 37 | 38 | def get_users(self): 39 | result = self.env.get_known_users() 40 | trac_users = list([]) 41 | for user in result: 42 | user_login = user[0].lower() 43 | if user_login in self._registered_users_logins: 44 | continue 45 | u = TracUser(user_login) 46 | u.email = user[2] 47 | trac_users.append(u) 48 | self._registered_users_logins.append(user_login) 49 | # if we accept only authorised users, we don't have any more users to return 50 | # all of them were returned by "get_known_users" method 51 | if not tracLib.ACCEPT_NON_AUTHORISED_USERS: 52 | return trac_users 53 | # here we must to get component owners, issue reporters, owners and attachment authors 54 | # that are not registered users 55 | user_fields = [("owner", "component"), ("reporter", "ticket"), ("owner", "ticket"), ("author", "attachment")] 56 | first = True 57 | request = "" 58 | for column_name, table_name in user_fields : 59 | if first: 60 | first = False 61 | else: 62 | request += "UNION " 63 | request += "SELECT DISTINCT lower(%s) FROM %s " % (column_name, table_name) 64 | with self.env.db_query as db: 65 | cursor = db.cursor() 66 | cursor.execute(request) 67 | for row in cursor: 68 | if row[0] not in self._registered_users_logins: 69 | trac_user = self._get_non_authorised_user(row[0]) 70 | if trac_user is not None : 71 | trac_users.append(trac_user) 72 | self._registered_users_logins.append(trac_user.name) 73 | return trac_users 74 | 75 | def _get_non_authorised_user(self, user_name): 76 | if user_name is None : 77 | return None 78 | # non authorized users in trac are stored like this "name " 79 | start = user_name.find("<") 80 | end = user_name.rfind(">") 81 | # we don't accept users who didn't leave the email 82 | if (start > -1) and (end > start + 1): 83 | if user_name.find("@", start, end) > 0: 84 | user = TracUser(user_name[start + 1 : end].replace(" ", "_")) 85 | user.email = user_name[start + 1 : end].replace(" ", "_") 86 | return user 87 | return None 88 | 89 | def _get_user_login(self, user_name): 90 | if user_name is None: 91 | return None 92 | if user_name in self._registered_users_logins: 93 | return user_name 94 | if not tracLib.ACCEPT_NON_AUTHORISED_USERS: 95 | return None 96 | user = self._get_non_authorised_user(user_name) 97 | if (user is None) or (user.name not in self._registered_users_logins) : 98 | return None 99 | return user.name 100 | 101 | def get_severities(self): 102 | return self._get_data_from_enum("severity") 103 | 104 | def get_issue_types(self): 105 | return self._get_data_from_enum("ticket_type") 106 | 107 | def get_issue_priorities(self): 108 | return self._get_data_from_enum("priority") 109 | 110 | def get_issue_resolutions(self): 111 | return [TracResolution(name) for name in self._get_data_from_enum("resolution")] 112 | 113 | def get_components(self): 114 | with self.env.db_query as db: 115 | cursor = db.cursor() 116 | cursor.execute("SELECT name, owner, description FROM component") 117 | trac_components = list([]) 118 | for row in cursor: 119 | component = TracComponent(row[0]) 120 | component.owner = self._get_user_login(component.owner) 121 | if row[2] is not None: 122 | component.description = row[2] 123 | trac_components.append(component) 124 | return trac_components 125 | 126 | def get_versions(self): 127 | with self.env.db_query as db: 128 | cursor = db.cursor() 129 | cursor.execute("SELECT name, time, description FROM version") 130 | trac_versions = list([]) 131 | for row in cursor: 132 | version = TracVersion(row[0]) 133 | if row[1]: 134 | version.time = to_unix_time(row[1]) 135 | if row[2] is not None: 136 | version.description = row[2] 137 | trac_versions.append(version) 138 | return trac_versions 139 | 140 | def get_milestones(self): 141 | with self.env.db_query as db: 142 | cursor = db.cursor() 143 | cursor.execute("SELECT name, completed, description FROM milestone") 144 | trac_milestones = list([]) 145 | for row in cursor: 146 | version = TracMilestone(row[0]) 147 | if row[1]: 148 | version.time = to_unix_time(row[1]) 149 | if row[2] is not None: 150 | version.description = row[2] 151 | trac_milestones.append(version) 152 | return trac_milestones 153 | 154 | def get_issues(self): 155 | with self.env.db_query as db: 156 | cursor = db.cursor() 157 | cursor.execute("SELECT id, type, time, changetime, component, severity, priority, owner, reporter," 158 | "cc, version, milestone, status, resolution, summary, description, keywords FROM ticket") 159 | trac_issues = list([]) 160 | for row in cursor: 161 | issue = TracIssue(row[0]) 162 | issue.time = to_unix_time(row[2]) 163 | issue.changetime = to_unix_time(row[3]) 164 | issue.reporter = self._get_user_login(row[8]) 165 | if row[9] is not None: 166 | cc = row[9].split(",") 167 | for c in cc: 168 | if len(c) > 0: 169 | cc_name = self._get_user_login(c.strip()) 170 | if cc_name is not None: 171 | issue.cc.add(cc_name) 172 | issue.summary = row[14] 173 | issue.description = row[15] 174 | issue.custom_fields["Type"] = row[1] 175 | issue.custom_fields["Component"] = None if row[4] is None else row[4].replace("/", "-") 176 | issue.custom_fields["Severity"] = row[5] 177 | issue.custom_fields["Priority"] = row[6] 178 | issue.custom_fields["Owner"] = self._get_user_login(row[7]) 179 | issue.custom_fields["Version"] = row[10] 180 | issue.custom_fields["Milestone"] = row[11] 181 | issue.custom_fields["Status"] = row[12] 182 | issue.custom_fields["Resolution"] = row[13] 183 | if row[16] is not None: 184 | keywords = row[16].rsplit(",") 185 | for kw in keywords: 186 | if len(kw) > 0: 187 | issue.keywords.add(kw.strip()) 188 | #getting custom fields from ticket_custom table 189 | with self.env.db_query as db: 190 | custom_field_cursor = db.cursor() 191 | custom_field_cursor.execute("SELECT name, value FROM ticket_custom WHERE ticket=%s", (str(row[0]),)) 192 | for cf in custom_field_cursor: 193 | issue.custom_fields[cf[0].capitalize()] = cf[1] 194 | # getting attachments from attachment table 195 | with self.env.db_query as db: 196 | attachment_cursor = db.cursor() 197 | attachment_cursor.execute("SELECT filename, size, time, description, author FROM attachment WHERE " 198 | "type = %s AND id = %s", ("ticket", str(issue.id))) 199 | #path = self.env_path + "/attachments/ticket/" + str(issue.id) + "/" 200 | for elem in attachment_cursor: 201 | #at = TracAttachment(path + elem[0]) 202 | at = TracAttachment(Attachment._get_path(self.env.path, 'ticket', str(issue.id), elem[0])) 203 | at.name = elem[0] 204 | at.size = elem[1] 205 | at.time = to_unix_time(elem[2]) 206 | at.description = elem[3] 207 | at.author_name = elem[4] 208 | issue.attachment.add(at) 209 | trac_issues.append(issue) 210 | #getting comments 211 | with self.env.db_query as db: 212 | change_cursor = db.cursor() 213 | change_cursor.execute("SELECT time, author, newvalue, oldvalue FROM ticket_change WHERE ticket = %s AND field = %s ORDER BY time DESC", (str(row[0]), "comment",)) 214 | for elem in change_cursor: 215 | if (elem[2] is None) or (not len(elem[2].lstrip())): 216 | continue 217 | comment = TracComment(to_unix_time(elem[0])) 218 | comment.author = str(elem[1]) 219 | comment.content = unicode(elem[2]) 220 | comment.id = elem[3] 221 | issue.comments.add(comment) 222 | #getting workitems 223 | for ttp in self._timetracking_plugins: 224 | issue.workitems.update(set(ttp[row[0]])) 225 | return trac_issues 226 | 227 | 228 | def get_custom_fields_declared(self): 229 | ini_file_path = self.env_path + "/conf/trac.ini" 230 | parser = ConfigParser() 231 | parser.read(ini_file_path) 232 | if not("ticket-custom" in parser.sections()): 233 | return set([]) 234 | result = parser.items("ticket-custom") 235 | items = dict([]) 236 | for elem in result: 237 | items[elem[0]] = elem[1] 238 | 239 | keys = items.keys() 240 | custom_fields = list([]) 241 | for k in keys: 242 | if not("." in k): 243 | field = TracCustomFieldDeclaration(k.capitalize()) 244 | field.type = items[k] 245 | options_key = k + ".options" 246 | if options_key in items: 247 | opts_str = items[options_key] 248 | opts = opts_str.rsplit("|") 249 | for o in opts: 250 | field.options.append(o) 251 | value_key = k + ".value" 252 | if value_key in items: 253 | field.value = items[value_key] 254 | label_key = k + ".label" 255 | if label_key in items: 256 | field.label = items[label_key] 257 | custom_fields.append(field) 258 | 259 | return custom_fields 260 | 261 | def _get_data_from_enum(self, type_name): 262 | with self.env.db_query as db: 263 | cursor = db.cursor() 264 | cursor.execute("SELECT name, value FROM enum WHERE type=%s", (type_name,)) 265 | return [row[0] for row in cursor] 266 | -------------------------------------------------------------------------------- /youtrackutils/tracLib/defaultTrac.py: -------------------------------------------------------------------------------- 1 | from youtrackutils import tracLib 2 | 3 | #if you defined your own types you should add them to the map 4 | TYPES = { 5 | "defect" : "Bug", 6 | "enhancement" : "Feature", 7 | "task" : "Task", 8 | } 9 | 10 | #if you defined your own priorities you should add them to the map 11 | PRIORITIES = { 12 | "trivial" : "Minor", #Minor 13 | "minor" : "Normal", #Normal 14 | "major" : "Major", #Major 15 | "critical" : "Critical", #Critical 16 | "blocker" : "Show-stopper" #Show-stopper 17 | } 18 | #we convert resolutions and statuses into statuses 19 | RESOLUTIONS = { 20 | "duplicate" : "Duplicate", 21 | "fixed" : "Fixed", 22 | "wontfix" : "Won't fix", 23 | "worksforme" : "Can't Reproduce", 24 | "invalid" : "Can't Reproduce" 25 | # : "To be discussed 26 | } 27 | STATES = { 28 | "accepted" : "Submitted", 29 | "new" : "Open", 30 | "reopened" : "Reopened", 31 | "assigned" : "Submitted", 32 | "closed" : None 33 | } 34 | 35 | # if you don't change rules of importing, don't change this map 36 | tracLib.CUSTOM_FIELD_TYPES = { 37 | "text" : "string", 38 | "checkbox" : "enum[*]", 39 | "select" : "enum[1]", 40 | "radio" : "enum[1]", 41 | "textarea" : "string" 42 | } 43 | 44 | tracLib.FIELD_VALUES = { 45 | "Priority" : PRIORITIES, 46 | "Type" : TYPES, 47 | "State" : dict(RESOLUTIONS.items() + STATES.items()), 48 | "YT Select" : {"uno" : "1", "dos" : "2", "tres" : "3", "cuatro" : "4"} 49 | } 50 | 51 | tracLib.FIELD_TYPES = { 52 | "Priority" : "enum[1]", 53 | "Type" : "enum[1]", 54 | "State" : "state[1]", 55 | "Fix versions" : "version[*]", 56 | "Affected versions" : "version[*]", 57 | "Assignee" : "user[1]", 58 | "Severity" : "enum[1]", 59 | "YT Select" : "enum[*]", 60 | "Subsystem" : "ownedField[1]" 61 | } 62 | 63 | tracLib.FIELD_NAMES = { 64 | "Resolution" : "State", 65 | "Status" : "State", 66 | "Owner" : "Assignee", 67 | "Version" : "Fix versions", 68 | "Component" : "Subsystem", 69 | "Test_five" : "YT Select" 70 | } 71 | 72 | 73 | # the default email to register users who doesn't have one 74 | tracLib.DEFAULT_EMAIL = "user@server.com" 75 | # if true users who were not authorized are registered 76 | # else they are known as guests 77 | tracLib.ACCEPT_NON_AUTHORISED_USERS = True 78 | 79 | # Enable support for plugins to import timetracking data into YouTrack. 80 | # You can set this option manualy if autodetection doesn't work correctly. 81 | # Available options: 82 | # - trachours 83 | # - timingandestimationplugin 84 | tracLib.SUPPORT_TIME_TRACKING = "auto" 85 | -------------------------------------------------------------------------------- /youtrackutils/tracLib/timetracking.py: -------------------------------------------------------------------------------- 1 | from youtrackutils.tracLib import TracWorkItem, to_unix_time 2 | 3 | class TimeTrackingPlugin(object): 4 | @classmethod 5 | def get_name(self): 6 | raise NotImplementedError 7 | 8 | def __init__(self, trac_env): 9 | self.env = trac_env 10 | 11 | def _get_issue_workitems(self, ticket_id): 12 | raise NotImplementedError 13 | 14 | def _build_workitem(self, time, duration, author, comment=None): 15 | return TracWorkItem(time, duration, author, comment) 16 | 17 | def __getitem__(self, id_): 18 | return self._get_issue_workitems(id_) 19 | 20 | 21 | class TimeHoursPlugin(TimeTrackingPlugin): 22 | @classmethod 23 | def get_name(self): 24 | return 'trachours' 25 | 26 | def _get_issue_workitems(self, ticket_id): 27 | query = """ 28 | SELECT 29 | time_started * 1000, /* convert to format with millis */ 30 | seconds_worked, 31 | worker, 32 | comments 33 | FROM 34 | ticket_time 35 | WHERE 36 | ticket=%s 37 | ORDER BY 38 | time_started DESC 39 | """ % str(ticket_id) 40 | 41 | workitems = [] 42 | for time, duration, author, comment in self.env.db_query(query): 43 | workitems.append( 44 | self._build_workitem(time, duration, author, comment)) 45 | 46 | return workitems 47 | 48 | 49 | class TimingAndEstimationPlugin(TimeTrackingPlugin): 50 | @classmethod 51 | def get_name(self): 52 | return 'timingandestimationplugin' 53 | 54 | def _get_issue_workitems(self, ticket_id): 55 | query = """ 56 | SELECT 57 | time, 58 | newvalue * 3600, /* convert hours to seconds */ 59 | author 60 | FROM 61 | ticket_change 62 | WHERE 63 | ticket=%s 64 | AND 65 | field='hours' 66 | ORDER BY 67 | time DESC 68 | """ % str(ticket_id) 69 | 70 | workitems = [] 71 | for time, duration, author in self.env.db_query(query): 72 | workitems.append( 73 | self._build_workitem(to_unix_time(time), duration, author)) 74 | 75 | return workitems 76 | 77 | 78 | plugins = (TimeHoursPlugin, 79 | TimingAndEstimationPlugin) 80 | -------------------------------------------------------------------------------- /youtrackutils/userTool.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | import getopt 10 | import os 11 | from youtrack.sync.users import UserImporter 12 | from youtrack.connection import Connection 13 | 14 | 15 | def _import_all_users(source, target, import_groups): 16 | print('Starting user importing') 17 | user_importer = UserImporter( 18 | source, target, caching_users=True, import_groups=import_groups) 19 | start = 0 20 | imported_number = 0 21 | users_to_import = source.getUsersTen(start) 22 | while len(users_to_import): 23 | refined_users = [source.getUser(user.login) for user in users_to_import] 24 | imported_number += user_importer.importUsersRecursively(refined_users) 25 | start += 10 26 | users_to_import = source.getUsersTen(start) 27 | if imported_number % 20 == 0: 28 | print('Imported ' + str(imported_number) + ' users') 29 | print('Finished. Total number of imported users: ' + str(imported_number)) 30 | 31 | 32 | def _delete_all_users_except_root_and_guest(target): 33 | print('Starting user deleting') 34 | excluded = ['root', 'guest'] 35 | deleted_number = 0 36 | users_to_delete = target.getUsersTen(0) 37 | while len(users_to_delete) > 2: 38 | for user in users_to_delete: 39 | if _check_login(user.login, excluded): 40 | target.deleteUser(user.login) 41 | deleted_number += 1 42 | users_to_delete = target.getUsersTen(0) 43 | if deleted_number % 50 == 0: 44 | print('Deleted ' + str(deleted_number) + ' users') 45 | print('Finished. Total number of deleted users: ' + str(deleted_number)) 46 | 47 | 48 | def _delete_all_groups_except_initial(target): 49 | print('Starting group deleting') 50 | excluded = ['All Users', 'New Users', 'Reporters'] 51 | deleted_number = 0 52 | groups_to_delete = target.getGroups() 53 | for group in groups_to_delete: 54 | if group.name not in excluded: 55 | target.deleteGroup(group.name) 56 | deleted_number += 1 57 | print('Deleted ' + str(group.name) + ' users') 58 | print('Finished. Total number of deleted groups: ' + str(deleted_number)) 59 | 60 | 61 | def _import_one_user_with_groups(source, target, login): 62 | print('Starting user importing') 63 | user_importer = UserImporter(source, target, caching_users=False) 64 | users_to_import = [source.getUser(login)] 65 | if _check_login(login): 66 | user_importer.importUsersRecursively(users_to_import) 67 | print('User was successfully imported') 68 | else: 69 | print('User login ' + str(login) + ' contains prohibited chars') 70 | 71 | 72 | def _check_login(login, excluded=None): 73 | if not excluded: 74 | excluded = [] 75 | return login not in excluded and '/' not in login 76 | 77 | 78 | def import_users( 79 | source_url, source_login, source_password, 80 | target_url, target_login, target_password, import_groups=False): 81 | 82 | source = Connection(source_url, source_login, source_password) 83 | target = Connection(target_url, target_login, target_password) 84 | 85 | _import_all_users(source, target, import_groups) 86 | # _delete_all_users_except_root_and_guest(target) 87 | # _delete_all_groups_except_initial(target) 88 | # _import_one_user_with_groups(source, target, 'batman') 89 | 90 | 91 | def usage(): 92 | print(""" 93 | Usage: 94 | %s [OPTIONS] s_url s_user s_pass t_url t_user t_pass 95 | 96 | s_url Source YouTrack URL 97 | s_user Source YouTrack user 98 | s_pass Source YouTrack user's password 99 | t_url Target YouTrack URL 100 | t_user Target YouTrack user 101 | t_pass Target YouTrack user's password 102 | 103 | Options: 104 | -h, Show this help and exit 105 | -g, Import groups for users 106 | """ % os.path.basename(sys.argv[0])) 107 | 108 | 109 | def main(): 110 | import_groups = False 111 | try: 112 | opts, args = getopt.getopt(sys.argv[1:], 'hg') 113 | for opt, val in opts: 114 | if opt == '-h': 115 | usage() 116 | sys.exit(0) 117 | elif opt == '-g': 118 | import_groups = True 119 | (source_url, source_login, source_password, 120 | target_url, target_login, target_password) = args[0:6] 121 | except getopt.GetoptError as e: 122 | print(e) 123 | usage() 124 | sys.exit(1) 125 | except ValueError: 126 | print('Not enough arguments') 127 | sys.exit(1) 128 | 129 | import_users(source_url, source_login, source_password, 130 | target_url, target_login, target_password, import_groups) 131 | 132 | if __name__ == "__main__": 133 | main() 134 | 135 | -------------------------------------------------------------------------------- /youtrackutils/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) -------------------------------------------------------------------------------- /youtrackutils/utils/mapfile.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import io 5 | 6 | default_mapping_filename = 'mapping.json' 7 | 8 | 9 | def dump_map_file(mapping_data, mapping_filename=None): 10 | if not mapping_filename: 11 | mapping_filename = default_mapping_filename 12 | if os.path.isfile(mapping_filename): 13 | print("Mapping file already exists: " + 14 | os.path.abspath(mapping_filename)) 15 | sys.exit(1) 16 | try: 17 | with io.open(mapping_filename, mode='w', encoding='utf-8') as f: 18 | f.write(unicode(json.dumps(mapping_data, ensure_ascii=False, sort_keys=True, indent=4))) 19 | print("Mapping file has been written to " + 20 | os.path.abspath(mapping_filename)) 21 | except (IOError, OSError) as e: 22 | print("Failed to write mapping file: " + str(e)) 23 | sys.exit(1) 24 | 25 | 26 | def load_map_file(mapping_filename=None): 27 | if not mapping_filename: 28 | mapping_filename = default_mapping_filename 29 | try: 30 | with io.open(mapping_filename, mode='r', encoding='utf-8') as f: 31 | return json.load(f) 32 | except (OSError, IOError) as e: 33 | print("Failed to read mapping file: " + str(e)) 34 | sys.exit(1) 35 | except (KeyError, ValueError) as e: 36 | print("Bad mapping file: " + str(e)) 37 | sys.exit(1) 38 | -------------------------------------------------------------------------------- /youtrackutils/zendesk/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.warn("deprecated", DeprecationWarning) 4 | 5 | __author__ = 'user' 6 | 7 | NAMES = { 8 | u'assignee_id' : u'Assignee', 9 | u'created_at' : u'created', 10 | u'due_at' : u'Due date', 11 | u'id' : u'numberInProject', 12 | u'requester_id' : u'reporterName', 13 | u'subject' : u'summary', 14 | u'updated_at' : u'updated', 15 | u'status' : u'state', 16 | u'organization_id' : u'organization', 17 | u'tags' : u'Tags', 18 | u'collaborator_ids' : u'watcherName' 19 | } 20 | -------------------------------------------------------------------------------- /youtrackutils/zendesk/zendeskClient.py: -------------------------------------------------------------------------------- 1 | __author__ = 'user' 2 | import httplib2 3 | import json 4 | 5 | class ZendeskClient: 6 | def __init__(self, url, login, password): 7 | self._http = httplib2.Http(disable_ssl_certificate_validation=True) 8 | self._url = url 9 | self._http.add_credentials(login, password) 10 | 11 | def _rest_url(self): 12 | return self._url + "/api/v2" 13 | 14 | def get_issues(self): 15 | iterator = PageIterator(self, "/tickets.json", u"tickets") 16 | for elem in iterator: 17 | org_id_key = u'organization_id' 18 | org_id = elem.get(org_id_key) 19 | if org_id is not None: 20 | elem[org_id_key] = self.get_organization(org_id)[u'name'] 21 | yield elem 22 | 23 | def get_ticket_audits(self, ticket_id): 24 | return PageIterator(self, "/tickets/%s/audits.json" % ticket_id, u"audits") 25 | 26 | def get_custom_fields(self): 27 | response, content = self._get("/ticket_fields.json") 28 | if response.status == 200: 29 | return content[u'ticket_fields'] 30 | 31 | def get_custom_field(self, id): 32 | response, content = self._get("/ticket_fields/%s.json" % id) 33 | if response.status == 200: 34 | return content["ticket_field"] 35 | else: 36 | return None 37 | 38 | 39 | def get_organization(self, id): 40 | response, content = self._get("/organizations/" + str(id) + ".json") 41 | if response.status == 200: 42 | return content[u'organization'] 43 | 44 | 45 | def get_user(self, id): 46 | response, content = self._get("/users/" + str(id) + ".json") 47 | if response.status == 200: 48 | return content[u'user'] 49 | 50 | def get_groups_for_user(self, id): 51 | iterator = PageIterator(self, "/users/" + str(id) + "/group_memberships.json", u"group_memberships") 52 | return [self.get_group(gm["group_id"])["name"] for gm in iterator] 53 | 54 | def get_group(self, id): 55 | response, content = self._get("/groups/" + str(id) + ".json") 56 | if response.status == 200: 57 | return content[u'group'] 58 | 59 | 60 | def _get(self, url): 61 | response, content = self._http.request(self._rest_url() + url) 62 | return response, json.loads(content) 63 | 64 | class PageIterator: 65 | def __init__(self, zd_client, url, entities_name): 66 | self._zd_client = zd_client 67 | self._url = url 68 | self._entities_name = entities_name 69 | self._current_page = 0 70 | self._values = [] 71 | 72 | def __iter__(self): 73 | return self 74 | 75 | def next(self): 76 | if not len(self._values): 77 | self._current_page += 1 78 | response, content = self._zd_client._get(self._url + "?page=" + str(self._current_page) + "&limit=100") 79 | if response.status != 200: 80 | raise StopIteration 81 | self._values = content[self._entities_name] 82 | if not len(self._values): 83 | raise StopIteration 84 | return self._values.pop(0) 85 | -------------------------------------------------------------------------------- /youtrackutils/zendesk2youtrack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 0): 6 | print("\nThe script doesn't support python 3. Please use python 2.7+\n") 7 | sys.exit(1) 8 | 9 | from youtrack.connection import Connection 10 | from youtrack.youtrackImporter import YouTrackImporter, YouTrackImportConfig 11 | from youtrack.youtrackImporter import AUTO_ATTACHED, NAME, TYPE, POLICY 12 | from youtrack import User, Group, Comment 13 | import youtrackutils.zendesk 14 | from youtrackutils.zendesk.zendeskClient import ZendeskClient 15 | import datetime 16 | import calendar 17 | import urllib2 18 | 19 | 20 | __author__ = 'user' 21 | 22 | 23 | def main(): 24 | source_url, source_login, source_password, target_url, target_login, target_password, project_id = sys.argv[1:8] 25 | zendesk2youtrack(source_url, source_login, source_password, target_url, target_login, target_password, project_id) 26 | 27 | 28 | def zendesk2youtrack(source_url, source_login, source_password, target_url, target_login, target_password, project_id): 29 | target = Connection(target_url, target_login, target_password) 30 | source = ZendeskClient(source_url, source_login, source_password) 31 | 32 | importer = ZendeskYouTrackImporter(source, target, ZendeskYouTrackImportConfig( 33 | youtrackutils.zendesk.NAMES, {}, {})) 34 | importer.do_import({project_id: project_id}) 35 | 36 | 37 | class ZendeskYouTrackImporter(YouTrackImporter): 38 | def __init__(self, source, target, import_config): 39 | super(ZendeskYouTrackImporter, self).__init__(source, target, import_config) 40 | 41 | def _get_fields_with_values(self, project_id): 42 | return [] 43 | 44 | def _to_yt_issue(self, issue, project_id): 45 | yt_issue = super(ZendeskYouTrackImporter, self)._to_yt_issue(issue, project_id) 46 | for item in issue[u'custom_fields']: 47 | self.process_field(self._source.get_custom_field(str(item[u'id']))[u'title'], project_id, yt_issue, item[u'value']) 48 | return yt_issue 49 | 50 | def _to_yt_comment(self, comment): 51 | yt_comment = Comment() 52 | user = self._to_yt_user(comment[u'author_id']) 53 | self._import_user(user) 54 | yt_comment.author = user.login 55 | yt_comment.text = comment[u'body'] 56 | yt_comment.created = self.to_unix_date(comment[u'created_at']) 57 | return yt_comment 58 | 59 | def _get_attachments(self, issue): 60 | result = [] 61 | issue_id = self._get_issue_id(issue) 62 | for audit in self._source.get_ticket_audits(issue_id): 63 | created = audit[u'created_at'] 64 | for event in audit[u'events']: 65 | attachments_key = u'attachments' 66 | if (attachments_key in event) and (len(event[attachments_key])): 67 | user = self._to_yt_user(event["author_id"]) 68 | self._import_user(user) 69 | for attachment in event[attachments_key]: 70 | result.append(ZdAttachment(attachment[u"file_name"], self.to_unix_date(created), user.login, attachment[u"content_url"])) 71 | return result 72 | 73 | def _get_issues(self, project_id): 74 | return self._source.get_issues() 75 | 76 | def _get_comments(self, issue): 77 | result = [] 78 | for audit in self._source.get_ticket_audits(self._get_issue_id(issue)): 79 | created = audit[u"created_at"] 80 | for event in audit[u'events']: 81 | if event[u'type'] == u"Comment": 82 | event[u"created_at"] = created 83 | result.append(event) 84 | return result[1:] 85 | 86 | def _get_custom_fields_for_projects(self, project_ids): 87 | fields = self._source.get_custom_fields() 88 | result = [] 89 | for field in fields: 90 | yt_field = {NAME: self._import_config.get_field_name(field[u'title'])} 91 | yt_field[AUTO_ATTACHED] = True 92 | yt_field[TYPE] = self._import_config.get_field_type(yt_field[NAME], field[u'type']) 93 | if yt_field[TYPE] is not None: 94 | result.append(yt_field) 95 | return result 96 | 97 | def _get_issue_links(self, project_id, after, limit): 98 | return [] 99 | 100 | def _to_yt_user(self, value): 101 | user = self._source.get_user(value) 102 | yt_groups = [] 103 | for g in self._source.get_groups_for_user(value): 104 | ytg = Group() 105 | ytg.name = g 106 | yt_groups.append(ytg) 107 | yt_user = User() 108 | if user[u'email'] is None: 109 | yt_user.email = "example@example.com" 110 | yt_user.login = user[u'name'].replace(" ", "_") 111 | else: 112 | yt_user.email = user[u'email'] 113 | yt_user.login = yt_user.email 114 | yt_user.fullName = user[u'name'] 115 | yt_user.getGroups = lambda: yt_groups 116 | return yt_user 117 | 118 | def to_unix_date(self, date): 119 | dt = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") 120 | return str(calendar.timegm(dt.timetuple()) * 1000) 121 | 122 | 123 | class ZendeskYouTrackImportConfig(YouTrackImportConfig): 124 | def __init__(self, name_mapping, type_mapping, value_mapping=None): 125 | super(ZendeskYouTrackImportConfig, self).__init__(name_mapping, type_mapping, value_mapping) 126 | 127 | def get_predefined_fields(self): 128 | return [ 129 | {NAME: u'Type', TYPE: u'enum[1]', POLICY: '0'}, 130 | {NAME: u'Priority', TYPE: u'enum[1]', POLICY: '0'}, 131 | {NAME: u'State', TYPE: u'state[1]', POLICY: '0'}, 132 | {NAME: u'Assignee', TYPE: u'user[1]', POLICY: '2'}, 133 | {NAME: u'Due date', TYPE: u'date'}, 134 | {NAME: u'Organization', TYPE: u'enum[1]', POLICY: '0'} 135 | ] 136 | 137 | def get_field_type(self, name, type): 138 | types = {u"text": u"string", u"checkbox": u"enum[*]", u"date": u"date", u"integer": u"integer", 139 | u"decimal": u"float", u"regexp": u"string", u"tagger": u"enum[1]"} 140 | return types.get(type) 141 | 142 | 143 | class ZdAttachment(): 144 | def __init__(self, name, created, author_login, url): 145 | self.name = name 146 | self.created = created 147 | self.authorLogin = author_login 148 | self._url = url 149 | 150 | def getContent(self): 151 | f = urllib2.urlopen(urllib2.Request(self._url)) 152 | return f 153 | 154 | 155 | if __name__ == "__main__": 156 | main() 157 | --------------------------------------------------------------------------------