Coverage for webapp/template_utils.py: 96%

82 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-15 22:06 +0000

1# Core 

2import hashlib 

3import os 

4from dateutil import parser 

5from markupsafe import Markup 

6from emoji import replace_emoji 

7 

8from webapp.vite_integration import ViteIntegration 

9 

10 

11# generator functions for templates 

12def generate_slug(path): 

13 """ 

14 Generate a slug for each page 

15 """ 

16 if path.endswith( 

17 ( 

18 "/snaps", 

19 "/listing", 

20 "/releases", 

21 "/metrics", 

22 "/publicise", 

23 "/publicise/badges", 

24 "/publicise/cards", 

25 "/settings", 

26 "/account/details", 

27 ) 

28 ): 

29 return "account" 

30 

31 if path == "/" or path.startswith("/first-snap"): 

32 return "home" 

33 

34 if path.startswith("/build"): 

35 return "build" 

36 

37 if path.startswith("/blog"): 

38 return "blog" 

39 

40 if path.startswith("/iot"): 

41 return "iot" 

42 

43 if path.startswith("/docs/snap-tutorials"): 

44 return "tutorials" 

45 

46 if path.startswith("/docs"): 

47 return "docs" 

48 

49 return "store" 

50 

51 

52# template filters 

53def contains(arr, contents): 

54 """ 

55 Template helper for detecting if an array contains an item 

56 """ 

57 

58 return contents in arr 

59 

60 

61def join(arr, separator=""): 

62 """ 

63 Template helper for joining array items into a string, using a separator 

64 """ 

65 

66 return separator.join(arr) 

67 

68 

69def static_url(filename): 

70 """ 

71 Template function for generating URLs to static assets: 

72 Given the path for a static file, output a url path 

73 with a hex hash as a query string for versioning 

74 """ 

75 

76 filepath = os.path.join("static", filename) 

77 url = "/" + filepath 

78 

79 if not os.path.isfile(filepath): 

80 # Could not find static file 

81 return url 

82 

83 # Use MD5 as we care about speed a lot 

84 # and not security in this case 

85 file_hash = hashlib.md5() 

86 with open(filepath, "rb") as file_contents: 

87 for chunk in iter(lambda: file_contents.read(4096), b""): 

88 file_hash.update(chunk) 

89 

90 return url + "?v=" + file_hash.hexdigest()[:7] 

91 

92 

93def vite_import(entrypoint: str): 

94 """ 

95 Template function that takes a .js/.ts source file as an argument and 

96 returns the <script> tags with the correct src URL based on Vite's 

97 output (a localhost URL in dev mode, or a static url in prod mode) 

98 """ 

99 entry_url = ViteIntegration.get_asset_url(entrypoint) 

100 is_css = entry_url.endswith((".css")) 

101 

102 if is_css: 

103 return Markup(f'<link rel="stylesheet" href="{entry_url}" />') 

104 

105 entry_script = f'<script type="module" src="{entry_url}"></script>' 

106 

107 chunks_urls = ViteIntegration.get_imported_chunks(entrypoint) 

108 chunks_scripts = [ 

109 f'<link rel="modulepreload" href="{c}" />' for c in chunks_urls 

110 ] 

111 css_urls = ViteIntegration.get_imported_css(entrypoint) 

112 css_scripts = [f'<link rel="stylesheet" href="{c}" />' for c in css_urls] 

113 

114 return Markup(entry_script + "".join(chunks_scripts + css_scripts)) 

115 

116 

117def vite_dev_tools(): 

118 """ 

119 Template function that returns <script> tags for Vite's dev server 

120 integration (or an empty string in prod mode) 

121 """ 

122 return Markup(ViteIntegration.get_dev_tools()) 

123 

124 

125def install_snippet( 

126 package_name, default_track, lowest_risk_available, confinement 

127): 

128 """ 

129 Template function that returns the snippet value to 

130 install a snap to be used in distro pages and/or snap 

131 detail pages 

132 """ 

133 

134 snippet_value = "sudo snap install " + package_name 

135 

136 if lowest_risk_available != "stable": 

137 snippet_value += f" --{lowest_risk_available}" 

138 

139 if confinement == "classic": 

140 snippet_value += " --classic" 

141 

142 return snippet_value 

143 

144 

145def format_number(number: int): 

146 """ 

147 Template function that transforms a int into a string 

148 with a comma between every thousands 

149 """ 

150 return "{:,}".format(number) 

151 

152 

153def format_display_name(display_name): 

154 """Template function that formats the displayed name 

155 primarily to remove emoji 

156 """ 

157 return replace_emoji(display_name, replace="") 

158 

159 

160def display_name(display_name, username): 

161 """Template function that returns the displayed name if the username 

162 is the same, or the dispayed name and the username if differents 

163 """ 

164 display_name = format_display_name(display_name) 

165 if display_name.lower() == username.lower(): 

166 return display_name 

167 else: 

168 return f"{display_name} ({username})" 

169 

170 

171def format_date(timestamp, format): 

172 """Template function that returns a formatted date 

173 based on the given timestamp 

174 """ 

175 datestring = parser.parse(timestamp) 

176 

177 return datestring.strftime(format) 

178 

179 

180def format_member_role(role): 

181 """Template function that returns the 

182 correct label for a members role 

183 """ 

184 roles = { 

185 "admin": "admin", 

186 "review": "reviewer", 

187 "view": "viewer", 

188 "access": "publisher", 

189 } 

190 

191 return roles[role] 

192 

193 

194def format_link(url): 

195 """ 

196 Template function that removes protocol, path and query string from links 

197 """ 

198 url_parts = url.split(":") 

199 

200 if url_parts[0] == "mailto": 

201 return url_parts[1] 

202 

203 if url_parts[0] == "http" or url_parts[0] == "https": 

204 url_parts_no_slashes = url_parts[1].split("//")[1] 

205 url_parts_no_query = url_parts_no_slashes.split("?")[0] 

206 url_parts_no_path = url_parts_no_query.split("/")[0] 

207 

208 if url_parts_no_path in [ 

209 "github.com", 

210 "gitlab.com", 

211 "bitbucket.org", 

212 "launchpad.net", 

213 "sourceforge.net", 

214 ]: 

215 return url_parts_no_query 

216 

217 return url_parts_no_path