OpenStack Technical Committee Decisions
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

badges.py 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  2. # not use this file except in compliance with the License. You may obtain
  3. # a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. # License for the specific language governing permissions and limitations
  11. # under the License.
  12. """
  13. Generate badges for the projects
  14. """
  15. import os
  16. from itertools import chain
  17. from itertools import zip_longest
  18. from PIL import ImageFont
  19. from sphinx.util import logging
  20. import projects
  21. LOG = logging.getLogger(__name__)
  22. PADDING = 8
  23. BASE_TAGS_URL = 'https://governance.openstack.org/tc/reference/tags/'
  24. COLOR_SCHEME = {
  25. "brightgreen": "#4c1",
  26. "green": "#97CA00",
  27. "yellow": "#dfb317",
  28. "yellowgreen": "#a4a61d",
  29. "orange": "#fe7d37",
  30. "red": "#e05d44",
  31. "blue": "#007ec6",
  32. "grey": "#555",
  33. "lightgrey": "#9f9f9f",
  34. }
  35. SVG_ROOT = """<?xml version="1.0" standalone="no"?>
  36. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  37. "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  38. <svg contentScriptType="text/ecmascript" zoomAndPan="magnify" contentStyleType="text/css"
  39. height="{height}" width="{width}" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"
  40. version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
  41. <title id="os:gov:badges:title">
  42. This is a container for a set of OpenStack badges indicating the status and
  43. features of this project and its repository
  44. </title>
  45. {svg}
  46. </svg>
  47. """
  48. FLAT_BADGE_TEMPLATE = """<svg id="{left_text}:{right_text}" width="{width}" height="20" x="{svg_x}" y="{svg_y}">
  49. <title>{left_text}:{right_text}</title>
  50. <a target="_blank" xlink:href="{link}">
  51. <linearGradient id="smooth:{left_text}:{right_text}" x2="0" y2="100%">
  52. <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
  53. <stop offset="1" stop-opacity=".1"/>
  54. </linearGradient>
  55. <mask id="round:{left_text}:{right_text}">
  56. <rect width="{width}" height="20" rx="3" fill="#fff"/>
  57. </mask>
  58. <g mask="url(#round:{left_text}:{right_text})">
  59. <rect width="{left_width}" height="20" fill="#555"/>
  60. <rect x="{left_width}" width="{right_width}" height="20" fill="{color}"/>
  61. <rect width="{width}" height="20" fill="url(#smooth:{left_text}:{right_text})"/>
  62. </g>
  63. <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
  64. <text x="{left_x}" y="15" fill="#010101" fill-opacity=".3">{left_text}</text>
  65. <text x="{left_x}" y="14">{left_text}</text>
  66. <text x="{right_x}" y="15" fill="#010101" fill-opacity=".3">{right_text}</text>
  67. <text x="{right_x}" y="14">{right_text}</text>
  68. </g>
  69. </a>
  70. </svg>
  71. """
  72. def _generate_badge(left_text, right_text, link=None, colorscheme='brightgreen'):
  73. font = ImageFont.truetype('DejaVuSans.ttf', 11)
  74. left_width = font.getsize(left_text)[0] + PADDING
  75. right_width = font.getsize(right_text)[0] + PADDING
  76. width = left_width + right_width
  77. data = {
  78. 'link': link or '',
  79. 'svg_x': 0,
  80. 'svg_y': 0,
  81. 'color': COLOR_SCHEME[colorscheme],
  82. 'width': width,
  83. 'left_width': left_width,
  84. 'left_text': left_text,
  85. 'left_x': left_width / 2,
  86. 'right_width': right_width,
  87. 'right_text': right_text,
  88. 'right_x': left_width + right_width / 2 - 1,
  89. }
  90. return data
  91. def _generate_tag_badges(tags):
  92. badges = []
  93. badges.append(_generate_badge('project', 'official'))
  94. for tag in tags:
  95. # NOTE(flaper87): will submit other patches to make these
  96. # tags consistent with the rest.
  97. if tag in ['starter-kit:compute', 'tc-approved-release']:
  98. group, tname = 'tc', tag
  99. else:
  100. group, tname = tag.split(':')
  101. link = BASE_TAGS_URL + '%s.html' % tag.replace(':', '_')
  102. badges.append(_generate_badge(group, tname, link, colorscheme='blue'))
  103. return badges
  104. def _organize_badges(badges):
  105. sbadges = sorted(badges, key=lambda badge: badge['width'])
  106. # NOTE(flaper87): 4 is the number of columns
  107. ziped = list(zip_longest(*(iter(sbadges),) * 4))
  108. result = []
  109. for y, group in enumerate(ziped):
  110. result.append([])
  111. col_x = 0
  112. for x, badge in enumerate(group):
  113. # NOTE(flaper87): zip_longest fills the
  114. # empty slots with None. We don't care about
  115. # those.
  116. if badge is None:
  117. break
  118. width_badge = ziped[-1][x]
  119. if width_badge is None:
  120. if len(ziped) > 1:
  121. width_badge = ziped[-2][x]
  122. else:
  123. width_badge = badge
  124. badge['height'] = 20
  125. badge['svg_y'] = (20 + 4) * y
  126. # NOTE(flaper87): 3 is just an extra padding in case there are two badges
  127. # with the same width in the same row
  128. badge['svg_x'] = col_x
  129. col_x += width_badge['width'] + 3
  130. result[y].append(badge)
  131. return result
  132. def _to_svg(badges):
  133. for badge in badges:
  134. yield FLAT_BADGE_TEMPLATE.format(**badge)
  135. def _generate_teams_badges(app, exception=None):
  136. LOG.info('Generating team badges')
  137. all_teams = projects.get_project_data()
  138. files = []
  139. badges_dir = os.path.join(app.outdir, 'badges')
  140. if not os.path.exists(badges_dir):
  141. os.mkdir(badges_dir)
  142. filename = os.path.join(badges_dir, 'project-unofficial.svg')
  143. svg_data = _generate_badge('project', 'unofficial', colorscheme='red')
  144. svg = FLAT_BADGE_TEMPLATE.format(**svg_data)
  145. with open(filename, 'w', encoding='utf-8') as f:
  146. f.write(SVG_ROOT.format(height=20, width=106, svg=svg))
  147. files.append(filename)
  148. for team, info in all_teams.items():
  149. LOG.info('generating team badge for %s' % team)
  150. for name, deliverable in info['deliverables'].items():
  151. tags = info.get('tags', []) + deliverable.get('tags', [])
  152. badges = _organize_badges(_generate_tag_badges(tags))
  153. svg = '\n'.join(_to_svg(chain(*badges)))
  154. root_width = max([bdg_row[-1]['width'] + bdg_row[-1]['svg_x']
  155. for bdg_row in badges])
  156. root_height = badges[-1][0]['svg_y'] + badges[-1][0]['height']
  157. for repo in deliverable.get('repos', []):
  158. repo_name = repo.split('/')[1]
  159. filename = os.path.join(badges_dir, '%s.svg' % projects.slugify(repo_name))
  160. with open(filename, 'w') as f:
  161. f.write(SVG_ROOT.format(height=root_height, width=root_width, svg=svg))
  162. files.append(filename)
  163. return files
  164. def setup(app):
  165. LOG.info('loading badges extension')
  166. app.connect('build-finished', _generate_teams_badges)