Coverage for /home/marco/Code/django-simple-captcha/captcha/fields.py: 89.15%

107 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-15 09:22 +0100

1from django.core.exceptions import ImproperlyConfigured 

2from django.forms import ValidationError 

3from django.forms.fields import CharField, MultiValueField 

4from django.forms.widgets import HiddenInput, MultiWidget, TextInput 

5from django.urls import NoReverseMatch, reverse 

6from django.utils import timezone 

7from django.utils.translation import gettext_lazy 

8 

9from captcha.conf import settings 

10from captcha.models import CaptchaStore 

11 

12 

13class CaptchaHiddenInput(HiddenInput): 

14 """Hidden input for the captcha key.""" 

15 

16 # Use *args and **kwargs because signature changed in Django 1.11 

17 def build_attrs(self, *args, **kwargs): 

18 """Disable autocomplete to prevent problems on page reload.""" 

19 

20 attrs = super().build_attrs(*args, **kwargs) 

21 attrs["autocomplete"] = "off" 

22 return attrs 

23 

24 

25class CaptchaAnswerInput(TextInput): 

26 """Text input for captcha answer.""" 

27 

28 # Use *args and **kwargs because signature changed in Django 1.11 

29 def build_attrs(self, *args, **kwargs): 

30 """Disable automatic corrections and completions.""" 

31 attrs = super().build_attrs(*args, **kwargs) 

32 attrs["autocapitalize"] = "off" 

33 attrs["autocomplete"] = "off" 

34 attrs["autocorrect"] = "off" 

35 attrs["spellcheck"] = "false" 

36 return attrs 

37 

38 

39class BaseCaptchaTextInput(MultiWidget): 

40 """ 

41 Base class for Captcha widgets 

42 """ 

43 

44 def __init__(self, attrs=None): 

45 widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs)) 

46 super().__init__(widgets, attrs) 

47 

48 def decompress(self, value): 

49 if value: 

50 return value.split(",") 

51 return [None, None] 

52 

53 def fetch_captcha_store(self, name, value, attrs=None, generator=None): 

54 """ 

55 Fetches a new CaptchaStore 

56 This has to be called inside render 

57 """ 

58 try: 

59 reverse("captcha-image", args=("dummy",)) 

60 except NoReverseMatch: 

61 raise ImproperlyConfigured( 

62 "Make sure you've included captcha.urls as explained in the INSTALLATION section on http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation" 

63 ) 

64 

65 if settings.CAPTCHA_GET_FROM_POOL: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 key = CaptchaStore.pick() 

67 else: 

68 key = CaptchaStore.generate_key(generator) 

69 

70 # these can be used by format_output and render 

71 self._value = [key, ""] 

72 self._key = key 

73 self.id_ = self.build_attrs(attrs).get("id", None) 

74 

75 def id_for_label(self, id_): 

76 if id_: 76 ↛ 78line 76 didn't jump to line 78 because the condition on line 76 was always true

77 return id_ + "_1" 

78 return id_ 

79 

80 def image_url(self): 

81 return reverse("captcha-image", kwargs={"key": self._key}) 

82 

83 def audio_url(self): 

84 return ( 

85 reverse("captcha-audio", kwargs={"key": self._key}) 

86 if settings.CAPTCHA_FLITE_PATH 

87 else None 

88 ) 

89 

90 def refresh_url(self): 

91 return reverse("captcha-refresh") 

92 

93 

94class CaptchaTextInput(BaseCaptchaTextInput): 

95 template_name = "captcha/widgets/captcha.html" 

96 

97 def __init__( 

98 self, 

99 attrs=None, 

100 id_prefix=None, 

101 generator=None, 

102 # output_format=None, 

103 ): 

104 self.id_prefix = id_prefix 

105 self.generator = generator 

106 super().__init__(attrs) 

107 

108 def build_attrs(self, *args, **kwargs): 

109 ret = super().build_attrs(*args, **kwargs) 

110 if self.id_prefix and "id" in ret: 

111 ret["id"] = "%s_%s" % (self.id_prefix, ret["id"]) 

112 return ret 

113 

114 def id_for_label(self, id_): 

115 ret = super().id_for_label(id_) 

116 if self.id_prefix and "id" in ret: 

117 ret = "%s_%s" % (self.id_prefix, ret) 

118 return ret 

119 

120 def get_context(self, name, value, attrs): 

121 """Add captcha specific variables to context.""" 

122 context = super().get_context(name, value, attrs) 

123 context["image"] = self.image_url() 

124 context["audio"] = self.audio_url() 

125 return context 

126 

127 def render(self, name, value, attrs=None, renderer=None): 

128 self.fetch_captcha_store(name, value, attrs, self.generator) 

129 

130 extra_kwargs = {} 

131 extra_kwargs["renderer"] = renderer 

132 

133 return super().render(name, self._value, attrs=attrs, **extra_kwargs) 

134 

135 

136class CaptchaField(MultiValueField): 

137 def __init__(self, *args, **kwargs): 

138 fields = (CharField(show_hidden_initial=True), CharField()) 

139 if "error_messages" not in kwargs or "invalid" not in kwargs.get( 

140 "error_messages" 

141 ): 

142 if "error_messages" not in kwargs: 142 ↛ 144line 142 didn't jump to line 144 because the condition on line 142 was always true

143 kwargs["error_messages"] = {} 

144 kwargs["error_messages"].update( 

145 {"invalid": gettext_lazy("Invalid CAPTCHA")} 

146 ) 

147 

148 kwargs["widget"] = kwargs.pop( 

149 "widget", 

150 CaptchaTextInput( 

151 id_prefix=kwargs.pop("id_prefix", None), 

152 generator=kwargs.pop("generator", None), 

153 ), 

154 ) 

155 

156 super().__init__(fields, *args, **kwargs) 

157 

158 def compress(self, data_list): 

159 if data_list: 

160 return ",".join(data_list) 

161 return None 

162 

163 def clean(self, value): 

164 super().clean(value) 

165 response, value[1] = (value[1] or "").strip().lower(), "" 

166 if not settings.CAPTCHA_GET_FROM_POOL: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true

167 CaptchaStore.remove_expired() 

168 if settings.CAPTCHA_TEST_MODE and response.lower() == "passed": 

169 # automatically pass the test 

170 try: 

171 # try to delete the captcha based on its hash 

172 CaptchaStore.objects.get(hashkey=value[0]).delete() 

173 except CaptchaStore.DoesNotExist: 

174 # ignore errors 

175 pass 

176 elif not self.required and not response: 

177 pass 

178 else: 

179 try: 

180 CaptchaStore.objects.get( 

181 response=response, hashkey=value[0], expiration__gt=timezone.now() 

182 ).delete() 

183 except CaptchaStore.DoesNotExist: 

184 raise ValidationError( 

185 getattr(self, "error_messages", {}).get( 

186 "invalid", gettext_lazy("Invalid CAPTCHA") 

187 ) 

188 ) 

189 return value