├── launchpad_report ├── __init__.py ├── hcf.html ├── leads.html ├── hcf.yaml ├── render.py ├── config.yaml ├── utils.py ├── leads.yaml ├── template.html ├── checks.py └── report.py ├── test-requirements.txt ├── requirements.txt ├── .gitignore ├── TODO.txt ├── tox.ini ├── setup.py ├── README.rst ├── cli.py └── lptool.py /launchpad_report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | hacking==0.7 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | launchpadlib 2 | PyYAML 3 | jinja2 4 | argparse 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | *.log 4 | dist 5 | nosetests.xml 6 | *.egg 7 | .testrepository 8 | .tox 9 | .venv 10 | .idea 11 | .DS_Store 12 | *.egg-info 13 | *.csv 14 | /report.html 15 | /report.json 16 | /cache 17 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | # TODO: implement work items 2 | # TODO: implement reviews checks + reviews checks as work items 3 | # TODO: implement bugs series checks as work items 4 | # TODO: proceed blueprint header 5 | # TODO: check milestones for bug series, collect statuses for backports, triage them 6 | # TODO: integration with gerrit 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | skipsdist = True 4 | ;envlist = py26,py27,pep8 5 | envlist = pep8 6 | 7 | [testenv] 8 | usedevelop = True 9 | install_command = pip install {packages} 10 | setenv = VIRTUAL_ENV={envdir} 11 | deps = -r{toxinidir}/test-requirements.txt 12 | commands = 13 | nosetests {posargs:launchpad_report} 14 | 15 | [tox:jenkins] 16 | downloadcache = ~/cache/pip 17 | 18 | [testenv:pep8] 19 | deps = hacking==0.7 20 | usedevelop = False 21 | commands = 22 | flake8 {posargs:.} 23 | 24 | [testenv:venv] 25 | commands = {posargs:} 26 | 27 | [testenv:devenv] 28 | envdir = devenv 29 | usedevelop = True 30 | 31 | [flake8] 32 | ignore = H234,H302,H802 33 | exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools,__init__.py,docs 34 | show-pep8 = True 35 | show-source = True 36 | count = True 37 | 38 | [hacking] 39 | import_exceptions = testtools.matchers 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | def parse_requirements_txt(): 8 | root = os.path.dirname(os.path.abspath(__file__)) 9 | requirements = [] 10 | with open(os.path.join(root, 'requirements.txt'), 'r') as f: 11 | for line in f.readlines(): 12 | line = line.rstrip() 13 | if not line or line.startswith('#'): 14 | continue 15 | requirements.append(line) 16 | return requirements 17 | 18 | 19 | setup( 20 | name='launchpad_report', 21 | version='0.0.1', 22 | description='Find inconsistencies in launchpad blueprints and bugs', 23 | classifiers=[ 24 | "Programming Language :: Python", 25 | "Topic :: Utilities" 26 | ], 27 | author='Mirantis Inc.', 28 | author_email='dpyzhov@mirantis.com', 29 | url='http://wiki.openstack.org/wiki/Fuel', 30 | packages=find_packages(), 31 | zip_safe=False, 32 | install_requires=parse_requirements_txt(), 33 | include_package_data=True, 34 | package_data={'': ['*.yaml']}, 35 | entry_points={'console_scripts': ['lp-report = launchpad_report.cli:main']} 36 | ) 37 | -------------------------------------------------------------------------------- /launchpad_report/hcf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | lp-report: {{ config.project }} 4 | 23 | 24 | 25 |

lp-report: {{ config.project }}

26 |

Bugs on teams

27 | 33 | {% for team_group in team_groups %} 34 |

{{ team_group.grouper }}

35 | 36 | {% for status_group in team_group.list|groupby('short_status') %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% for item in status_group.list|sort(attribute='milestone') %} 48 | {% if item.type == "bug" %} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% endif %} 59 | {% endfor %} 60 | {% endfor %} 61 |
{{ status_group.grouper }} {{ team_group.grouper }} - {{ status_group.list|length }} bug(s)
TitleMilestoneStatusPriorityAssigneeFull NameRequired triage actions
{{ item.title }}{{ item.milestone }}{{ item.status }}{{ item.priority }}{{ item.assignee }}{{ item.name }}{{ item.triage }}
62 | {% endfor %} 63 | 64 | 65 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | launchpad-report 2 | ================ 3 | 4 | Gather csv statistics for launchpad project, aggregate teams info, list triage 5 | actions. 6 | 7 | Report example: 8 | 9 | ,Link,Title,Status,Priority,Team,Nick,Name,Triage actions 10 | bp,https://blueprints.launchpad.net/fuel/+spec/horizon-basic-auth-by-default,Please add basic auth to horizon UI,Unknown,Low,unknown,unassigned,unassigned,No assignee 11 | bp,https://blueprints.launchpad.net/fuel/+spec/external-mongodb-support,Implement possibility to set external MongoDB connection,Needs Code Review,Undefined,mos,iberezovskiy,Ivan Berezovskiy,"No priority, No series" 12 | bug,https://bugs.launchpad.net/fuel/+bug/1332097,Error during the deployment,Confirmed,Critical,library,fuel-library,Fuel Library Team,Related to non-current milestone (4.1.2) 13 | bug,https://bugs.launchpad.net/fuel/+bug/1342617,Need to add possibility to build tarballs for patching only,New,High,python,ikalnitsky,Igor Kalnitsky,Not triaged 14 | 15 | 16 | Installation 17 | ============ 18 | In virtual env, run pip install -r requirements.txt. Check Known Issues. 19 | 20 | Known Issues 21 | ============ 22 | 23 | lazr.authentication (requirement for launchpadlib) is broken on pypi. You can install it manually from `launchpad `_: 24 | 25 | pip install https://launchpad.net/lazr.authentication/trunk/0.1.2/+download/lazr.authentication-0.1.2.tar.gz 26 | 27 | 28 | How to use 29 | ========== 30 | In order to get list of bugs per particular team lead: 31 | $ python cli.py -c launchpad_report/leads.yaml --template=launchpad_report/leads.html 32 | 33 | In order to get list of bugs affecting HCF: 34 | $ python cli.py -c launchpad_report/hcf.yaml --template=launchpad_report/hcf.html 35 | -------------------------------------------------------------------------------- /launchpad_report/leads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | lp-report: {{ config.project }} 4 | 23 | 24 | 25 |

lp-report: {{ config.project }}

26 |

Bugs on teams

27 | 33 | {% for team_group in team_groups %} 34 |

{{ team_group.grouper }}

35 | 36 | {% for status_group in team_group.list|groupby('short_status') %} 37 | {% if status_group.grouper in ['open', 'untriaged'] %} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for item in status_group.list|sort(attribute='milestone') %} 49 | {% if item.type == "bug" %} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% endif %} 60 | {% endfor %} 61 | {% endif %} 62 | {% endfor %} 63 |
{{ status_group.grouper }} {{ team_group.grouper }} - {{ team_group.list|length }} bug(s)
TitleMilestoneStatusPriorityAssigneeFull NameRequired triage actions
{{ item.title }}{{ item.milestone }}{{ item.status }}{{ item.priority }}{{ item.assignee }}{{ item.name }}{{ item.triage }}
64 | {% endfor %} 65 | 66 | 67 | -------------------------------------------------------------------------------- /launchpad_report/hcf.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | - fuel 3 | - mos 4 | # For debug. Proceed only limited amount of items in project 5 | trunc_report: 0 6 | # used for HCF calcs only 7 | hcf: 1 8 | cache_dir: cache 9 | use_auth: True 10 | teams: 11 | Fuel: 12 | fuel-python 13 | alekseyk-ru 14 | dshulyak 15 | rustyrobot 16 | ikalnitsky 17 | nmarkov 18 | aroma-x 19 | akislitsky 20 | kozhukalov 21 | lux-place 22 | fuel-ui 23 | vkramskikh 24 | astepanchuk 25 | bdudko 26 | kpimenova 27 | jkirnosova 28 | fuel-library 29 | fuel-astute 30 | vsharshov 31 | a-gordeev 32 | xenolog 33 | adidenko 34 | raytrac3r 35 | idv1985 36 | vkuklin 37 | sbogatkin 38 | xdeller 39 | andreika-mail 40 | manashkin 41 | ekozhemyakin 42 | akolesnikov 43 | fsoppelsa 44 | fuel-osci 45 | sotpuschennikov 46 | dburmistrov 47 | vparakhin 48 | r0mikiam 49 | mrasskazov 50 | dborodaenko 51 | rmoe 52 | xarses 53 | tzn 54 | salmon 55 | ksambor 56 | prmtl 57 | loles 58 | sgolovatiuk 59 | bogdando 60 | longgeek 61 | berendt 62 | jesse-pretorius 63 | sammiestoel 64 | zynzel 65 | vpleshakov 66 | vdenisov 67 | nikishov-da 68 | atarasov 69 | dtyzhnenko 70 | mos-linux: 71 | mos-linux 72 | apodrepniy 73 | kdanylov 74 | msemenov 75 | asheplyakov 76 | asyriy 77 | mos-openstack: 78 | mos-neutron 79 | mos-nova 80 | mos-horizon 81 | mos-ceilometer 82 | mos-oslo 83 | mos-sahara 84 | mos-heat 85 | mos-murano 86 | iberezovskiy 87 | e0ne 88 | dmitrymex 89 | smurashov 90 | rpodolyaka 91 | skolekonov 92 | vrovachev 93 | ylobankov 94 | alexei-kornienko 95 | aepifanov 96 | akamyshnikova 97 | dbelova 98 | efedorova 99 | enikanorov 100 | iyozhikov 101 | shakhat 102 | mdurnosvistov 103 | ruhe 104 | smelikyan 105 | skolekonov 106 | skraynev 107 | sreshetniak 108 | tnurlygayanov 109 | tsufiev-x 110 | yorik-sar 111 | mmaxur 112 | maxmazurenka 113 | teselkin-d 114 | obondarev 115 | akuznetsova 116 | i159 117 | akurilin 118 | pkholkin 119 | ekudryashova 120 | Partners: 121 | fuel-partner 122 | eshumakher 123 | izinovik 124 | gcon-monolake 125 | igajsin 126 | moshele 127 | aviramb 128 | srogov 129 | ekorekin 130 | excludes: 131 | fuel-devops 132 | afedorova 133 | teran 134 | acharykov 135 | dreidellhasa 136 | docaedo 137 | fuel-qa 138 | apalkina 139 | aurlapova 140 | tatyana-leontovich 141 | asledzinskiy 142 | apanchenko-8 143 | ykotko 144 | -------------------------------------------------------------------------------- /launchpad_report/render.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from io import BytesIO 5 | from jinja2 import Environment 6 | from jinja2 import FileSystemLoader 7 | 8 | 9 | import codecs 10 | import cStringIO 11 | import csv 12 | 13 | 14 | class UnicodeWriter: 15 | def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): 16 | # Redirect output to a queue 17 | self.queue = cStringIO.StringIO() 18 | self.writer = csv.writer(self.queue, dialect=dialect, **kwds) 19 | self.stream = f 20 | self.encoder = codecs.getincrementalencoder(encoding)() 21 | 22 | def writerow(self, row): 23 | self.writer.writerow([s.encode("utf-8") for s in row]) 24 | # Fetch UTF-8 output from the queue ... 25 | data = self.queue.getvalue() 26 | data = data.decode("utf-8") 27 | # ... and reencode it into the target encoding 28 | data = self.encoder.encode(data) 29 | # write to the target stream 30 | self.stream.write(data) 31 | # empty queue 32 | self.queue.truncate(0) 33 | 34 | def writerows(self, rows): 35 | for row in rows: 36 | self.writerow(row) 37 | 38 | 39 | class Renderer(object): 40 | def __init__(self, filename): 41 | self.filename = filename 42 | 43 | def render(self, data): 44 | if self.filename == '-': 45 | print(self._render(data)) 46 | else: 47 | rep_file = open(self.filename, 'wb') 48 | rep_file.write(self._render(data)) 49 | 50 | 51 | class CSVRenderer(Renderer): 52 | def _render(self, data): 53 | csvfile = BytesIO() 54 | reporter = UnicodeWriter(csvfile) 55 | reporter.writerow([ 56 | '', 'Link', 'Title', 'Milestone', 'Short status', 'Status', 57 | 'Priority', 'Team', 'Nick', 'Name', 'Triage actions' 58 | ]) 59 | for row in data['rows']: 60 | reporter.writerow([ 61 | row['type'], row['link'], row['title'], row['milestone'], 62 | row['short_status'], row['status'], row['priority'], 63 | row['team'], row['assignee'], row['name'], row['triage'] 64 | ]) 65 | return csvfile.getvalue() 66 | 67 | 68 | class JSONRenderer(Renderer): 69 | def _render(self, data): 70 | return json.dumps(data) 71 | 72 | 73 | class HTMLRenderer(Renderer): 74 | def __init__(self, filename, template_filename): 75 | self.filename = filename 76 | self.template_filename = template_filename 77 | 78 | def _render(self, data): 79 | env = Environment( 80 | loader=FileSystemLoader( 81 | os.path.dirname(os.path.abspath(self.template_filename)) 82 | ) 83 | ) 84 | template = env.get_template( 85 | os.path.basename(os.path.abspath(self.template_filename)) 86 | ) 87 | return template.render(data) 88 | -------------------------------------------------------------------------------- /launchpad_report/config.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | - fuel 3 | # For debug. Proceed only limited amount of items in project 4 | trunc_report: 0 5 | cache_dir: cache 6 | use_auth: True 7 | teams: 8 | python: 9 | fuel-python 10 | alekseyk-ru 11 | dshulyak 12 | rustyrobot 13 | ikalnitsky 14 | nmarkov 15 | aroma-x 16 | akislitsky 17 | kozhukalov 18 | lux-place 19 | a-gordeev 20 | ui: 21 | fuel-ui 22 | vkramskikh 23 | astepanchuk 24 | bdudko 25 | kpimenova 26 | jkirnosova 27 | library: 28 | fuel-library 29 | sgolovatiuk 30 | xenolog 31 | adidenko 32 | raytrac3r 33 | idv1985 34 | bogdando 35 | vkuklin 36 | sbogatkin 37 | xdeller 38 | andreika-mail 39 | mmaxur 40 | maxmazurenka 41 | l2: 42 | manashkin 43 | ekozhemyakin 44 | akolesnikov 45 | fsoppelsa 46 | astute: 47 | fuel-astute 48 | vsharshov 49 | osci: 50 | fuel-osci 51 | sotpuschennikov 52 | dburmistrov 53 | vparakhin 54 | r0mikiam 55 | mrasskazov 56 | qa: 57 | fuel-qa 58 | apalkina 59 | aurlapova 60 | tatyana-leontovich 61 | asledzinskiy 62 | apanchenko-8 63 | ykotko 64 | devops: 65 | fuel-devops 66 | afedorova 67 | teran 68 | acharykov 69 | us: 70 | dborodaenko 71 | rmoe 72 | xarses 73 | dreidellhasa 74 | docaedo 75 | moslinux: 76 | mos-linux 77 | apodrepniy 78 | kdanylov 79 | msemenov 80 | asheplyakov 81 | asyriy 82 | mosopenstack: 83 | mos-neutron 84 | mos-nova 85 | mos-horizon 86 | mos-ceilometer 87 | mos-oslo 88 | mos-sahara 89 | mos-heat 90 | mos-murano 91 | iberezovskiy 92 | e0ne 93 | dmitrymex 94 | smurashov 95 | rpodolyaka 96 | skolekonov 97 | vrovachev 98 | ylobankov 99 | alexei-kornienko 100 | aepifanov 101 | akamyshnikova 102 | dbelova 103 | efedorova 104 | enikanorov 105 | iyozhikov 106 | shakhat 107 | mdurnosvistov 108 | ruhe 109 | smelikyan 110 | skolekonov 111 | skraynev 112 | sreshetniak 113 | tnurlygayanov 114 | tsufiev-x 115 | yorik-sar 116 | teselkin-d 117 | obondarev 118 | akuznetsova 119 | i159 120 | akurilin 121 | pkholkin 122 | ekudryashova 123 | partners: 124 | fuel-partner 125 | eshumakher 126 | izinovik 127 | gcon-monolake 128 | igajsin 129 | moshele 130 | aviramb 131 | srogov 132 | ekorekin 133 | poland: 134 | tzn 135 | salmon 136 | ksambor 137 | prmtl 138 | loles 139 | external: 140 | longgeek 141 | berendt 142 | jesse-pretorius 143 | sammiestoel 144 | other_mirantis: 145 | zynzel 146 | vpleshakov 147 | vdenisov 148 | nikishov-da 149 | atarasov 150 | dtyzhnenko 151 | -------------------------------------------------------------------------------- /launchpad_report/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | untriaged_bug_statuses = [ 6 | 'New', 7 | ] 8 | 9 | open_bug_statuses = [ 10 | 'Incomplete', 'Confirmed', 'Triaged', 'In Progress', 11 | 'Incomplete (with response)', 'Incomplete (without response)', 12 | ] 13 | 14 | open_bug_statuses_for_HCF = [ 15 | 'Confirmed', 'Triaged', 'In Progress', 16 | ] 17 | 18 | rejected_bug_statuses = [ 19 | 'Opinion', 'Invalid', 'Won\'t Fix', 'Expired', 20 | ] 21 | 22 | closed_bug_statuses = [ 23 | 'Fix Committed', 'Fix Released', 24 | ] + rejected_bug_statuses 25 | 26 | all_bug_statuses = ( 27 | untriaged_bug_statuses + open_bug_statuses + closed_bug_statuses 28 | ) 29 | 30 | untriaged_bp_statuses = [ 31 | 'Unknown', 32 | ] 33 | 34 | untriaged_bp_def_statuses = [ 35 | 'New', 36 | ] 37 | 38 | rejected_bp_def_statuses = ['Superseded', 'Obsolete'] 39 | 40 | closed_bp_statuses = ['Implemented'] 41 | 42 | valid_bp_priorities = [ 43 | 'Essential', 'High', 'Medium', 'Low' 44 | ] 45 | 46 | valid_bug_priorities = [ 47 | 'Critical', 'High', 'Medium', 'Low', 'Wishlist' 48 | ] 49 | 50 | 51 | logger = logging.getLogger(__name__) 52 | 53 | cached_names = { 54 | } 55 | 56 | 57 | def is_bug(obj): 58 | return ( 59 | obj.resource_type_link == u'https://api.launchpad.net/devel/#bug_task' 60 | ) 61 | 62 | 63 | def is_bp(obj): 64 | return ( 65 | obj.resource_type_link == 66 | u'https://api.launchpad.net/devel/#specification' 67 | ) 68 | 69 | 70 | def is_project(obj): 71 | return ( 72 | obj.resource_type_link == 73 | u'https://api.launchpad.net/devel/#project' 74 | ) 75 | 76 | 77 | def is_series(obj): 78 | return ( 79 | obj.resource_type_link == 80 | u'https://api.launchpad.net/devel/#project_series' 81 | ) 82 | 83 | 84 | def short_status(obj): 85 | if is_bp(obj): 86 | if obj.definition_status in rejected_bp_def_statuses: 87 | return 'rejected' 88 | if obj.implementation_status in closed_bp_statuses: 89 | return 'done' 90 | if ( 91 | obj.definition_status in untriaged_bp_def_statuses or 92 | obj.assignee is None or 93 | obj.priority not in valid_bp_priorities or 94 | obj.implementation_status in untriaged_bp_statuses 95 | ): 96 | return 'untriaged' 97 | return 'open' 98 | if is_bug(obj): 99 | if obj.status in rejected_bug_statuses: 100 | return 'rejected' 101 | if obj.status in closed_bug_statuses: 102 | return 'done' 103 | if ( 104 | obj.status in untriaged_bug_statuses or 105 | obj.assignee is None or 106 | obj.importance not in valid_bug_priorities 107 | ): 108 | return 'untriaged' 109 | return 'open' 110 | return 'unknown' 111 | 112 | 113 | def get_name(obj): 114 | key = obj._wadl_resource._url 115 | if key not in cached_names: 116 | logger.debug("Miss name: " + key) 117 | cached_names[key] = obj.name 118 | else: 119 | logger.debug("Hit name: " + key) 120 | return cached_names[key] 121 | 122 | 123 | def printn(text): 124 | sys.stdout.write(text) 125 | sys.stdout.flush() 126 | -------------------------------------------------------------------------------- /launchpad_report/leads.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | - fuel 3 | # For debug. Proceed only limited amount of items in project 4 | trunc_report: 0 5 | cache_dir: cache 6 | use_auth: True 7 | teams: 8 | dpyzhov: 9 | fuel-python 10 | alekseyk-ru 11 | dshulyak 12 | rustyrobot 13 | ikalnitsky 14 | nmarkov 15 | aroma-x 16 | akislitsky 17 | kozhukalov 18 | lux-place 19 | ivankliuk 20 | vkramskikh: 21 | fuel-ui 22 | vkramskikh 23 | astepanchuk 24 | bdudko 25 | kpimenova 26 | jkirnosova 27 | vkuklin: 28 | fuel-library 29 | fuel-astute 30 | vsharshov 31 | a-gordeev 32 | xenolog 33 | adidenko 34 | raytrac3r 35 | idv1985 36 | dmy-ilyin 37 | vkuklin 38 | sbogatkin 39 | xdeller 40 | andreika-mail 41 | manashkin: 42 | manashkin 43 | ekozhemyakin 44 | akolesnikov 45 | fsoppelsa 46 | rvyalov: 47 | fuel-osci 48 | sotpuschennikov 49 | dburmistrov 50 | vparakhin 51 | r0mikiam 52 | mrasskazov 53 | dkaiharodsev 54 | aurlapova: 55 | fuel-qa 56 | apalkina 57 | aurlapova 58 | tatyana-leontovich 59 | asledzinskiy 60 | apanchenko-8 61 | ykotko 62 | komelchenko 63 | ddmitriev 64 | dtyzhnenko 65 | ishishkin: 66 | fuel-devops 67 | afedorova 68 | teran 69 | acharykov 70 | skulanov 71 | dborodaenko: 72 | dborodaenko 73 | rmoe 74 | xarses 75 | dreidellhasa 76 | docaedo 77 | fuel-docs 78 | msemenov: 79 | mos-linux 80 | apodrepniy 81 | kdanylov 82 | msemenov 83 | asheplyakov 84 | asyriy 85 | vlos 86 | amogylchenko 87 | imarnat: 88 | mos-neutron 89 | mos-keystone 90 | mos-nova 91 | mos-horizon 92 | mos-ceilometer 93 | mos-oslo 94 | mos-sahara 95 | mos-heat 96 | mos-cinder 97 | mos-murano 98 | mos-glance 99 | degorenko 100 | iberezovskiy 101 | e0ne 102 | dmitrymex 103 | smurashov 104 | rpodolyaka 105 | skolekonov 106 | vrovachev 107 | ylobankov 108 | alexei-kornienko 109 | aepifanov 110 | akamyshnikova 111 | dbelova 112 | efedorova 113 | enikanorov 114 | iyozhikov 115 | shakhat 116 | mdurnosvistov 117 | ruhe 118 | smelikyan 119 | skolekonov 120 | skraynev 121 | sreshetniak 122 | tnurlygayanov 123 | tsufiev-x 124 | yorik-sar 125 | mmaxur 126 | maxmazurenka 127 | teselkin-d 128 | obondarev 129 | akuznetsova 130 | i159 131 | akurilin 132 | pkholkin 133 | ekudryashova 134 | eshumakher: 135 | fuel-partner 136 | eshumakher 137 | izinovik 138 | gcon-monolake 139 | igajsin 140 | moshele 141 | aviramb 142 | srogov 143 | ekorekin 144 | ipovolotskaya 145 | azemlyanov 146 | nuritv 147 | tnapierala: 148 | tzn 149 | salmon 150 | ksambor 151 | prmtl 152 | loles 153 | sgolovatiuk 154 | bogdando 155 | smakar 156 | omolchanov 157 | external: 158 | longgeek 159 | berendt 160 | jesse-pretorius 161 | sammiestoel 162 | other_mirantis: 163 | zynzel 164 | vpleshakov 165 | vdenisov 166 | nikishov-da 167 | atarasov 168 | agasanoff 169 | slava-val-al 170 | gleb-q 171 | dukov 172 | daniele-pizzolli 173 | -------------------------------------------------------------------------------- /launchpad_report/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | lp-report: {{ config.project }} 4 | 23 | 24 | 25 |

lp-report: {{ config.project }}

26 |

Triage actions

27 |

Blueprints

28 | 34 |

Bugs

35 | 41 |

Bugs on teams

42 | 48 | 49 | {% for type_group in rows|groupby('type') %} 50 | {% for status_group in type_group.list|groupby('short_status') %} 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% for item in status_group.list|sort(attribute='milestone') %} 65 | {% if item.triage %} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {% endif %} 77 | {% endfor %} 78 | {% endfor %} 79 | {% endfor %} 80 |
52 | {{ status_group.grouper }} {{ type_group.grouper }}s 53 |
TitleMilestoneStatusPriorityTeamAssigneeFull NameRequired triage actions
{{ item.title }}{{ item.milestone }}{{ item.status }}{{ item.priority }}{{ item.team }}{{ item.assignee }}{{ item.name }}{{ item.triage }}
81 | {% for team_group in team_groups %} 82 |

{{ team_group.grouper }}

83 | 84 | {% for status_group in team_group.list|groupby('short_status') %} 85 | {% if status_group.grouper in ['open', 'untriaged'] %} 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {% for item in status_group.list|sort(attribute='milestone') %} 98 | {% if item.type == "bug" %} 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {% endif %} 110 | {% endfor %} 111 | {% endif %} 112 | {% endfor %} 113 |
{{ status_group.grouper }} {{ team_group.grouper }}
TitleMilestoneStatusPriorityTeamAssigneeFull NameRequired triage actions
{{ item.title }}{{ item.milestone }}{{ item.status }}{{ item.priority }}{{ item.team }}{{ item.assignee }}{{ item.name }}{{ item.triage }}
114 | {% endfor %} 115 | 116 | 117 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import argparse 5 | from launchpad_report.report import Report 6 | 7 | import httplib 8 | import traceback 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | # All this http-related code is an attempt to cache duplicated requests 15 | # Right now it just finds all duplicated requests and send traceback. 16 | 17 | my_cache = {} 18 | 19 | 20 | def my_request(*args, **kwargs): 21 | # self = args[0] 22 | method = args[1] 23 | url = args[2] 24 | if method == 'GET': 25 | if url in my_cache: 26 | logger.debug("Hit " + url) 27 | logger.debug(''.join(traceback.format_stack())) 28 | # return 29 | else: 30 | logger.debug("Miss " + url) 31 | my_cache[url] = 1 32 | # self.my_url = url 33 | # self.my_method = method 34 | return old_httplib_request(*args, **kwargs) 35 | 36 | 37 | class my_resp_obj(dict): 38 | def __init__(self, obj): 39 | self.status = obj.status 40 | self.reason = obj.reason 41 | self.data = obj.read() 42 | self.headers = obj.getheaders() 43 | 44 | def read(self): 45 | return self.data 46 | 47 | def getheaders(self): 48 | return self.headers 49 | 50 | 51 | def my_response(*args, **kwargs): 52 | self = args[0] 53 | method = self.my_method 54 | url = self.my_url 55 | if ( 56 | method == 'GET' and 57 | (url.startswith('/devel/~') or url.startswith('/devel/fuel')) 58 | ): 59 | if url in my_cache: 60 | logger.debug("Hit " + url) 61 | else: 62 | logger.debug("Miss " + url) 63 | my_cache[url] = 1 64 | # my_resp_obj(old_httplib_response(*args, **kwargs)) 65 | # return my_cache[url] 66 | # return old_httplib_response(*args, **kwargs) 67 | 68 | old_httplib_request = httplib.HTTPConnection.request 69 | httplib.HTTPConnection.request = my_request 70 | #old_httplib_response = httplib.HTTPConnection.getresponse 71 | #httplib.HTTPConnection.getresponse = my_response 72 | 73 | # /http_magic 74 | 75 | def main(): 76 | reload(sys) 77 | sys.setdefaultencoding('utf-8') 78 | description = """ 79 | Generate status report for bugs and blueprints in Launchpad project 80 | """ 81 | parser = argparse.ArgumentParser(epilog=description) 82 | parser.add_argument( 83 | '--template', dest='template', action='store', type=str, 84 | help='html template file', 85 | default=os.path.join(os.path.dirname(__file__), 'template.html') 86 | ) 87 | parser.add_argument( 88 | '-c', '--config', dest='config', action='store', type=str, 89 | help='yaml config file', 90 | default=os.path.join(os.path.dirname(__file__), 'config.yaml') 91 | ) 92 | parser.add_argument( 93 | '--outjson', dest='outjson', action='store', type=str, 94 | help='where to output json report', default='report.json' 95 | ) 96 | parser.add_argument( 97 | '--outcsv', dest='outcsv', action='store', type=str, 98 | help='where to output csv report', default='report.csv' 99 | ) 100 | parser.add_argument( 101 | '--outhtml', dest='outhtml', action='store', type=str, 102 | help='where to output html report', default='report.html' 103 | ) 104 | parser.add_argument( 105 | '--load-json', dest='loadjson', action='store', type=str, 106 | help='generate report from previous json report' 107 | ) 108 | parser.add_argument( 109 | '-l', '--logfile', dest='logfile', action='store', type=str, 110 | help='Generate debug logfile' 111 | ) 112 | parser.add_argument( 113 | '-a', '--all', dest='all', action='store_true', default=False, 114 | help='Generate report both for open and closed bugs and blueprints' 115 | ) 116 | params, other_params = parser.parse_known_args() 117 | 118 | report = Report( 119 | config_filename=params.config 120 | ) 121 | 122 | if params.logfile: 123 | logger.setLevel(logging.DEBUG) 124 | file_handler = logging.FileHandler(params.logfile) 125 | logger.addHandler(file_handler) 126 | else: 127 | logger.setLevel(logging.WARNING) 128 | 129 | if params.loadjson: 130 | report.load(params.loadjson) 131 | else: 132 | report.generate(all=params.all) 133 | 134 | report.render2csv(params.outcsv) 135 | report.render2json(params.outjson) 136 | report.render2html(params.outhtml, params.template) 137 | 138 | if __name__ == "__main__": 139 | main() 140 | -------------------------------------------------------------------------------- /lptool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import argparse 5 | from launchpadlib.launchpad import Launchpad 6 | import yaml 7 | 8 | 9 | def bulk(): 10 | bulkfile = open('bulk.yaml') 11 | tasks = yaml.load(bulkfile) 12 | lp = Launchpad.login_with('lp-report-bot', 'production', version='devel') 13 | for prj_name in tasks.keys(): 14 | prj = lp.projects[prj_name] 15 | prj_tasks = tasks[prj_name] 16 | for bp_name in prj_tasks['bp'].keys(): 17 | bp = prj.getSpecification(name=bp_name) 18 | bp_tasks = prj_tasks['bp'][bp_name] 19 | if 'series' in bp_tasks: 20 | bp.proposeGoal(goal=prj.getSeries(name=bp_tasks['series'])) 21 | if 'milestone' in bp_tasks: 22 | if bp_tasks['milestone'] != 'None': 23 | bp.milestone = prj.getMilestone(name=bp_tasks['milestone']) 24 | bp.lp_save() 25 | bp.proposeGoal(goal=bp.milestone.series_target) 26 | else: 27 | bp.milestone = None 28 | bp.lp_save() 29 | if 'approve' in bp_tasks: 30 | pass 31 | 32 | lp = None 33 | prj = None 34 | 35 | 36 | def update_bp(item_id, params): 37 | bp = prj.getSpecification(name=item_id) 38 | if params.milestone: 39 | print("Updating (%s) milestone to (%s)" % ( 40 | item_id, params.milestone 41 | )) 42 | if params.milestone != 'None': 43 | bp.milestone = prj.getMilestone(name=params.milestone) 44 | bp.lp_save() 45 | bp.proposeGoal(goal=bp.milestone.series_target) 46 | else: 47 | bp.milestone = None 48 | bp.lp_save() 49 | if params.series: 50 | print("Updating (%s) series to (%s)" % ( 51 | item_id, params.series 52 | )) 53 | if params.series != 'None': 54 | bp.proposeGoal(goal=prj.getSeries(name=params.series)) 55 | else: 56 | bp.proposeGoal(goal=None) 57 | if params.approved: 58 | print("Approving (%s)" % item_id) 59 | bp.direction_approved = True 60 | bp.definition_status = 'Approved' 61 | bp.lp_save() 62 | if params.create: 63 | print("Creation of blueprints is not implemented") 64 | if params.delete: 65 | print("Removal of blueprints is not available") 66 | if params.priority: 67 | print("TODO") 68 | if params.status: 69 | print("Setting implementation status of (%s) to (%s)" % ( 70 | item_id, params.status 71 | )) 72 | bp.implementation_status = params.status 73 | bp.lp_save() 74 | 75 | 76 | def update_bug(item_id, params): 77 | try: 78 | (bug_id, series_name) = item_id.split(":") 79 | except ValueError: 80 | (bug_id, series_name) = (item_id, None) 81 | bug = lp.bugs[bug_id] 82 | if series_name is None: 83 | series = prj 84 | else: 85 | series = prj.getSeries(name=series_name) 86 | bug_task = filter(lambda x: x.target == series, bug.bug_tasks)[0] 87 | if params.milestone: 88 | print("Updating (%s) milestone to (%s)" % ( 89 | item_id, params.milestone 90 | )) 91 | if params.milestone != 'None': 92 | milestone = prj.getMilestone(name=params.milestone) 93 | is_active = milestone.is_active 94 | if not is_active: 95 | milestone.is_active = True 96 | milestone.lp_save() 97 | bug_task.milestone = prj.getMilestone(name=params.milestone) 98 | bug_task.lp_save() 99 | if not is_active: 100 | milestone.is_active = False 101 | milestone.lp_save() 102 | else: 103 | bug.milestone = None 104 | bug.lp_save() 105 | if params.series: 106 | print("Not suitable for bugs") 107 | if params.approved: 108 | print("Not suitable for bugs") 109 | if params.create: 110 | bug.addTask(target=series) 111 | if params.delete: 112 | bug_task.lp_delete() 113 | if params.priority: 114 | print("TODO") 115 | if params.status: 116 | print("Setting status of (%s) to (%s)" % ( 117 | item_id, params.status 118 | )) 119 | bug.status = params.status 120 | bug.lp_save() 121 | 122 | 123 | def main(): 124 | description = """ 125 | Command line tool to operate with bugs and blueprints 126 | """ 127 | parser = argparse.ArgumentParser(epilog=description) 128 | parser.add_argument('project', type=str) 129 | parser.add_argument('cmd', type=str, choices=['get', 'set']) 130 | parser.add_argument('item_type', type=str, choices=['bp', 'bug']) 131 | parser.add_argument('item_id', type=str, nargs='+') 132 | parser.add_argument('--milestone', type=str) 133 | parser.add_argument('--series', type=str) 134 | parser.add_argument('--approve', dest='approved', action='store_true') 135 | parser.add_argument('--create', action='store_true') 136 | parser.add_argument('--delete', action='store_true') 137 | parser.add_argument('--priority', type=str) 138 | parser.add_argument('--status', type=str) 139 | params, other_params = parser.parse_known_args() 140 | global lp 141 | global prj 142 | lp = Launchpad.login_with('lp-client', 'production', version='devel') 143 | prj = lp.projects[params.project] 144 | if params.cmd == 'set': 145 | for item_id in params.item_id: 146 | if params.item_type == 'bp': 147 | update_bp(item_id, params) 148 | if params.item_type == 'bug': 149 | update_bug(item_id, params) 150 | 151 | 152 | if __name__ == "__main__": 153 | main() 154 | -------------------------------------------------------------------------------- /launchpad_report/checks.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | # from launchpad_report.utils import all_bug_statuses 4 | # from launchpad_report.utils import open_bug_statuses 5 | # from launchpad_report.utils import rejected_bug_statuses 6 | # from launchpad_report.utils import untriaged_bp_def_statuses 7 | from launchpad_report.utils import closed_bp_statuses 8 | from launchpad_report.utils import closed_bug_statuses 9 | from launchpad_report.utils import get_name 10 | from launchpad_report.utils import is_bp 11 | from launchpad_report.utils import is_bug 12 | from launchpad_report.utils import is_series 13 | from launchpad_report.utils import rejected_bp_def_statuses 14 | from launchpad_report.utils import rejected_bug_statuses 15 | from launchpad_report.utils import untriaged_bug_statuses 16 | from launchpad_report.utils import valid_bp_priorities 17 | from launchpad_report.utils import valid_bug_priorities 18 | 19 | 20 | class Checks(object): 21 | def __init__(self, mapping): 22 | self.mapping = mapping 23 | 24 | def run(self, obj, series): 25 | tests = inspect.getmembers( 26 | Checks, 27 | lambda x: inspect.ismethod(x) and 28 | x.__name__.startswith('is_') 29 | ) 30 | actions = [] 31 | for test in tests: 32 | actions.append(getattr(Checks, test[0])(self, obj, series)) 33 | return filter(lambda x: x is not None, actions) 34 | 35 | def is_bp_series_defined(self, obj, series): 36 | if is_bp(obj) and series is None: 37 | return "No series" 38 | 39 | def is_rejected_bp_has_milestone(self, obj, series): 40 | if ( 41 | is_bp(obj) and 42 | obj.definition_status in rejected_bp_def_statuses and 43 | obj.milestone is not None 44 | ): 45 | return ( 46 | "Rejected blueprint has milestone (%s) for series (%s)" % 47 | (obj.milestone.name, series) 48 | ) 49 | 50 | def is_milestone_in_series(self, obj, series): 51 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 52 | return 53 | if obj.milestone is None: 54 | return "No milestone for series (%s)" % series 55 | if series is None: 56 | return # There is another check for missed series 57 | if ( 58 | get_name(obj.milestone) in self.mapping['milestones'] and 59 | self.mapping['milestones'][get_name(obj.milestone)] != series 60 | ): 61 | return ("Wrong milestone (%s) for series (%s)" % ( 62 | get_name(obj.milestone), series)) 63 | 64 | def is_milestone_active(self, obj, series): 65 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 66 | return 67 | if obj.milestone is None: 68 | return 69 | if obj.milestone.is_active: 70 | return 71 | if ( 72 | (is_bug(obj) and obj.status not in closed_bug_statuses) or 73 | ( 74 | is_bp(obj) and 75 | obj.implementation_status not in closed_bp_statuses 76 | ) 77 | ): 78 | return ( 79 | "Open and targeted to closed milestone (%s) on series (%s)" % 80 | (get_name(obj.milestone), series) 81 | ) 82 | 83 | def is_bug_targeted_to_focus_series(self, obj, series): 84 | if ( 85 | is_bug(obj) and 86 | is_series(obj.target) and 87 | get_name(obj.target.project.development_focus) == series 88 | ): 89 | return ( 90 | "Targeted to the current development focus (%s)" % 91 | series) 92 | 93 | def is_priority_set(self, obj, series): 94 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 95 | return 96 | if (is_bug(obj) and obj.status in rejected_bug_statuses): 97 | return 98 | if is_bp(obj) and obj.priority not in valid_bp_priorities: 99 | return "Priority (%s) is not valid for series (%s)" % ( 100 | obj.priority, series) 101 | if is_bug(obj) and obj.importance not in valid_bug_priorities: 102 | return "Priority (%s) is not valid for series (%s)" % ( 103 | obj.importance, series) 104 | 105 | def is_assignee_set(self, obj, series): 106 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 107 | return 108 | if (is_bug(obj) and obj.status in rejected_bug_statuses): 109 | return 110 | if not obj.assignee: 111 | return "No assignee for series (%s)" % series 112 | 113 | def is_bug_confirmed(self, obj, series): 114 | if is_bug(obj) and obj.status in untriaged_bug_statuses: 115 | return "Not confirmed for series (%s)" % series 116 | 117 | def is_bp_in_unknown_status(self, obj, series): 118 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 119 | return 120 | if ( 121 | is_bp(obj) and 122 | obj.implementation_status == 'Unknown' 123 | ): 124 | return "Status unknown for series (%s)" % series 125 | 126 | def is_bp_done_but_unapproved(self, obj, series): 127 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 128 | return 129 | if ( 130 | is_bp(obj) and 131 | obj.implementation_status in closed_bp_statuses 132 | ): 133 | if ( 134 | obj.definition_status != 'Approved' or 135 | obj.direction_approved is not True 136 | ): 137 | return "Implemented, but not approved for series (%s)" % series 138 | 139 | def is_bp_semiapproved(self, obj, series): 140 | if (is_bp(obj) and obj.definition_status in rejected_bp_def_statuses): 141 | return 142 | if ( 143 | is_bp(obj) and 144 | obj.definition_status == 'Approved' and 145 | obj.direction_approved is not True 146 | ): 147 | return ( 148 | "Definition is approved, but direction is not for series (%s)" 149 | % series 150 | ) 151 | -------------------------------------------------------------------------------- /launchpad_report/report.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import json 4 | 5 | from launchpadlib.launchpad import Launchpad 6 | import yaml 7 | 8 | from launchpad_report.checks import Checks 9 | from launchpad_report.render import CSVRenderer 10 | from launchpad_report.render import HTMLRenderer 11 | from launchpad_report.render import JSONRenderer 12 | from launchpad_report.utils import all_bug_statuses 13 | from launchpad_report.utils import get_name 14 | from launchpad_report.utils import is_series 15 | from launchpad_report.utils import open_bug_statuses 16 | from launchpad_report.utils import open_bug_statuses_for_HCF 17 | from launchpad_report.utils import printn 18 | from launchpad_report.utils import short_status 19 | from launchpad_report.utils import untriaged_bug_statuses 20 | 21 | 22 | class ConfigError(Exception): 23 | pass 24 | 25 | 26 | class Report(object): 27 | def __init__(self, config_filename): 28 | with open(config_filename, "r") as f: 29 | self.config = yaml.load(f.read()) 30 | 31 | self.teams = self.config['teams'] 32 | self.trunc = self.config['trunc_report'] 33 | 34 | cache_dir = self.config['cache_dir'] 35 | 36 | if self.config['use_auth']: 37 | lp = Launchpad.login_with( 38 | 'lp-report-bot', 'production', 39 | cache_dir, version='devel' 40 | ) 41 | else: 42 | lp = Launchpad.login_anonymously( 43 | 'lp-report-bot', 'production', version='devel' 44 | ) 45 | #import pdb; pdb.set_trace() 46 | self.projects = [lp.projects[prj] for prj in self.config['project']] 47 | 48 | # for backward compatibility 49 | #self.project = lp.projects[self.config['project'][0]] 50 | 51 | self.blueprint_series = {} 52 | 53 | def render2html(self, filename, template_filename): 54 | HTMLRenderer(filename, template_filename).render(self.data) 55 | 56 | def render2csv(self, filename): 57 | CSVRenderer(filename).render(self.data) 58 | 59 | def render2json(self, filename): 60 | JSONRenderer(filename).render(self.data) 61 | 62 | def load(self, filename): 63 | jsonfile = open(filename) 64 | self.data = json.load(jsonfile) 65 | 66 | def generate(self, all=False): 67 | self.data = {'rows': []} 68 | self.bug_issues = {} 69 | self.data['config'] = self.config 70 | for project in self.projects: 71 | self.checks = Checks(self.iter_series(project)) 72 | #self.data['rows'] += self.bp_report(all=all) 73 | self.data['rows'] += self.bug_report(project, all=all) 74 | 75 | def iter_series(self, project): 76 | print("Collecting series data for %s:" % project) 77 | self.bps_series = {} 78 | milestones_series = {} 79 | for series in project.series: 80 | printn(" %s" % get_name(series)) 81 | # Blueprints 82 | #for (counter, bp) in enumerate(series.all_specifications): 83 | #self.bps_series[get_name(bp)] = get_name(series) 84 | # Milestones 85 | for milestone in series.all_milestones: 86 | milestones_series[get_name(milestone)] = get_name(series) 87 | printn(" none") 88 | # Search for blueprints without series 89 | #for (counter, bp) in enumerate(self.project.all_specifications): 90 | #self.bps_series.setdefault(get_name(bp), None) 91 | print() 92 | return { 93 | 'milestones': milestones_series, 94 | } 95 | 96 | def bp_report(self, all=False): 97 | report = [] 98 | if all: 99 | blueprints = self.project.all_specifications 100 | else: 101 | blueprints = self.project.valid_specifications 102 | printn("Processing blueprints (%d):" % len(blueprints)) 103 | for (counter, bp) in enumerate(blueprints, 1): 104 | if counter > self.trunc and self.trunc > 0: 105 | break 106 | if counter % 200 == 10: 107 | print() 108 | if counter % 10 == 0: 109 | printn("%4d" % counter) 110 | assignee = 'unassigned' 111 | assignee_name = 'unassigned' 112 | try: 113 | assignee = get_name(bp.assignee) 114 | assignee_name = bp.assignee.display_name 115 | except Exception: 116 | pass 117 | if bp.milestone: 118 | milestone = get_name(bp.milestone) 119 | else: 120 | milestone = 'None' 121 | team = 'unknown' 122 | for t in self.teams.keys(): 123 | if assignee in self.teams[t]: 124 | team = t 125 | triage = self.checks.run(bp, self.bps_series[get_name(bp)]) 126 | report.append({ 127 | 'type': 'bp', 128 | 'link': bp.web_link.encode('utf-8'), 129 | 'id': bp.web_link[ 130 | bp.web_link.rfind('/') + 1: 131 | ].encode('utf-8'), 132 | 'title': bp.title.encode('utf-8'), 133 | 'milestone': milestone, 134 | 'series': self.bps_series[get_name(bp)], 135 | 'status': bp.implementation_status, 136 | 'short_status': short_status(bp), 137 | 'priority': bp.priority, 138 | 'team': team.encode('utf-8'), 139 | 'assignee': assignee.encode('utf-8'), 140 | 'name': assignee_name.encode('utf-8'), 141 | 'triage': ', '.join(triage).encode('utf-8') 142 | }) 143 | print() 144 | return report 145 | 146 | def bug_report(self, project, all=False): 147 | report = [] 148 | milestone51 = project.getMilestone(name="6.0") # 5.1 149 | milestone502 = project.getMilestone(name="5.0.3") # 5.0.2 150 | if all: 151 | bugs = project.searchTasks(status=all_bug_statuses) 152 | else: 153 | if self.config.get('hcf'): 154 | bugs51 = project.searchTasks(status=( 155 | open_bug_statuses_for_HCF), milestone=milestone51, 156 | importance=["Critical", "High"], 157 | # We would ideally filter our system-tests tag, 158 | # however I saw bugs which were just found during 159 | # sytem-tests run which are being real bugs in Fuel 160 | tags=["-docs", "-devops", "-fuel-devops", "-experimental"], 161 | tags_combinator="All") 162 | bugs502 = project.searchTasks(status=( 163 | open_bug_statuses_for_HCF), milestone=milestone502, 164 | importance=["Critical", "High"], 165 | tags=["-docs", "-devops", "-fuel-devops", "-experimental"], 166 | tags_combinator="All") 167 | else: 168 | bugs51 = project.searchTasks(status=( 169 | untriaged_bug_statuses + open_bug_statuses), milestone=milestone51) 170 | bugs502 = project.searchTasks(status=( 171 | untriaged_bug_statuses + open_bug_statuses), milestone=milestone502) 172 | #printn("Processing bugs (%d):" % (len(bugs51) + len(bugs502))) 173 | 174 | for bugs in (bugs51, bugs502): 175 | for (counter, bug) in enumerate(bugs, 1): 176 | if counter > self.trunc and self.trunc > 0: 177 | break 178 | if counter % 200 == 10: 179 | print() 180 | if counter % 10 == 0: 181 | printn("%4d" % counter) 182 | 183 | assignee = 'unassigned' 184 | assignee_name = 'unassigned' 185 | try: 186 | assignee = get_name(bug.assignee) 187 | # We want to exclude all from QA & 188 | # fuel-devops & docs for HCF calcs 189 | if self.config.get('excludes') and assignee in self.config['excludes']: 190 | continue 191 | assignee_name = bug.assignee.display_name 192 | except Exception: 193 | pass 194 | if bug.milestone: 195 | milestone = get_name(bug.milestone) 196 | else: 197 | milestone = 'None' 198 | team = 'unknown' 199 | self.bug_issues.setdefault(bug.bug.web_link, []) 200 | for t in self.teams.keys(): 201 | if assignee in self.teams[t]: 202 | team = t 203 | title = bug.bug.title 204 | triage = [] 205 | for task in bug.bug.bug_tasks: 206 | series = task.target 207 | if is_series(series): 208 | series = get_name(series) 209 | if task.target.project != project: 210 | continue 211 | else: 212 | series = None 213 | if task.target != project: 214 | continue 215 | triage += self.checks.run(task, series) 216 | report.append({ 217 | 'type': 'bug', 218 | 'link': bug.web_link.encode('utf-8'), 219 | 'id': bug.web_link[ 220 | bug.web_link.rfind('/') + 1: 221 | ].encode('utf-8'), 222 | 'title': title.encode('utf-8'), 223 | 'milestone': milestone, 224 | 'status': bug.status, 225 | 'short_status': short_status(bug), 226 | 'priority': bug.importance, 227 | 'team': team.encode('utf-8'), 228 | 'assignee': assignee.encode('utf-8'), 229 | 'name': assignee_name.encode('utf-8'), 230 | 'triage': ', '.join(triage).encode('utf-8'), 231 | }) 232 | print() 233 | return report 234 | --------------------------------------------------------------------------------