Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/cdn.py: 50%

60 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7import json 

8import os 

9from typing import TYPE_CHECKING, ClassVar 

10from uuid import uuid4 

11 

12from django.conf import settings 

13from django.template.loader import render_to_string 

14from django.utils.functional import cached_property 

15from django.utils.translation import gettext_lazy 

16 

17from weblate.addons.base import BaseAddon 

18from weblate.addons.events import AddonEvent 

19from weblate.addons.forms import CDNJSForm 

20from weblate.addons.tasks import cdn_parse_html 

21from weblate.utils.state import STATE_TRANSLATED 

22 

23if TYPE_CHECKING: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true

24 from weblate.addons.models import Addon 

25 from weblate.auth.models import User 

26 from weblate.trans.models import Component, Project 

27 

28 

29class CDNJSAddon(BaseAddon): 

30 events: ClassVar[set[AddonEvent]] = { 

31 AddonEvent.EVENT_DAILY, 

32 AddonEvent.EVENT_POST_COMMIT, 

33 AddonEvent.EVENT_POST_UPDATE, 

34 } 

35 name = "weblate.cdn.cdnjs" 

36 verbose = gettext_lazy("JavaScript localization CDN") 

37 description = gettext_lazy( 

38 "Publishes translations into content delivery network " 

39 "for use in JavaScript or HTML localization." 

40 ) 

41 user_name = "cdn" 

42 user_verbose = "CDN add-on" 

43 

44 settings_form = CDNJSForm 

45 icon = "cloud-upload.svg" 

46 stay_on_create = True 

47 

48 @classmethod 

49 def create_object( 

50 cls, 

51 *, 

52 component: Component | None = None, 

53 project: Project | None = None, 

54 acting_user: User | None = None, 

55 **kwargs, 

56 ) -> Addon: 

57 # Generate UUID for the CDN 

58 if "state" not in kwargs: 

59 kwargs["state"] = {"uuid": uuid4().hex} 

60 return super().create_object( 

61 component=component, project=project, acting_user=acting_user, **kwargs 

62 ) 

63 

64 @classmethod 

65 def can_install(cls, component: Component, user: User | None) -> bool: 

66 if ( 

67 not settings.LOCALIZE_CDN_URL 

68 or not settings.LOCALIZE_CDN_PATH 

69 or not component.has_template() 

70 or not component.translation_set.exists() 

71 ): 

72 return False 

73 return super().can_install(component, user) 

74 

75 def cdn_path(self, filename: str) -> str: 

76 return os.path.join( 

77 settings.LOCALIZE_CDN_PATH, self.instance.state["uuid"], filename 

78 ) 

79 

80 @cached_property 

81 def cdn_js_url(self) -> str: 

82 return os.path.join( 

83 settings.LOCALIZE_CDN_URL, self.instance.state["uuid"], "weblate.js" 

84 ) 

85 

86 def post_commit( 

87 self, component: Component, store_hash: bool, activity_log_id: int | None = None 

88 ) -> None: 

89 # Get list of applicable translations 

90 threshold = self.instance.configuration["threshold"] 

91 translations = [ 

92 translation 

93 for translation in component.translation_set.all() 

94 if (not translation.is_source or component.intermediate) 

95 and translation.stats.translated > threshold 

96 ] 

97 

98 # Create output directory 

99 dirname = self.cdn_path("") 

100 if not os.path.exists(dirname): 

101 os.makedirs(dirname) 

102 

103 # Generate JavasScript loader 

104 with open(self.cdn_path("weblate.js"), "w") as handle: 

105 # The languages variable is included inside quotes to make 

106 # sure the template is valid JavaScript code as well 

107 handle.write( 

108 render_to_string( 

109 "addons/js/weblate.js.template", 

110 { 

111 "languages": sorted( 

112 translation.language.code for translation in translations 

113 ), 

114 "url": os.path.join( 

115 settings.LOCALIZE_CDN_URL, 

116 self.instance.state["uuid"], 

117 ), 

118 "cookie_name": self.instance.configuration["cookie_name"], 

119 "css_selector": self.instance.configuration["css_selector"], 

120 }, 

121 ) 

122 ) 

123 

124 # Generate bilingual JSON files 

125 for translation in translations: 

126 with open( 

127 self.cdn_path(f"{translation.language.code}.json"), "w" 

128 ) as handle: 

129 json.dump( 

130 { 

131 unit.source: unit.target 

132 for unit in translation.unit_set.filter( 

133 state__gte=STATE_TRANSLATED 

134 ) 

135 }, 

136 handle, 

137 ) 

138 

139 def daily(self, component: Component, activity_log_id: int | None = None) -> None: 

140 if not self.instance.configuration["files"].strip(): 

141 return 

142 # Trigger parsing files 

143 cdn_parse_html.delay(self.instance.id, component.id) 

144 

145 def post_update( 

146 self, 

147 component: Component, 

148 previous_head: str, 

149 skip_push: bool, 

150 activity_log_id: int | None = None, 

151 ) -> None: 

152 self.daily(component)