Coverage for app/venv/lib/python3.14/site-packages/weblate/fonts/models.py: 69%

75 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 django.conf import settings 

6from django.core.exceptions import ValidationError 

7from django.core.files.storage import FileSystemStorage 

8from django.db import models 

9from django.urls import reverse 

10from django.utils.translation import gettext_lazy 

11 

12from weblate.fonts.utils import get_font_name 

13from weblate.fonts.validators import validate_font 

14from weblate.lang.models import Language 

15from weblate.trans.mixins import UserDisplayMixin 

16from weblate.trans.models import Project 

17from weblate.utils.data import data_dir 

18 

19FONT_STORAGE = FileSystemStorage(location=data_dir("fonts")) 

20 

21 

22class Font(models.Model, UserDisplayMixin): 

23 family = models.CharField( 

24 verbose_name=gettext_lazy("Font family"), 

25 max_length=100, 

26 blank=True, 

27 db_index=False, 

28 ) 

29 style = models.CharField( 

30 verbose_name=gettext_lazy("Font style"), max_length=100, blank=True 

31 ) 

32 font = models.FileField( 

33 verbose_name=gettext_lazy("Font file"), 

34 validators=[validate_font], 

35 storage=FONT_STORAGE, 

36 help_text=gettext_lazy("OpenType and TrueType fonts are supported."), 

37 ) 

38 project = models.ForeignKey(Project, on_delete=models.deletion.CASCADE) 

39 timestamp = models.DateTimeField(auto_now_add=True) 

40 user = models.ForeignKey( 

41 settings.AUTH_USER_MODEL, 

42 null=True, 

43 blank=True, 

44 on_delete=models.deletion.SET_NULL, 

45 ) 

46 

47 class Meta: 

48 unique_together = [ # noqa: RUF012 

49 ("family", "style", "project") 

50 ] 

51 verbose_name = "Font" 

52 verbose_name_plural = "Fonts" 

53 

54 def __str__(self) -> str: 

55 return f"{self.family} {self.style}" 

56 

57 def __init__(self, *args, **kwargs) -> None: 

58 super().__init__(*args, **kwargs) 

59 self.field_errors: dict[str, list[ValidationError]] = {} 

60 

61 def save( 

62 self, force_insert=False, force_update=False, using=None, update_fields=None 

63 ) -> None: 

64 from weblate.fonts.tasks import update_fonts_cache 

65 

66 self.clean() 

67 super().save( 

68 force_insert=force_insert, 

69 force_update=force_update, 

70 using=using, 

71 update_fields=update_fields, 

72 ) 

73 update_fonts_cache.delay() 

74 

75 def get_absolute_url(self) -> str: 

76 return reverse("font", kwargs={"pk": self.pk, "project": self.project.slug}) 

77 

78 def clean_fields(self, exclude=None) -> None: 

79 self.field_errors = {} 

80 try: 

81 super().clean_fields(exclude) 

82 except ValidationError as error: 

83 self.field_errors = error.error_dict 

84 raise 

85 

86 def clean(self) -> None: 

87 # Try to parse file only if it passed validation 

88 if self.font and "font" not in self.field_errors and not self.family: 

89 self.family, self.style = get_font_name(self.font) 

90 

91 def get_usage(self): 

92 related = FontGroup.objects.filter( 

93 models.Q(font=self) | models.Q(fontoverride__font=self) 

94 ) 

95 return related.order().distinct() 

96 

97 

98class FontGroupQuerySet(models.QuerySet): 

99 def order(self): 

100 return self.order_by("name") 

101 

102 

103class FontGroup(models.Model): 

104 name = models.SlugField( 

105 verbose_name=gettext_lazy("Font group name"), 

106 max_length=100, 

107 help_text=gettext_lazy( 

108 "Identifier you will use in checks to select this font group. " 

109 "Avoid whitespaces and special characters." 

110 ), 

111 ) 

112 font = models.ForeignKey( 

113 Font, 

114 verbose_name=gettext_lazy("Default font"), 

115 on_delete=models.deletion.CASCADE, 

116 help_text=gettext_lazy( 

117 "Default font is used unless per language override matches." 

118 ), 

119 ) 

120 project = models.ForeignKey( 

121 Project, on_delete=models.deletion.CASCADE, db_index=False 

122 ) 

123 

124 objects = FontGroupQuerySet.as_manager() 

125 

126 class Meta: 

127 unique_together = [ # noqa: RUF012 

128 ("project", "name") 

129 ] 

130 verbose_name = "Font group" 

131 verbose_name_plural = "Font groups" 

132 

133 def __str__(self) -> str: 

134 return self.name 

135 

136 def get_absolute_url(self) -> str: 

137 return reverse( 

138 "font_group", kwargs={"pk": self.pk, "project": self.project.slug} 

139 ) 

140 

141 

142class FontOverride(models.Model): 

143 group = models.ForeignKey( 

144 FontGroup, on_delete=models.deletion.CASCADE, db_index=False 

145 ) 

146 font = models.ForeignKey( 

147 Font, on_delete=models.deletion.CASCADE, verbose_name=gettext_lazy("Font") 

148 ) 

149 language = models.ForeignKey( 

150 Language, 

151 on_delete=models.deletion.CASCADE, 

152 verbose_name=gettext_lazy("Language"), 

153 ) 

154 

155 class Meta: 

156 unique_together = [ # noqa: RUF012 

157 ("group", "language") 

158 ] 

159 verbose_name = "Font override" 

160 verbose_name_plural = "Font overrides" 

161 

162 def __str__(self) -> str: 

163 return f"{self.group}:{self.font}:{self.language}"