added captcha to all forms

This commit is contained in:
Federico Justus Denkena 2023-11-06 16:21:10 +01:00
parent 6c561e32e1
commit 841f0e9dbd
Signed by: f-denkena
GPG Key ID: 28F91C66EE36F382
716 changed files with 5935 additions and 1008 deletions

0
bin/activate Normal file → Executable file
View File

View File

@ -15,7 +15,7 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from django.conf import settings
from website.views import newspage, post, about, search, home, contact, datenschutz, impressum, anmeldung, tagungsplan, success, failure
@ -34,4 +34,5 @@ urlpatterns = [
path('tagungsplan/', tagungsplan, name = 'tagungsplan'),
path('success/', success, name = 'success'),
path('failure/', failure, name = 'failure'),
path('captcha/', include('captcha.urls')),
]

View File

@ -0,0 +1,6 @@
VERSION = (0, 5, 20)
def get_version():
"Return the version as a human-format string."
return ".".join([str(i) for i in VERSION])

View File

@ -0,0 +1,99 @@
import os
import warnings
from django.conf import settings
CAPTCHA_FONT_PATH = getattr(
settings,
"CAPTCHA_FONT_PATH",
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "fonts/Vera.ttf")),
)
CAPTCHA_FONT_SIZE = getattr(settings, "CAPTCHA_FONT_SIZE", 22)
CAPTCHA_LETTER_ROTATION = getattr(settings, "CAPTCHA_LETTER_ROTATION", (-35, 35))
CAPTCHA_BACKGROUND_COLOR = getattr(settings, "CAPTCHA_BACKGROUND_COLOR", "#ffffff")
CAPTCHA_FOREGROUND_COLOR = getattr(settings, "CAPTCHA_FOREGROUND_COLOR", "#001100")
CAPTCHA_CHALLENGE_FUNCT = getattr(
settings, "CAPTCHA_CHALLENGE_FUNCT", "captcha.helpers.random_char_challenge"
)
CAPTCHA_NOISE_FUNCTIONS = getattr(
settings,
"CAPTCHA_NOISE_FUNCTIONS",
("captcha.helpers.noise_arcs", "captcha.helpers.noise_dots"),
)
CAPTCHA_FILTER_FUNCTIONS = getattr(
settings, "CAPTCHA_FILTER_FUNCTIONS", ("captcha.helpers.post_smooth",)
)
CAPTCHA_WORDS_DICTIONARY = getattr(
settings, "CAPTCHA_WORDS_DICTIONARY", "/usr/share/dict/words"
)
CAPTCHA_PUNCTUATION = getattr(settings, "CAPTCHA_PUNCTUATION", """_"',.;:-""")
CAPTCHA_FLITE_PATH = getattr(settings, "CAPTCHA_FLITE_PATH", None)
CAPTCHA_SOX_PATH = getattr(settings, "CAPTCHA_SOX_PATH", None)
CAPTCHA_TIMEOUT = getattr(settings, "CAPTCHA_TIMEOUT", 5) # Minutes
CAPTCHA_LENGTH = int(getattr(settings, "CAPTCHA_LENGTH", 4)) # Chars
# CAPTCHA_IMAGE_BEFORE_FIELD = getattr(settings, 'CAPTCHA_IMAGE_BEFORE_FIELD', True)
CAPTCHA_DICTIONARY_MIN_LENGTH = getattr(settings, "CAPTCHA_DICTIONARY_MIN_LENGTH", 0)
CAPTCHA_DICTIONARY_MAX_LENGTH = getattr(settings, "CAPTCHA_DICTIONARY_MAX_LENGTH", 99)
CAPTCHA_IMAGE_SIZE = getattr(settings, "CAPTCHA_IMAGE_SIZE", None)
CAPTCHA_IMAGE_TEMPLATE = getattr(
settings, "CAPTCHA_IMAGE_TEMPLATE", "captcha/image.html"
)
CAPTCHA_HIDDEN_FIELD_TEMPLATE = getattr(
settings, "CAPTCHA_HIDDEN_FIELD_TEMPLATE", "captcha/hidden_field.html"
)
CAPTCHA_TEXT_FIELD_TEMPLATE = getattr(
settings, "CAPTCHA_TEXT_FIELD_TEMPLATE", "captcha/text_field.html"
)
if getattr(settings, "CAPTCHA_FIELD_TEMPLATE", None):
msg = "CAPTCHA_FIELD_TEMPLATE setting is deprecated in favor of widget's template_name."
warnings.warn(msg, DeprecationWarning)
CAPTCHA_FIELD_TEMPLATE = getattr(settings, "CAPTCHA_FIELD_TEMPLATE", None)
if getattr(settings, "CAPTCHA_OUTPUT_FORMAT", None):
msg = "CAPTCHA_OUTPUT_FORMAT setting is deprecated in favor of widget's template_name."
warnings.warn(msg, DeprecationWarning)
CAPTCHA_OUTPUT_FORMAT = getattr(settings, "CAPTCHA_OUTPUT_FORMAT", None)
CAPTCHA_MATH_CHALLENGE_OPERATOR = getattr(
settings, "CAPTCHA_MATH_CHALLENGE_OPERATOR", "*"
)
CAPTCHA_GET_FROM_POOL = getattr(settings, "CAPTCHA_GET_FROM_POOL", False)
CAPTCHA_GET_FROM_POOL_TIMEOUT = getattr(settings, "CAPTCHA_GET_FROM_POOL_TIMEOUT", 5)
CAPTCHA_TEST_MODE = getattr(settings, "CAPTCHA_TEST_MODE", False)
CAPTCHA_2X_IMAGE = getattr(settings, "CAPTCHA_2X_IMAGE", True)
# Failsafe
if CAPTCHA_DICTIONARY_MIN_LENGTH > CAPTCHA_DICTIONARY_MAX_LENGTH:
CAPTCHA_DICTIONARY_MIN_LENGTH, CAPTCHA_DICTIONARY_MAX_LENGTH = (
CAPTCHA_DICTIONARY_MAX_LENGTH,
CAPTCHA_DICTIONARY_MIN_LENGTH,
)
def _callable_from_string(string_or_callable):
if callable(string_or_callable):
return string_or_callable
else:
return getattr(
__import__(".".join(string_or_callable.split(".")[:-1]), {}, {}, [""]),
string_or_callable.split(".")[-1],
)
def get_challenge(generator=None):
return _callable_from_string(generator or CAPTCHA_CHALLENGE_FUNCT)
def noise_functions():
if CAPTCHA_NOISE_FUNCTIONS:
return map(_callable_from_string, CAPTCHA_NOISE_FUNCTIONS)
return []
def filter_functions():
if CAPTCHA_FILTER_FUNCTIONS:
return map(_callable_from_string, CAPTCHA_FILTER_FUNCTIONS)
return []

View File

@ -0,0 +1,276 @@
import warnings
import django
from django.core.exceptions import ImproperlyConfigured
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import HiddenInput, MultiWidget, TextInput
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from captcha.conf import settings
from captcha.models import CaptchaStore
class CaptchaHiddenInput(HiddenInput):
"""Hidden input for the captcha key."""
# Use *args and **kwargs because signature changed in Django 1.11
def build_attrs(self, *args, **kwargs):
"""Disable autocomplete to prevent problems on page reload."""
attrs = super().build_attrs(*args, **kwargs)
attrs["autocomplete"] = "off"
return attrs
class CaptchaAnswerInput(TextInput):
"""Text input for captcha answer."""
# Use *args and **kwargs because signature changed in Django 1.11
def build_attrs(self, *args, **kwargs):
"""Disable automatic corrections and completions."""
attrs = super().build_attrs(*args, **kwargs)
attrs["autocapitalize"] = "off"
attrs["autocomplete"] = "off"
attrs["autocorrect"] = "off"
attrs["spellcheck"] = "false"
return attrs
class BaseCaptchaTextInput(MultiWidget):
"""
Base class for Captcha widgets
"""
def __init__(self, attrs=None):
widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs))
super(BaseCaptchaTextInput, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return value.split(",")
return [None, None]
def fetch_captcha_store(self, name, value, attrs=None, generator=None):
"""
Fetches a new CaptchaStore
This has to be called inside render
"""
try:
reverse("captcha-image", args=("dummy",))
except NoReverseMatch:
raise ImproperlyConfigured(
"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"
)
if settings.CAPTCHA_GET_FROM_POOL:
key = CaptchaStore.pick()
else:
key = CaptchaStore.generate_key(generator)
# these can be used by format_output and render
self._value = [key, ""]
self._key = key
self.id_ = self.build_attrs(attrs).get("id", None)
def id_for_label(self, id_):
if id_:
return id_ + "_1"
return id_
def image_url(self):
return reverse("captcha-image", kwargs={"key": self._key})
def audio_url(self):
return (
reverse("captcha-audio", kwargs={"key": self._key})
if settings.CAPTCHA_FLITE_PATH
else None
)
def refresh_url(self):
return reverse("captcha-refresh")
class CaptchaTextInput(BaseCaptchaTextInput):
template_name = "captcha/widgets/captcha.html"
def __init__(
self,
attrs=None,
field_template=None,
id_prefix=None,
generator=None,
output_format=None,
):
self.id_prefix = id_prefix
self.generator = generator
if field_template is not None:
msg = "CaptchaTextInput's field_template argument is deprecated in favor of widget's template_name."
warnings.warn(msg, DeprecationWarning)
self.field_template = field_template or settings.CAPTCHA_FIELD_TEMPLATE
if output_format is not None:
msg = "CaptchaTextInput's output_format argument is deprecated in favor of widget's template_name."
warnings.warn(msg, DeprecationWarning)
self.output_format = output_format or settings.CAPTCHA_OUTPUT_FORMAT
# Fallback to custom rendering in Django < 1.11
if (
not hasattr(self, "_render")
and self.field_template is None
and self.output_format is None
):
self.field_template = "captcha/field.html"
if self.output_format:
for key in ("image", "hidden_field", "text_field"):
if "%%(%s)s" % key not in self.output_format:
raise ImproperlyConfigured(
"All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s"
% (
", ".join(
[
"%%(%s)s" % k
for k in ("image", "hidden_field", "text_field")
]
),
"%%(%s)s" % key,
)
)
super(CaptchaTextInput, self).__init__(attrs)
def build_attrs(self, *args, **kwargs):
ret = super(CaptchaTextInput, self).build_attrs(*args, **kwargs)
if self.id_prefix and "id" in ret:
ret["id"] = "%s_%s" % (self.id_prefix, ret["id"])
return ret
def id_for_label(self, id_):
ret = super(CaptchaTextInput, self).id_for_label(id_)
if self.id_prefix and "id" in ret:
ret = "%s_%s" % (self.id_prefix, ret)
return ret
def get_context(self, name, value, attrs):
"""Add captcha specific variables to context."""
context = super(CaptchaTextInput, self).get_context(name, value, attrs)
context["image"] = self.image_url()
context["audio"] = self.audio_url()
return context
def format_output(self, rendered_widgets):
# hidden_field, text_field = rendered_widgets
if self.output_format:
ret = self.output_format % {
"image": self.image_and_audio,
"hidden_field": self.hidden_field,
"text_field": self.text_field,
}
return ret
elif self.field_template:
context = {
"image": mark_safe(self.image_and_audio),
"hidden_field": mark_safe(self.hidden_field),
"text_field": mark_safe(self.text_field),
}
return render_to_string(self.field_template, context)
def _direct_render(self, name, attrs):
"""Render the widget the old way - using field_template or output_format."""
context = {
"image": self.image_url(),
"name": name,
"key": self._key,
"id": "%s_%s" % (self.id_prefix, attrs.get("id"))
if self.id_prefix
else attrs.get("id"),
"audio": self.audio_url(),
}
self.image_and_audio = render_to_string(
settings.CAPTCHA_IMAGE_TEMPLATE, context
)
self.hidden_field = render_to_string(
settings.CAPTCHA_HIDDEN_FIELD_TEMPLATE, context
)
self.text_field = render_to_string(
settings.CAPTCHA_TEXT_FIELD_TEMPLATE, context
)
return self.format_output(None)
def render(self, name, value, attrs=None, renderer=None):
self.fetch_captcha_store(name, value, attrs, self.generator)
if self.field_template or self.output_format:
return self._direct_render(name, attrs)
extra_kwargs = {}
if django.VERSION >= (1, 11):
# https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.render
extra_kwargs["renderer"] = renderer
return super(CaptchaTextInput, self).render(
name, self._value, attrs=attrs, **extra_kwargs
)
class CaptchaField(MultiValueField):
def __init__(self, *args, **kwargs):
fields = (CharField(show_hidden_initial=True), CharField())
if "error_messages" not in kwargs or "invalid" not in kwargs.get(
"error_messages"
):
if "error_messages" not in kwargs:
kwargs["error_messages"] = {}
kwargs["error_messages"].update(
{"invalid": gettext_lazy("Invalid CAPTCHA")}
)
kwargs["widget"] = kwargs.pop(
"widget",
CaptchaTextInput(
output_format=kwargs.pop("output_format", None),
id_prefix=kwargs.pop("id_prefix", None),
generator=kwargs.pop("generator", None),
),
)
super(CaptchaField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
return ",".join(data_list)
return None
def clean(self, value):
super(CaptchaField, self).clean(value)
response, value[1] = (value[1] or "").strip().lower(), ""
if not settings.CAPTCHA_GET_FROM_POOL:
CaptchaStore.remove_expired()
if settings.CAPTCHA_TEST_MODE and response.lower() == "passed":
# automatically pass the test
try:
# try to delete the captcha based on its hash
CaptchaStore.objects.get(hashkey=value[0]).delete()
except CaptchaStore.DoesNotExist:
# ignore errors
pass
elif not self.required and not response:
pass
else:
try:
CaptchaStore.objects.get(
response=response, hashkey=value[0], expiration__gt=timezone.now()
).delete()
except CaptchaStore.DoesNotExist:
raise ValidationError(
getattr(self, "error_messages", {}).get(
"invalid", gettext_lazy("Invalid CAPTCHA")
)
)
return value

View File

@ -0,0 +1,123 @@
Bitstream Vera Fonts Copyright
The fonts have a generous copyright, allowing derivative works (as
long as "Bitstream" or "Vera" are not in the names), and full
redistribution (so long as they are not *sold* by themselves). They
can be be bundled, redistributed and sold with any software.
The fonts are distributed under the following copyright:
Copyright
=========
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream
Vera is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute
the Font Software, including without limitation the rights to use,
copy, merge, publish, distribute, and/or sell copies of the Font
Software, and to permit persons to whom the Font Software is furnished
to do so, subject to the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Bitstream" or the word "Vera".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Bitstream Vera" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT
SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font
Software without prior written authorization from the Gnome Foundation
or Bitstream Inc., respectively. For further information, contact:
fonts at gnome dot org.
Copyright FAQ
=============
1. I don't understand the resale restriction... What gives?
Bitstream is giving away these fonts, but wishes to ensure its
competitors can't just drop the fonts as is into a font sale system
and sell them as is. It seems fair that if Bitstream can't make money
from the Bitstream Vera fonts, their competitors should not be able to
do so either. You can sell the fonts as part of any software package,
however.
2. I want to package these fonts separately for distribution and
sale as part of a larger software package or system. Can I do so?
Yes. A RPM or Debian package is a "larger software package" to begin
with, and you aren't selling them independently by themselves.
See 1. above.
3. Are derivative works allowed?
Yes!
4. Can I change or add to the font(s)?
Yes, but you must change the name(s) of the font(s).
5. Under what terms are derivative works allowed?
You must change the name(s) of the fonts. This is to ensure the
quality of the fonts, both to protect Bitstream and Gnome. We want to
ensure that if an application has opened a font specifically of these
names, it gets what it expects (though of course, using fontconfig,
substitutions could still could have occurred during font
opening). You must include the Bitstream copyright. Additional
copyrights can be added, as per copyright law. Happy Font Hacking!
6. If I have improvements for Bitstream Vera, is it possible they might get
adopted in future versions?
Yes. The contract between the Gnome Foundation and Bitstream has
provisions for working with Bitstream to ensure quality additions to
the Bitstream Vera font family. Please contact us if you have such
additions. Note, that in general, we will want such additions for the
entire family, not just a single font, and that you'll have to keep
both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add
glyphs to the font, they must be stylistically in keeping with Vera's
design. Vera cannot become a "ransom note" font. Jim Lyles will be
providing a document describing the design elements used in Vera, as a
guide and aid for people interested in contributing to Vera.
7. I want to sell a software package that uses these fonts: Can I do so?
Sure. Bundle the fonts with your software and sell your software
with the fonts. That is the intent of the copyright.
8. If applications have built the names "Bitstream Vera" into them,
can I override this somehow to use fonts of my choosing?
This depends on exact details of the software. Most open source
systems and software (e.g., Gnome, KDE, etc.) are now converting to
use fontconfig (see www.fontconfig.org) to handle font configuration,
selection and substitution; it has provisions for overriding font
names and substituting alternatives. An example is provided by the
supplied local.conf file, which chooses the family Bitstream Vera for
"sans", "serif" and "monospace". Other software (e.g., the XFree86
core server) has other mechanisms for font substitution.

View File

@ -0,0 +1,11 @@
Contained herin is the Bitstream Vera font family.
The Copyright information is found in the COPYRIGHT.TXT file (along
with being incorporated into the fonts themselves).
The releases notes are found in the file "RELEASENOTES.TXT".
We hope you enjoy Vera!
Bitstream, Inc.
The Gnome Project

Binary file not shown.

View File

@ -0,0 +1,105 @@
import random
from django.urls import reverse
from captcha.conf import settings
def math_challenge():
operators = ("+", "*", "-")
operands = (random.randint(1, 10), random.randint(1, 10))
operator = random.choice(operators)
if operands[0] < operands[1] and "-" == operator:
operands = (operands[1], operands[0])
challenge = "%d%s%d" % (operands[0], operator, operands[1])
return (
"{}=".format(challenge.replace("*", settings.CAPTCHA_MATH_CHALLENGE_OPERATOR)),
str(eval(challenge)),
)
def random_char_challenge():
chars, ret = "abcdefghijklmnopqrstuvwxyz", ""
for i in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret
def unicode_challenge():
chars, ret = "äàáëéèïíîöóòüúù", ""
for i in range(settings.CAPTCHA_LENGTH):
ret += random.choice(chars)
return ret.upper(), ret
def word_challenge():
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, "r")
lines = fd.readlines()
fd.close()
while True:
word = random.choice(lines).strip()
if (
len(word) >= settings.CAPTCHA_DICTIONARY_MIN_LENGTH
and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH
):
break
return word.upper(), word.lower()
def huge_words_and_punctuation_challenge():
"Yay, undocumneted. Mostly used to test Issue 39 - http://code.google.com/p/django-simple-captcha/issues/detail?id=39"
fd = open(settings.CAPTCHA_WORDS_DICTIONARY, "rb")
lines = fd.readlines()
fd.close()
word = ""
while True:
word1 = random.choice(lines).strip()
word2 = random.choice(lines).strip()
punct = random.choice(settings.CAPTCHA_PUNCTUATION)
word = "%s%s%s" % (word1, punct, word2)
if (
len(word) >= settings.CAPTCHA_DICTIONARY_MIN_LENGTH
and len(word) <= settings.CAPTCHA_DICTIONARY_MAX_LENGTH
):
break
return word.upper(), word.lower()
def noise_arcs(draw, image):
size = image.size
draw.arc([-20, -20, size[0], 20], 0, 295, fill=settings.CAPTCHA_FOREGROUND_COLOR)
draw.line(
[-20, 20, size[0] + 20, size[1] - 20], fill=settings.CAPTCHA_FOREGROUND_COLOR
)
draw.line([-20, 0, size[0] + 20, size[1]], fill=settings.CAPTCHA_FOREGROUND_COLOR)
return draw
def noise_dots(draw, image):
size = image.size
for p in range(int(size[0] * size[1] * 0.1)):
draw.point(
(random.randint(0, size[0]), random.randint(0, size[1])),
fill=settings.CAPTCHA_FOREGROUND_COLOR,
)
return draw
def noise_null(draw, image):
return draw
def post_smooth(image):
from PIL import ImageFilter
return image.filter(ImageFilter.SMOOTH)
def captcha_image_url(key):
"""Return url to image. Need for ajax refresh and, etc"""
return reverse("captcha-image", args=[key])
def captcha_audio_url(key):
"""Return url to image. Need for ajax refresh and, etc"""
return reverse("captcha-audio", args=[key])

View File

@ -0,0 +1,2 @@
{% if audio %}<a href="{{ audio }}">{% endif %}<img src="{{ image }}" alt="captcha" class="captcha" />{% if audio %}</a>{% endif %}
{% include "django/forms/widgets/multiwidget.html" %}

View File

@ -0,0 +1,31 @@
# django-simple-captcha Bulgarian translation
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the django-simple-captcha package.
# Venelin Stoykov <vkstoykov@gmail.com>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.4.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-10 14:43+0200\n"
"PO-Revision-Date: 2014-02-10 15:00+0200\n"
"Last-Translator: Venelin Stoykov <vkstoykov@gmail.com>\n"
"Language-Team: bg <vkstoykov@gmail.com>\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: fields.py:91
msgid "Play CAPTCHA as audio file"
msgstr "Чуй текста като аудио файл"
#: fields.py:106 fields.py:135 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Сгрешен текст"
#: tests/tests.py:125
msgid "This field is required."
msgstr "Това поле е задължително"

View File

@ -0,0 +1,30 @@
# Czech translation of django-simple-captcha.
# Copyright (C) 2012 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Beda Kosata <bedrich.kosata@nic.cz>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.3.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-10-09 07:05+0200\n"
"PO-Revision-Date: 2012-10-09 07:08+0200\n"
"Last-Translator: Beda Kosata <bedrich.kosata@nic.cz>\n"
"Language-Team: Czech <>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Přehrát captchu jako audio soubor"
#: fields.py:67 fields.py:99 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Neplatná CAPTCHA"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Toto pole je povinné."

View File

@ -0,0 +1,32 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-16 12:06+0200\n"
"PO-Revision-Date: 2013-07-16 12:10+0100\n"
"Last-Translator: Patrick Lauber <patrick.lauber@divio.ch>\n"
"Language-Team: DE <digi@treepy.com>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.5.7\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "CAPTCHA als Audiodatei abspielen."
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Ungültiges CAPTCHA"
#: tests/tests.py:125
msgid "This field is required."
msgstr "Dieses Feld wird benötigt."

View File

@ -0,0 +1,30 @@
# django-simple-captcha French translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Patrick Samson <maxcom@laposte.net>, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:44+0300\n"
"PO-Revision-Date: 2010-09-16 12:16+0200\n"
"Last-Translator: Marco Bonetti <mbonetti@gmail.com>\n"
"Language-Team: en <mbonetti@gmail.com>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Play CAPTCHA as audio file"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Invalid CAPTCHA"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "This field is required."

View File

@ -0,0 +1,32 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-16 12:06+0200\n"
"PO-Revision-Date: 2014-05-20 21:22+0100\n"
"Last-Translator: https://github.com/dragosdobrota\n"
"Language-Team: es\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.5\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "Reproducir CAPTCHA de audio"
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA no válido"
#: tests/tests.py:125
msgid "This field is required."
msgstr "Este campo es obligatorio."

View File

@ -0,0 +1,31 @@
# django-simple-captcha French translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Patrick Samson <maxcom@laposte.net>, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:44+0300\n"
"PO-Revision-Date: 2020-10-04 19:08+0330\n"
"Last-Translator: Mehdi Namaki <mavenium@gmail.com>\n"
"Language-Team: fa <mavenium@gmail.com>\n"
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
"X-Generator: Poedit 2.3\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "پخش کد امنیتی به عنوان یک پرونده صوتی"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "کد امنیتی صحیح نیست"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "این فیلد اجباری است."

View File

@ -0,0 +1,30 @@
# django-simple-captcha French translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Patrick Samson <maxcom@laposte.net>, 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:44+0300\n"
"PO-Revision-Date: 2010-09-16 12:16+0200\n"
"Last-Translator: Patrick Samson <maxcom@laposte.net>\n"
"Language-Team: fr <maxcom@laposte.net>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Écouter la version audio"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA invalide"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Ce champ est obligatoire."

View File

@ -0,0 +1,30 @@
# django-simple-captcha Italian translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Arjuna Del Toso <stupidate@gmail.net>, 2012
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-11-14 02:53+0000\n"
"PO-Revision-Date: 2012-11-14 02:53+0000\n"
"Last-Translator: Arjuna Del Toso <stupidate@gmail.com>\n"
"MIME-Version: 1.0\n"
"Language-Team: Arjuna Del Toso <stupidate@gmail.com>\n"
"Language: it\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: .\fields.py:56
msgid "Play CAPTCHA as audio file"
msgstr "Ascolta la parola di controllo"
#: .\fields.py:71 .\fields.py:96 .\tests\__init__.py:70
msgid "Invalid CAPTCHA"
msgstr "Parola di controllo sbagliata"
#: .\tests\__init__.py:97
msgid "This field is required."
msgstr "Questo campo è obbligatorio"

View File

@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Keisuke URAGO <bravo@resourcez.org>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-13 07:11+0900\n"
"PO-Revision-Date: 2014-02-13 07:11+0900\n"
"Last-Translator: Keisuke URAGO <bravo@resourcez.org>\n"
"Language-Team: Keisuke URAGO <bravo@resourcez.org>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "CAPTCHAをオーディオで読み上げる"
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "CAPTCHAの値が違っています"
#: tests/tests.py:125
msgid "This field is required."
msgstr "この項目は必須です"

View File

@ -0,0 +1,31 @@
# django-simple-captcha Dutch translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Leon de Rijke <leon@cmsworks.nl>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-02-15 13:26+0100\n"
"PO-Revision-Date: 2013-02-15 13:26+0100\n"
"Last-Translator: Leon de Rijke <leon@cmsworks.nl>\n"
"MIME-Version: 1.0\n"
"Language-Team: Leon de Rijke <leon@cmsworks.nl>\n"
"Language: nl\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Speel CAPTCHA als audiobestand af"
#: fields.py:67 fields.py:94 tests/__init__.py:64 tests/__init__.py:186
#: tests/__init__.py:193
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA ongeldig, probeer het opnieuw"
#: tests/__init__.py:90
msgid "This field is required."
msgstr "Dit veld is verplicht."

View File

@ -0,0 +1,32 @@
# Polish translation for django-simple-captcha.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the django-simple-captcha package.
# Sławomir Zborowski <slawomir.zborowski@hotmail.com>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-08-18 18:49+0200\n"
"PO-Revision-Date: 2013-08-18 18:52+0200\n"
"Last-Translator: Sławomir Zborowski <slawomir.zborowski@hotmail.com>\n"
"Language-Team: Polisch\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2)\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "Odtwórz CAPTCHĘ jako plik dźwiękowy"
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Niepoprawnie wpisana CAPTCHA"
#: tests/tests.py:125
msgid "This field is required."
msgstr "To pole jest wymagane."

View File

@ -0,0 +1,31 @@
# django-simple-captcha Portuguese (Brazilian) translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Alisson Patricio <eu@alisson.net>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.7\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-05-18 10:58-0300\n"
"PO-Revision-Date: 2013-05-18 13:12-0300\n"
"Last-Translator: Alisson Patricio <eu@alisson.net>\n"
"Language-Team: Alisson Patricio <eu@alisson.net>\n"
"Language: pt_br\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Ouça o arquivo de áudio"
#: fields.py:66 fields.py:93 tests/__init__.py:69
#: tests/__init__.py:198 tests/__init__.py:205
msgid "Invalid CAPTCHA"
msgstr "Resposta inválida"
#: tests/__init__.py:95
msgid "This field is required."
msgstr "Este campo é obrigatório."

View File

@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.7\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:17+0300\n"
"PO-Revision-Date: 2012-07-25 11:17+0300\n"
"Last-Translator: \n"
"Language-Team: ru <LL@li.org>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Воспроизвести CAPTCHA в виде аудио файла"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Неверный ответ"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Это поле обязательно для заполнения."

View File

@ -0,0 +1,32 @@
# Slovak translation of django-simple-captcha.
# Copyright (C) 2013 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Pavol Otto <pavol.otto@nic.cz>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.7\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-10-15 17:16+0200\n"
"PO-Revision-Date: 2013-10-15 17:16+0200\n"
"Last-Translator: Pavol Otto <pavol.otto@nic.cz>\n"
"Language-Team: SK\n"
"Language: sk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
#: fields.py:90
msgid "Play CAPTCHA as audio file"
msgstr "Prehrať captchu ako audio súbor"
#: fields.py:105 fields.py:134 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Neplatná CAPTCHA"
#: tests/tests.py:125
msgid "This field is required."
msgstr "Toto pole je povinné."

View File

@ -0,0 +1,31 @@
# django-simple-captcha Swedish translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Stefan Norman <stefan.norman@bricco.se>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.2.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-07-25 11:44+0300\n"
"PO-Revision-Date: 2018-12-03 06:41+0100\n"
"Language-Team: en <mbonetti@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.2\n"
"Last-Translator: Stefan Norman <stefan.norman@bricco.se>\n"
"Language: sv\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "Spela CAPTCHA som ljudfil"
#: fields.py:66 fields.py:89 tests/__init__.py:62
msgid "Invalid CAPTCHA"
msgstr "Ogiltig CAPTCHA"
#: tests/__init__.py:88
msgid "This field is required."
msgstr "Detta fält är obligatoriskt."

View File

@ -0,0 +1,31 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-01-19 16:33-0200\n"
"PO-Revision-Date: 2013-01-19 20:52+0200\n"
"Last-Translator: Gokmen Gorgen <gokmen@alageek.com>\n"
"Language-Team: TR Gokmen Gorgen <gokmen@alageek.com>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: fields.py:50
msgid "Play CAPTCHA as audio file"
msgstr "Değeri ses dosyası olarak çal"
#: fields.py:67 fields.py:94 tests/__init__.py:64 tests/__init__.py:186
#: tests/__init__.py:193
msgid "Invalid CAPTCHA"
msgstr "Geçersiz değer"
#: tests/__init__.py:90
msgid "This field is required."
msgstr "Bu alan zorunludur."

View File

@ -0,0 +1,32 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-02-21 22:05+0200\n"
"PO-Revision-Date: 2014-02-21 22:05+0200\n"
"Last-Translator: @FuriousCoder\n"
"Language-Team: uk @FuriousCoder\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: fields.py:91
msgid "Play CAPTCHA as audio file"
msgstr "Відтворити CAPTCHA як аудіо файл."
#: fields.py:106 fields.py:135 tests/tests.py:99 tests/tests.py:239
#: tests/tests.py:246
msgid "Invalid CAPTCHA"
msgstr "Неправильна відповідь."
#: tests/tests.py:125
msgid "This field is required."
msgstr "Це поле є обов'язковим."

View File

@ -0,0 +1,31 @@
# django-simple-captcha Chinese Simplified translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Ming Chen <c.ming@live.com>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-03-01 05:04+0800\n"
"PO-Revision-Date: 2013-03-01 05:04+0800\n"
"Last-Translator: Ming Chen <c.ming@live.com>\n"
"Language-Team: zh_cn Ming Chen <c.ming@live.com>\n"
"Language: zh_cn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "使用语音方式播放认证码"
#: fields.py:66 fields.py:93 tests/__init__.py:69 tests/__init__.py:198
#: tests/__init__.py:205
msgid "Invalid CAPTCHA"
msgstr "认证码错误"
#: tests/__init__.py:95
msgid "This field is required."
msgstr "这个字段是必须的"

View File

@ -0,0 +1,31 @@
# django-simple-captcha Chinese Simplified translation.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Ming Chen <c.ming@live.com>, 2013.
#
msgid ""
msgstr ""
"Project-Id-Version: django-simple-captcha 0.3.6\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-03-01 05:04+0800\n"
"PO-Revision-Date: 2013-03-01 05:04+0800\n"
"Last-Translator: Ming Chen <c.ming@live.com>\n"
"Language-Team: zh_cn Ming Chen <c.ming@live.com>\n"
"Language: zh_cn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
#: fields.py:49
msgid "Play CAPTCHA as audio file"
msgstr "使用语音方式播放认证码"
#: fields.py:66 fields.py:93 tests/__init__.py:69 tests/__init__.py:198
#: tests/__init__.py:205
msgid "Invalid CAPTCHA"
msgstr "认证码错误"
#: tests/__init__.py:95
msgid "This field is required."
msgstr "这个字段是必须的"

View File

@ -0,0 +1,29 @@
import sys
from django.core.management.base import BaseCommand
from django.utils import timezone
class Command(BaseCommand):
help = "Clean up expired captcha hashkeys."
def handle(self, **options):
from captcha.models import CaptchaStore
verbose = int(options.get("verbosity"))
expired_keys = CaptchaStore.objects.filter(
expiration__lte=timezone.now()
).count()
if verbose >= 1:
print("Currently %d expired hashkeys" % expired_keys)
try:
CaptchaStore.remove_expired()
except Exception:
if verbose >= 1:
print("Unable to delete expired hashkeys.")
sys.exit(1)
if verbose >= 1:
if expired_keys > 0:
print("%d expired hashkeys removed." % expired_keys)
else:
print("No keys to remove.")

View File

@ -0,0 +1,34 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from captcha.models import CaptchaStore
class Command(BaseCommand):
help = "Create a pool of random captchas."
def add_arguments(self, parser):
parser.add_argument(
"--pool-size",
type=int,
default=1000,
help="Number of new captchas to create, default=1000",
)
parser.add_argument(
"--cleanup-expired",
action="store_true",
default=True,
help="Cleanup expired captchas after creating new ones",
)
@transaction.atomic()
def handle(self, **options):
verbose = int(options.get("verbosity"))
count = options.get("pool_size")
CaptchaStore.create_pool(count)
verbose and self.stdout.write("Created %d new captchas\n" % count)
options.get("cleanup_expired") and CaptchaStore.remove_expired()
options.get("cleanup_expired") and verbose and self.stdout.write(
"Expired captchas cleaned up\n"
)

View File

@ -0,0 +1,31 @@
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="CaptchaStore",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("challenge", models.CharField(max_length=32)),
("response", models.CharField(max_length=32)),
("hashkey", models.CharField(unique=True, max_length=40)),
("expiration", models.DateTimeField()),
],
options={},
bases=(models.Model,),
)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-03-06 11:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("captcha", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="captchastore",
name="id",
field=models.AutoField(primary_key=True, serialize=False),
),
]

View File

@ -0,0 +1,88 @@
import datetime
import hashlib
import logging
import random
import time
from django.db import models
from django.utils import timezone
from django.utils.encoding import smart_str
from captcha.conf import settings as captcha_settings
# Heavily based on session key generation in Django
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, "SystemRandom"):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
MAX_RANDOM_KEY = 18446744073709551616 # 2 << 63
logger = logging.getLogger(__name__)
class CaptchaStore(models.Model):
id = models.AutoField(primary_key=True)
challenge = models.CharField(blank=False, max_length=32)
response = models.CharField(blank=False, max_length=32)
hashkey = models.CharField(blank=False, max_length=40, unique=True)
expiration = models.DateTimeField(blank=False)
def save(self, *args, **kwargs):
self.response = self.response.lower()
if not self.expiration:
self.expiration = timezone.now() + datetime.timedelta(
minutes=int(captcha_settings.CAPTCHA_TIMEOUT)
)
if not self.hashkey:
key_ = (
smart_str(randrange(0, MAX_RANDOM_KEY))
+ smart_str(time.time())
+ smart_str(self.challenge, errors="ignore")
+ smart_str(self.response, errors="ignore")
).encode("utf8")
self.hashkey = hashlib.sha1(key_).hexdigest()
del key_
super(CaptchaStore, self).save(*args, **kwargs)
def __str__(self):
return self.challenge
def remove_expired(cls):
cls.objects.filter(expiration__lte=timezone.now()).delete()
remove_expired = classmethod(remove_expired)
@classmethod
def generate_key(cls, generator=None):
challenge, response = captcha_settings.get_challenge(generator)()
store = cls.objects.create(challenge=challenge, response=response)
return store.hashkey
@classmethod
def pick(cls):
if not captcha_settings.CAPTCHA_GET_FROM_POOL:
return cls.generate_key()
def fallback():
logger.error("Couldn't get a captcha from pool, generating")
return cls.generate_key()
# Pick up a random item from pool
minimum_expiration = timezone.now() + datetime.timedelta(
minutes=int(captcha_settings.CAPTCHA_GET_FROM_POOL_TIMEOUT)
)
store = (
cls.objects.filter(expiration__gt=minimum_expiration).order_by("?").first()
)
return (store and store.hashkey) or fallback()
@classmethod
def create_pool(cls, count=1000):
assert count > 0
while count > 0:
cls.generate_key()
count -= 1

View File

@ -0,0 +1 @@
{{image}}{{hidden_field}}{{text_field}}

View File

@ -0,0 +1 @@
<input id="{{id}}_0" name="{{name}}_0" type="hidden" value="{{key}}" />

View File

@ -0,0 +1,4 @@
{% load i18n %}
{% spaceless %}
{% if audio %}<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{audio}}">{% endif %}<img src="{{image}}" alt="captcha" class="captcha" />{% if audio %}</a>{% endif %}
{% endspaceless %}

View File

@ -0,0 +1 @@
<input autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" id="{{id}}_1" name="{{name}}_1" type="text" />

View File

@ -0,0 +1,9 @@
{% load i18n %}
{% spaceless %}
{% if audio %}
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}">
{% endif %}
<img src="{{ image }}" alt="captcha" class="captcha" />
{% if audio %}</a>{% endif %}
{% endspaceless %}
{% include "django/forms/widgets/multiwidget.html" %}

View File

@ -0,0 +1 @@
from .tests import CaptchaCase, trivial_challenge # NOQA

View File

@ -0,0 +1,594 @@
import datetime
import json
import os
import re
import warnings
from io import BytesIO
from PIL import Image
from testfixtures import LogCapture
import django
from django.core import management
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy
from captcha.conf import settings
from captcha.fields import CaptchaField, CaptchaTextInput
from captcha.models import CaptchaStore
@override_settings(ROOT_URLCONF="captcha.tests.urls")
class CaptchaCase(TestCase):
def setUp(self):
self.stores = {}
self.__current_settings_output_format = settings.CAPTCHA_OUTPUT_FORMAT
self.__current_settings_dictionary = settings.CAPTCHA_WORDS_DICTIONARY
self.__current_settings_punctuation = settings.CAPTCHA_PUNCTUATION
tested_helpers = [
"captcha.helpers.math_challenge",
"captcha.helpers.random_char_challenge",
"captcha.helpers.unicode_challenge",
]
if os.path.exists("/usr/share/dict/words"):
settings.CAPTCHA_WORDS_DICTIONARY = "/usr/share/dict/words"
settings.CAPTCHA_PUNCTUATION = ";-,."
tested_helpers.append("captcha.helpers.word_challenge")
tested_helpers.append(
"captcha.helpers.huge_words_and_punctuation_challenge"
)
for helper in tested_helpers:
challenge, response = settings._callable_from_string(helper)()
(
self.stores[helper.rsplit(".", 1)[-1].replace("_challenge", "_store")],
_,
) = CaptchaStore.objects.get_or_create(
challenge=challenge, response=response
)
challenge, response = settings.get_challenge()()
self.stores["default_store"], _ = CaptchaStore.objects.get_or_create(
challenge=challenge, response=response
)
self.default_store = self.stores["default_store"]
def tearDown(self):
settings.CAPTCHA_OUTPUT_FORMAT = self.__current_settings_output_format
settings.CAPTCHA_WORDS_DICTIONARY = self.__current_settings_dictionary
settings.CAPTCHA_PUNCTUATION = self.__current_settings_punctuation
def _assertFormError(self, response, form_name, *args, **kwargs):
if django.VERSION >= (4, 1):
self.assertFormError(response.context.get(form_name), *args, **kwargs)
else:
self.assertFormError(response, form_name, *args, **kwargs)
def __extract_hash_and_response(self, r):
hash_ = re.findall(r'value="([0-9a-f]+)"', str(r.content))[0]
response = CaptchaStore.objects.get(hashkey=hash_).response
return hash_, response
def test_image(self):
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.has_header("content-type"))
self.assertEqual(response["content-type"], "image/png")
def test_audio(self):
if not settings.CAPTCHA_FLITE_PATH:
return
for key in (
self.stores.get("math_store").hashkey,
self.stores.get("math_store").hashkey,
self.default_store.hashkey,
):
response = self.client.get(reverse("captcha-audio", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.ranged_file.size > 1024)
self.assertTrue(response.has_header("content-type"))
self.assertEqual(response["content-type"], "audio/wav")
def test_form_submit(self):
r = self.client.get(reverse("captcha-test"))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0=hash_,
captcha_1=response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find("Form validated") > 0)
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0=hash_,
captcha_1=response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertFalse(str(r.content).find("Form validated") > 0)
def test_modelform(self):
r = self.client.get(reverse("captcha-test-model-form"))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(
reverse("captcha-test-model-form"),
dict(
captcha_0=hash_,
captcha_1=response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find("Form validated") > 0)
r = self.client.post(
reverse("captcha-test-model-form"),
dict(
captcha_0=hash_,
captcha_1=response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertFalse(str(r.content).find("Form validated") > 0)
def test_wrong_submit(self):
for urlname in ("captcha-test", "captcha-test-model-form"):
r = self.client.get(reverse(urlname))
self.assertEqual(r.status_code, 200)
r = self.client.post(
reverse(urlname),
dict(
captcha_0="abc",
captcha_1="wrong response",
subject="xxx",
sender="asasd@asdasd.com",
),
)
self._assertFormError(r, "form", "captcha", gettext_lazy("Invalid CAPTCHA"))
def test_deleted_expired(self):
self.default_store.expiration = timezone.now() - datetime.timedelta(minutes=5)
self.default_store.save()
hash_ = self.default_store.hashkey
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0=hash_,
captcha_1=self.default_store.response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertFalse("Form validated" in str(r.content))
# expired -> deleted
try:
CaptchaStore.objects.get(hashkey=hash_)
self.fail()
except Exception:
pass
def test_custom_error_message(self):
r = self.client.get(reverse("captcha-test-custom-error-message"))
self.assertEqual(r.status_code, 200)
# Wrong answer
r = self.client.post(
reverse("captcha-test-custom-error-message"),
dict(captcha_0="abc", captcha_1="wrong response"),
)
self._assertFormError(r, "form", "captcha", "TEST CUSTOM ERROR MESSAGE")
# empty answer
r = self.client.post(
reverse("captcha-test-custom-error-message"),
dict(captcha_0="abc", captcha_1=""),
)
self._assertFormError(
r, "form", "captcha", gettext_lazy("This field is required.")
)
def test_repeated_challenge(self):
CaptchaStore.objects.create(challenge="xxx", response="xxx")
try:
CaptchaStore.objects.create(challenge="xxx", response="xxx")
except Exception:
self.fail()
def test_repeated_challenge_form_submit(self):
__current_challange_function = settings.CAPTCHA_CHALLENGE_FUNCT
for urlname in ("captcha-test", "captcha-test-model-form"):
settings.CAPTCHA_CHALLENGE_FUNCT = "captcha.tests.trivial_challenge"
r1 = self.client.get(reverse(urlname))
r2 = self.client.get(reverse(urlname))
self.assertEqual(r1.status_code, 200)
self.assertEqual(r2.status_code, 200)
if re.findall(r'value="([0-9a-f]+)"', str(r1.content)):
hash_1 = re.findall(r'value="([0-9a-f]+)"', str(r1.content))[0]
else:
self.fail()
if re.findall(r'value="([0-9a-f]+)"', str(r2.content)):
hash_2 = re.findall(r'value="([0-9a-f]+)"', str(r2.content))[0]
else:
self.fail()
try:
store_1 = CaptchaStore.objects.get(hashkey=hash_1)
store_2 = CaptchaStore.objects.get(hashkey=hash_2)
except Exception:
self.fail()
self.assertTrue(store_1.pk != store_2.pk)
self.assertTrue(store_1.response == store_2.response)
self.assertTrue(hash_1 != hash_2)
r1 = self.client.post(
reverse(urlname),
dict(
captcha_0=hash_1,
captcha_1=store_1.response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r1.status_code, 200)
self.assertTrue(str(r1.content).find("Form validated") > 0)
try:
store_2 = CaptchaStore.objects.get(hashkey=hash_2)
except Exception:
self.fail()
r2 = self.client.post(
reverse(urlname),
dict(
captcha_0=hash_2,
captcha_1=store_2.response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r2.status_code, 200)
self.assertTrue(str(r2.content).find("Form validated") > 0)
settings.CAPTCHA_CHALLENGE_FUNCT = __current_challange_function
def test_output_format(self):
for urlname in ("captcha-test", "captcha-test-model-form"):
settings.CAPTCHA_OUTPUT_FORMAT = (
"%(image)s<p>Hello, captcha world</p>%(hidden_field)s%(text_field)s"
)
r = self.client.get(reverse(urlname))
self.assertEqual(r.status_code, 200)
self.assertTrue("<p>Hello, captcha world</p>" in str(r.content))
def test_invalid_output_format(self):
for urlname in ("captcha-test", "captcha-test-model-form"):
settings.CAPTCHA_OUTPUT_FORMAT = "%(image)s"
try:
with warnings.catch_warnings(record=True) as w:
self.client.get(reverse(urlname))
assert len(w) == 1
self.assertTrue("CAPTCHA_OUTPUT_FORMAT" in str(w[-1].message))
self.fail()
except ImproperlyConfigured as e:
self.assertTrue("CAPTCHA_OUTPUT_FORMAT" in str(e))
def test_per_form_format(self):
settings.CAPTCHA_OUTPUT_FORMAT = (
"%(image)s testCustomFormatString %(hidden_field)s %(text_field)s"
)
r = self.client.get(reverse("captcha-test"))
self.assertTrue("testCustomFormatString" in str(r.content))
r = self.client.get(reverse("test_per_form_format"))
self.assertTrue("testPerFieldCustomFormatString" in str(r.content))
def test_custom_generator(self):
r = self.client.get(reverse("test_custom_generator"))
hash_, response = self.__extract_hash_and_response(r)
self.assertEqual(response, "111111")
def test_issue31_proper_abel(self):
settings.CAPTCHA_OUTPUT_FORMAT = "%(image)s %(hidden_field)s %(text_field)s"
r = self.client.get(reverse("captcha-test"))
self.assertTrue('<label for="id_captcha_1"' in str(r.content))
def test_refresh_view(self):
r = self.client.get(
reverse("captcha-refresh"), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
try:
new_data = json.loads(str(r.content, encoding="ascii"))
self.assertTrue("image_url" in new_data)
self.assertTrue("audio_url" in new_data)
except Exception:
self.fail()
def test_content_length(self):
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertTrue(response.has_header("content-length"))
self.assertTrue(response["content-length"].isdigit())
self.assertTrue(int(response["content-length"]))
def test_issue12_proper_instantiation(self):
"""
This test covers a default django field and widget behavior
It not assert anything. If something is wrong it will raise a error!
"""
settings.CAPTCHA_OUTPUT_FORMAT = "%(image)s %(hidden_field)s %(text_field)s"
widget = CaptchaTextInput(attrs={"class": "required"})
CaptchaField(widget=widget)
def test_test_mode_issue15(self):
__current_test_mode_setting = settings.CAPTCHA_TEST_MODE
settings.CAPTCHA_TEST_MODE = False
r = self.client.get(reverse("captcha-test"))
self.assertEqual(r.status_code, 200)
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0="abc",
captcha_1="wrong response",
subject="xxx",
sender="asasd@asdasd.com",
),
)
self._assertFormError(r, "form", "captcha", gettext_lazy("Invalid CAPTCHA"))
settings.CAPTCHA_TEST_MODE = True
# Test mode, only 'PASSED' is accepted
r = self.client.get(reverse("captcha-test"))
self.assertEqual(r.status_code, 200)
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0="abc",
captcha_1="wrong response",
subject="xxx",
sender="asasd@asdasd.com",
),
)
self._assertFormError(r, "form", "captcha", gettext_lazy("Invalid CAPTCHA"))
r = self.client.get(reverse("captcha-test"))
self.assertEqual(r.status_code, 200)
r = self.client.post(
reverse("captcha-test"),
dict(
captcha_0="abc",
captcha_1="passed",
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertTrue(str(r.content).find("Form validated") > 0)
settings.CAPTCHA_TEST_MODE = __current_test_mode_setting
def test_get_version(self):
import captcha
captcha.get_version()
def test_missing_value(self):
r = self.client.get(reverse("captcha-test-non-required"))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
# Empty response is okay when required is False
r = self.client.post(
reverse("captcha-test-non-required"),
dict(subject="xxx", sender="asasd@asdasd.com"),
)
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find("Form validated") > 0)
# But a valid response is okay, too
r = self.client.get(reverse("captcha-test-non-required"))
self.assertEqual(r.status_code, 200)
hash_, response = self.__extract_hash_and_response(r)
r = self.client.post(
reverse("captcha-test-non-required"),
dict(
captcha_0=hash_,
captcha_1=response,
subject="xxx",
sender="asasd@asdasd.com",
),
)
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find("Form validated") > 0)
def test_autocomplete_off(self):
r = self.client.get(reverse("captcha-test"))
captcha_input = (
'<input type="text" name="captcha_1" autocomplete="off" spellcheck="false" autocorrect="off" '
'autocapitalize="off" id="id_captcha_1" required />'
)
self.assertContains(r, captcha_input, html=True)
def test_issue201_autocomplete_off_on_hiddeninput(self):
r = self.client.get(reverse("captcha-test"))
# Inspect the response context to find out the captcha key.
key = r.context["form"]["captcha"].field.widget._key
# Assety that autocomplete=off is set on the hidden captcha field.
self.assertInHTML(
'<input type="hidden" name="captcha_0" value="{}" id="id_captcha_0" autocomplete="off" required />'.format(
key
),
str(r.content),
)
def test_transparent_background(self):
__current_test_mode_setting = settings.CAPTCHA_BACKGROUND_COLOR
settings.CAPTCHA_BACKGROUND_COLOR = "transparent"
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.has_header("content-type"))
self.assertEqual(response["content-type"], "image/png")
settings.CAPTCHA_BACKGROUND_COLOR = __current_test_mode_setting
def test_expired_captcha_returns_410(self):
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
CaptchaStore.objects.filter(hashkey=key).delete()
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 410)
def test_id_prefix(self):
r = self.client.get(reverse("captcha-test-id-prefix"))
self.assertTrue(
'<label for="form1_id_captcha1_1">Captcha1:</label>' in str(r.content)
)
self.assertTrue('id="form1_id_captcha1_1"' in str(r.content))
self.assertTrue(
'<label for="form2_id_captcha2_1">Captcha2:</label>' in str(r.content)
)
self.assertTrue('id="form2_id_captcha2_1"' in str(r.content))
def test_image_size(self):
__current_test_mode_setting = settings.CAPTCHA_IMAGE_SIZE
for key in [store.hashkey for store in self.stores.values()]:
settings.CAPTCHA_IMAGE_SIZE = (201, 97)
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.has_header("content-type"))
self.assertEqual(response["content-type"], "image/png")
self.assertEqual(Image.open(BytesIO(response.content)).size, (201, 97))
settings.CAPTCHA_IMAGE_SIZE = __current_test_mode_setting
def test_multiple_fonts(self):
vera = os.path.join(os.path.dirname(__file__), "..", "fonts", "Vera.ttf")
__current_test_mode_setting = settings.CAPTCHA_FONT_PATH
settings.CAPTCHA_FONT_PATH = vera
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["content-type"], "image/png")
settings.CAPTCHA_FONT_PATH = [vera, vera, vera]
for key in [store.hashkey for store in self.stores.values()]:
response = self.client.get(reverse("captcha-image", kwargs=dict(key=key)))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["content-type"], "image/png")
settings.CAPTCHA_FONT_PATH = False
for key in [store.hashkey for store in self.stores.values()]:
try:
response = self.client.get(
reverse("captcha-image", kwargs=dict(key=key))
)
self.fail()
except ImproperlyConfigured:
pass
settings.CAPTCHA_FONT_PATH = __current_test_mode_setting
def test_template_overrides(self):
__current_test_mode_setting = settings.CAPTCHA_IMAGE_TEMPLATE
__current_field_template = settings.CAPTCHA_FIELD_TEMPLATE
settings.CAPTCHA_IMAGE_TEMPLATE = "captcha_test/image.html"
settings.CAPTCHA_FIELD_TEMPLATE = "captcha/field.html"
for urlname in ("captcha-test", "captcha-test-model-form"):
settings.CAPTCHA_CHALLENGE_FUNCT = "captcha.tests.trivial_challenge"
r = self.client.get(reverse(urlname))
self.assertTrue("captcha-template-test" in str(r.content))
settings.CAPTCHA_IMAGE_TEMPLATE = __current_test_mode_setting
settings.CAPTCHA_FIELD_TEMPLATE = __current_field_template
def test_math_challenge(self):
__current_test_mode_setting = settings.CAPTCHA_MATH_CHALLENGE_OPERATOR
settings.CAPTCHA_MATH_CHALLENGE_OPERATOR = "~"
helper = "captcha.helpers.math_challenge"
challenge, response = settings._callable_from_string(helper)()
while settings.CAPTCHA_MATH_CHALLENGE_OPERATOR not in challenge:
challenge, response = settings._callable_from_string(helper)()
self.assertEqual(
response,
str(
eval(
challenge.replace(settings.CAPTCHA_MATH_CHALLENGE_OPERATOR, "*")[
:-1
]
)
),
)
settings.CAPTCHA_MATH_CHALLENGE_OPERATOR = __current_test_mode_setting
def test_get_from_pool(self):
__current_test_get_from_pool_setting = settings.CAPTCHA_GET_FROM_POOL
__current_test_get_from_pool_timeout_setting = (
settings.CAPTCHA_GET_FROM_POOL_TIMEOUT
)
__current_test_timeout_setting = settings.CAPTCHA_TIMEOUT
settings.CAPTCHA_GET_FROM_POOL = True
settings.CAPTCHA_GET_FROM_POOL_TIMEOUT = 5
settings.CAPTCHA_TIMEOUT = 90
CaptchaStore.objects.all().delete() # Delete objects created during SetUp
POOL_SIZE = 10
CaptchaStore.create_pool(count=POOL_SIZE)
self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
pool = CaptchaStore.objects.values_list("hashkey", flat=True)
random_pick = CaptchaStore.pick()
self.assertIn(random_pick, pool)
# pick() should not create any extra captcha
self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
settings.CAPTCHA_GET_FROM_POOL = __current_test_get_from_pool_setting
settings.CAPTCHA_GET_FROM_POOL_TIMEOUT = (
__current_test_get_from_pool_timeout_setting
)
settings.CAPTCHA_TIMEOUT = __current_test_timeout_setting
def test_captcha_create_pool(self):
CaptchaStore.objects.all().delete() # Delete objects created during SetUp
POOL_SIZE = 10
management.call_command("captcha_create_pool", pool_size=POOL_SIZE, verbosity=0)
self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
def test_empty_pool_fallback(self):
__current_test_get_from_pool_setting = settings.CAPTCHA_GET_FROM_POOL
settings.CAPTCHA_GET_FROM_POOL = True
CaptchaStore.objects.all().delete() # Delete objects created during SetUp
with LogCapture() as log:
CaptchaStore.pick()
log.check(
("captcha.models", "ERROR", "Couldn't get a captcha from pool, generating")
)
self.assertEqual(CaptchaStore.objects.count(), 1)
settings.CAPTCHA_GET_FROM_POOL = __current_test_get_from_pool_setting
def trivial_challenge():
return "trivial", "trivial"

View File

@ -0,0 +1,27 @@
from django.urls import include, re_path
from .views import (
test,
test_custom_error_message,
test_custom_generator,
test_id_prefix,
test_model_form,
test_non_required,
test_per_form_format,
)
urlpatterns = [
re_path(r"test/$", test, name="captcha-test"),
re_path(r"test-modelform/$", test_model_form, name="captcha-test-model-form"),
re_path(
r"test2/$", test_custom_error_message, name="captcha-test-custom-error-message"
),
re_path(r"test3/$", test_per_form_format, name="test_per_form_format"),
re_path(r"custom-generator/$", test_custom_generator, name="test_custom_generator"),
re_path(
r"test-non-required/$", test_non_required, name="captcha-test-non-required"
),
re_path(r"test-id-prefix/$", test_id_prefix, name="captcha-test-id-prefix"),
re_path(r"", include("captcha.urls")),
]

View File

@ -0,0 +1,128 @@
from django import forms
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.template import engines
from captcha.fields import CaptchaField
TEST_TEMPLATE = r"""
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>captcha test</title>
</head>
<body>
{% if passed %}
<p style="color:green">Form validated</p>
{% endif %}
{% if form.errors %}
{{form.errors}}
{% endif %}
<form action="{% url 'captcha-test' %}" method="post">
{{form.as_p}}
<p><input type="submit" value="Continue &rarr;"></p>
</form>
</body>
</html>
"""
def _get_template(template_string):
return engines["django"].from_string(template_string)
def _test(request, form_class):
passed = False
if request.POST:
form = form_class(request.POST)
if form.is_valid():
passed = True
else:
form = form_class()
t = _get_template(TEST_TEMPLATE)
return HttpResponse(
t.render(context=dict(passed=passed, form=form), request=request)
)
def test(request):
class CaptchaTestForm(forms.Form):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
captcha = CaptchaField(help_text="asdasd")
return _test(request, CaptchaTestForm)
def test_model_form(request):
class CaptchaTestModelForm(forms.ModelForm):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
captcha = CaptchaField(help_text="asdasd")
class Meta:
model = User
fields = ("subject", "sender", "captcha")
return _test(request, CaptchaTestModelForm)
def test_custom_generator(request):
class CaptchaTestModelForm(forms.ModelForm):
subject = forms.CharField(max_length=100)
sender = forms.EmailField()
captcha = CaptchaField(generator=lambda: ("111111", "111111"))
class Meta:
model = User
fields = ("subject", "sender", "captcha")
return _test(request, CaptchaTestModelForm)
def test_custom_error_message(request):
class CaptchaTestErrorMessageForm(forms.Form):
captcha = CaptchaField(
help_text="asdasd", error_messages=dict(invalid="TEST CUSTOM ERROR MESSAGE")
)
return _test(request, CaptchaTestErrorMessageForm)
def test_per_form_format(request):
class CaptchaTestFormatForm(forms.Form):
captcha = CaptchaField(
help_text="asdasd",
error_messages=dict(invalid="TEST CUSTOM ERROR MESSAGE"),
output_format=(
"%(image)s testPerFieldCustomFormatString "
"%(hidden_field)s %(text_field)s"
),
)
return _test(request, CaptchaTestFormatForm)
def test_non_required(request):
class CaptchaTestForm(forms.Form):
sender = forms.EmailField()
subject = forms.CharField(max_length=100)
captcha = CaptchaField(help_text="asdasd", required=False)
return _test(request, CaptchaTestForm)
def test_id_prefix(request):
class CaptchaTestForm(forms.Form):
sender = forms.EmailField()
subject = forms.CharField(max_length=100)
captcha1 = CaptchaField(id_prefix="form1")
captcha2 = CaptchaField(id_prefix="form2")
return _test(request, CaptchaTestForm)

View File

@ -0,0 +1,21 @@
from django.urls import re_path
from captcha import views
urlpatterns = [
re_path(
r"image/(?P<key>\w+)/$",
views.captcha_image,
name="captcha-image",
kwargs={"scale": 1},
),
re_path(
r"image/(?P<key>\w+)@2/$",
views.captcha_image,
name="captcha-image-2x",
kwargs={"scale": 2},
),
re_path(r"audio/(?P<key>\w+).wav$", views.captcha_audio, name="captcha-audio"),
re_path(r"refresh/$", views.captcha_refresh, name="captcha-refresh"),
]

View File

@ -0,0 +1,225 @@
import json
import os
import random
import subprocess
import tempfile
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from ranged_response import RangedFileResponse
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404, HttpResponse
from captcha.conf import settings
from captcha.helpers import captcha_audio_url, captcha_image_url
from captcha.models import CaptchaStore
# Distance of the drawn text from the top of the captcha image
DISTANCE_FROM_TOP = 4
def getsize(font, text):
if hasattr(font, "getbbox"):
_top, _left, _right, _bottom = font.getbbox(text)
return _right - _left, _bottom - _top
elif hasattr(font, "getoffset"):
return tuple([x + y for x, y in zip(font.getsize(text), font.getoffset(text))])
else:
return font.getsize(text)
def makeimg(size):
if settings.CAPTCHA_BACKGROUND_COLOR == "transparent":
image = Image.new("RGBA", size)
else:
image = Image.new("RGB", size, settings.CAPTCHA_BACKGROUND_COLOR)
return image
def captcha_image(request, key, scale=1):
if scale == 2 and not settings.CAPTCHA_2X_IMAGE:
raise Http404
try:
store = CaptchaStore.objects.get(hashkey=key)
except CaptchaStore.DoesNotExist:
# HTTP 410 Gone status so that crawlers don't index these expired urls.
return HttpResponse(status=410)
random.seed(key) # Do not generate different images for the same key
text = store.challenge
if isinstance(settings.CAPTCHA_FONT_PATH, str):
fontpath = settings.CAPTCHA_FONT_PATH
elif isinstance(settings.CAPTCHA_FONT_PATH, (list, tuple)):
fontpath = random.choice(settings.CAPTCHA_FONT_PATH)
else:
raise ImproperlyConfigured(
"settings.CAPTCHA_FONT_PATH needs to be a path to a font or list of paths to fonts"
)
if fontpath.lower().strip().endswith("ttf"):
font = ImageFont.truetype(fontpath, settings.CAPTCHA_FONT_SIZE * scale)
else:
font = ImageFont.load(fontpath)
if settings.CAPTCHA_IMAGE_SIZE:
size = settings.CAPTCHA_IMAGE_SIZE
else:
size = getsize(font, text)
size = (size[0] * 2, int(size[1] * 1.4))
image = makeimg(size)
xpos = 2
charlist = []
for char in text:
if char in settings.CAPTCHA_PUNCTUATION and len(charlist) >= 1:
charlist[-1] += char
else:
charlist.append(char)
for char in charlist:
fgimage = Image.new("RGB", size, settings.CAPTCHA_FOREGROUND_COLOR)
charimage = Image.new("L", getsize(font, " %s " % char), "#000000")
chardraw = ImageDraw.Draw(charimage)
chardraw.text((0, 0), " %s " % char, font=font, fill="#ffffff")
if settings.CAPTCHA_LETTER_ROTATION:
charimage = charimage.rotate(
random.randrange(*settings.CAPTCHA_LETTER_ROTATION),
expand=0,
resample=Image.BICUBIC,
)
charimage = charimage.crop(charimage.getbbox())
maskimage = Image.new("L", size)
maskimage.paste(
charimage,
(
xpos,
DISTANCE_FROM_TOP,
xpos + charimage.size[0],
DISTANCE_FROM_TOP + charimage.size[1],
),
)
size = maskimage.size
image = Image.composite(fgimage, image, maskimage)
xpos = xpos + 2 + charimage.size[0]
if settings.CAPTCHA_IMAGE_SIZE:
# centering captcha on the image
tmpimg = makeimg(size)
tmpimg.paste(
image,
(
int((size[0] - xpos) / 2),
int((size[1] - charimage.size[1]) / 2 - DISTANCE_FROM_TOP),
),
)
image = tmpimg.crop((0, 0, size[0], size[1]))
else:
image = image.crop((0, 0, xpos + 1, size[1]))
draw = ImageDraw.Draw(image)
for f in settings.noise_functions():
draw = f(draw, image)
for f in settings.filter_functions():
image = f(image)
out = BytesIO()
image.save(out, "PNG")
out.seek(0)
response = HttpResponse(content_type="image/png")
response.write(out.read())
response["Content-length"] = out.tell()
# At line :50 above we fixed the random seed so that we always generate the
# same image, see: https://github.com/mbi/django-simple-captcha/pull/194
# This is a problem though, because knowledge of the seed will let an attacker
# predict the next random (globally). We therefore reset the random here.
# Reported in https://github.com/mbi/django-simple-captcha/pull/221
random.seed()
return response
def captcha_audio(request, key):
if settings.CAPTCHA_FLITE_PATH:
try:
store = CaptchaStore.objects.get(hashkey=key)
except CaptchaStore.DoesNotExist:
# HTTP 410 Gone status so that crawlers don't index these expired urls.
return HttpResponse(status=410)
text = store.challenge
if "captcha.helpers.math_challenge" == settings.CAPTCHA_CHALLENGE_FUNCT:
text = text.replace("*", "times").replace("-", "minus").replace("+", "plus")
else:
text = ", ".join(list(text))
path = str(os.path.join(tempfile.gettempdir(), "%s.wav" % key))
subprocess.call([settings.CAPTCHA_FLITE_PATH, "-t", text, "-o", path])
# Add arbitrary noise if sox is installed
if settings.CAPTCHA_SOX_PATH:
arbnoisepath = str(
os.path.join(tempfile.gettempdir(), "%s_arbitrary.wav") % key
)
mergedpath = str(os.path.join(tempfile.gettempdir(), "%s_merged.wav") % key)
subprocess.call(
[
settings.CAPTCHA_SOX_PATH,
"-r",
"8000",
"-n",
arbnoisepath,
"synth",
"2",
"brownnoise",
"gain",
"-15",
]
)
subprocess.call(
[
settings.CAPTCHA_SOX_PATH,
"-m",
arbnoisepath,
path,
"-t",
"wavpcm",
"-b",
"16",
mergedpath,
]
)
os.remove(arbnoisepath)
os.remove(path)
os.rename(mergedpath, path)
if os.path.isfile(path):
response = RangedFileResponse(
request, open(path, "rb"), content_type="audio/wav"
)
response["Content-Disposition"] = 'attachment; filename="{}.wav"'.format(
key
)
return response
raise Http404
def captcha_refresh(request):
"""Return json with new captcha for ajax refresh request"""
if not request.headers.get("x-requested-with") == "XMLHttpRequest":
raise Http404
new_key = CaptchaStore.pick()
to_json_response = {
"key": new_key,
"image_url": captcha_image_url(new_key),
"audio_url": captcha_audio_url(new_key)
if settings.CAPTCHA_FLITE_PATH
else None,
}
return HttpResponse(json.dumps(to_json_response), content_type="application/json")

View File

@ -0,0 +1,10 @@
Metadata-Version: 2.1
Name: django-ranged-response
Version: 0.2.0
Summary: Modified Django FileResponse that adds Content-Range headers.
Home-page: https://github.com/wearespindle/django-ranged-fileresponse
Author: Spindle
Author-email: jeroen@wearespindle.com
License: MIT
Requires-Dist: django

View File

@ -0,0 +1,7 @@
django_ranged_response-0.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
django_ranged_response-0.2.0.dist-info/METADATA,sha256=zD0L0iFcE_b2_nSKGUFYT3eqSzdKZ6eRt1GNUuAa4ME,297
django_ranged_response-0.2.0.dist-info/RECORD,,
django_ranged_response-0.2.0.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
django_ranged_response-0.2.0.dist-info/top_level.txt,sha256=1Vr01kwR92rMMkPOVv4QTsqa-gt5dxCPiRcPM0Qahak,16
ranged_response/__init__.py,sha256=gURFm41igrS4xwZHO83i57ta6uXAxMPoJO9KgZLDpOg,5057
ranged_response/__pycache__/__init__.cpython-311.pyc,,

View File

@ -1,5 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.40.0)
Generator: bdist_wheel (0.41.3)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1 @@
ranged_response

View File

@ -0,0 +1,20 @@
Copyright (c) 2008 - 2014 Marco Bonetti
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,65 @@
Metadata-Version: 2.1
Name: django-simple-captcha
Version: 0.5.20
Summary: A very simple, yet powerful, Django captcha application
Home-page: https://github.com/mbi/django-simple-captcha
Author: Marco Bonetti
Author-email: mbonetti@gmail.com
License: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Security
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Framework :: Django
License-File: LICENSE
Requires-Dist: Django >=3.2
Requires-Dist: Pillow >=6.2.0
Requires-Dist: django-ranged-response ==0.2.0
Provides-Extra: test
Requires-Dist: testfixtures ; extra == 'test'
*********************
Django Simple Captcha
*********************
.. image:: https://github.com/mbi/django-simple-captcha/actions/workflows/test.yml/badge.svg
:target: https://github.com/mbi/django-simple-captcha/actions/workflows/test.yml
.. image:: https://img.shields.io/pypi/v/django-simple-captcha
:target: https://pypi.org/project/django-simple-captcha/
.. image:: https://img.shields.io/pypi/l/django-simple-captcha
:target: https://github.com/mbi/django-simple-captcha/blob/master/LICENSE
Django Simple Captcha is an extremely simple, yet highly customizable Django application to add captcha images to any Django form.
.. image:: http://django-simple-captcha.readthedocs.io/en/latest/_images/captcha3.png
Features
++++++++
* Very simple to setup and deploy, yet very configurable
* Can use custom challenges (e.g. random chars, simple math, dictionary word, ...)
* Custom generators, noise and filter functions alter the look of the generated image
* Supports text-to-speech audio output of the challenge text, for improved accessibility
* Ajax refresh
Requirements
++++++++++++
* Django 3.2+, Python3.8+
* A recent version of the Pillow compiled with FreeType support
* Flite is required for text-to-speech (audio) output, but not mandatory
Documentation
+++++++++++++
Read the `documentation online <http://django-simple-captcha.readthedocs.org/en/latest/>`_.

View File

@ -0,0 +1,92 @@
captcha/__init__.py,sha256=Cz2BKUx-KbJ2yLXHZ9JdfVIsLBP2ugtIoCK65Ywx79Q,140
captcha/__pycache__/__init__.cpython-311.pyc,,
captcha/__pycache__/fields.cpython-311.pyc,,
captcha/__pycache__/helpers.cpython-311.pyc,,
captcha/__pycache__/models.cpython-311.pyc,,
captcha/__pycache__/urls.cpython-311.pyc,,
captcha/__pycache__/views.cpython-311.pyc,,
captcha/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
captcha/conf/__pycache__/__init__.cpython-311.pyc,,
captcha/conf/__pycache__/settings.cpython-311.pyc,,
captcha/conf/settings.py,sha256=AH5rdbUoZWqRbZO8KpI2EChKa6SKepS-KLRFPPhP9w4,3845
captcha/fields.py,sha256=DEaPQ8CVjuhFowDVNXAoOQI7L0WN2lfh_X9NctVSOGQ,9889
captcha/fonts/COPYRIGHT.TXT,sha256=ICRpD7nb_bF8U2uD1MYwp2Dt1ROoeK5hihPPWG-iwWw,5950
captcha/fonts/README.TXT,sha256=J3YHBi0jkpUjqB4vMrfoQ08MAUhr-gbngH8q1SVYXc8,321
captcha/fonts/Vera.ttf,sha256=xMRWkLNFQ1ssulLsq-J18F5Js4mzn-aK0Dr7tVEojT0,65932
captcha/helpers.py,sha256=LWDyxeYjPGiXQCZWIlxChJR_0t8wrOuyDbon5FQhAV8,3069
captcha/jinja2/captcha/widgets/captcha.html,sha256=UNvyLbRvQCkGp_AbOKrrfy1d6iXF1fozqxiMw8fRm9I,186
captcha/locale/bg/LC_MESSAGES/django.mo,sha256=UAkafercICutRXyYrBeExNF0-KrzLJ-tw2OK5RNkMJo,653
captcha/locale/bg/LC_MESSAGES/django.po,sha256=5o14ZmKzCmfdhCpiRyb-fVBkwldIc_NBNaDLVCA2AuI,1060
captcha/locale/cs/LC_MESSAGES/django.mo,sha256=AHs5edgI79WL2VACnNTmcnSaN7GC2WCEbJBl7zp5vgo,597
captcha/locale/cs/LC_MESSAGES/django.po,sha256=hJcBhAQZdFnZpqZwhpuDgaEzrEl-zY2ANY-kBX0tSTY,950
captcha/locale/de/LC_MESSAGES/django.mo,sha256=5hZSrteOGmsf6UX0F-sPiL5ZLRjYmc2mWHxJ601_TJ8,634
captcha/locale/de/LC_MESSAGES/django.po,sha256=HnWs3cwc2AFekknV2vgw-bJVWNbnKNmgS7cvjIMZYO0,1002
captcha/locale/en/LC_MESSAGES/django.mo,sha256=oOYm0ZDZinVXRs-9KaqtbNz3opWjc6-psC9rgOwc-x4,591
captcha/locale/en/LC_MESSAGES/django.po,sha256=FGoY9PDI0B1TsSBy6ytnA_oJoPpRjZg8L8P-W4A0fJ4,942
captcha/locale/es/LC_MESSAGES/django.mo,sha256=qWdpPZe45meLuxcoKbDslGn0-UFoZt8hrYfIGZaJgDU,600
captcha/locale/es/LC_MESSAGES/django.po,sha256=Gk9coJCwhogv0JQXBKxzhpGqPbS9Q8-68pl-wNWpA5U,968
captcha/locale/fa/LC_MESSAGES/django.mo,sha256=KNb_G6fhiwjjBjWO2u5jVeUgRxJD-KX91vPIjuGKYXg,688
captcha/locale/fa/LC_MESSAGES/django.po,sha256=A2OKIY9h8K9CE948-V8UsnfBU6wAXUzlwn_hcHylGDA,1043
captcha/locale/fr/LC_MESSAGES/django.mo,sha256=QtmIqpiyxFQjYRBuEn53EDSLdP63jF7M8v9Hz600vLM,594
captcha/locale/fr/LC_MESSAGES/django.po,sha256=ppkwxlKg-ATrDFHAKQUfzTbDJyw2h19pHH8YSxitTIk,945
captcha/locale/it/LC_MESSAGES/django.mo,sha256=WTElquRQcavHoP7tsERWvCKR6XSxwI3iat8DIFD48xQ,631
captcha/locale/it/LC_MESSAGES/django.po,sha256=wTwLs5xko7UQvXRhud143j4Q9zrQUZ29G1GscMyL_ZE,994
captcha/locale/ja/LC_MESSAGES/django.mo,sha256=x8M-JHYPyARaiWQd8yGXXsYU86jFIbbWdeTjr_wxEeM,636
captcha/locale/ja/LC_MESSAGES/django.po,sha256=Uqbys4Sr96GfYR_Nx6GX2-y6qePmZYrB8SKB4Q4D6QY,1007
captcha/locale/nl/LC_MESSAGES/django.mo,sha256=A32eapFCLUVGyYWcDTTfvvYL30AUvkdCRhwAtccpuKU,630
captcha/locale/nl/LC_MESSAGES/django.po,sha256=kuQWY1li7KIcF6AXaMZtFBMLHyjLhgpWkg6pPmXpYcA,1024
captcha/locale/pl/LC_MESSAGES/django.mo,sha256=RY3o7PISCB5dfi3PNWl1MsJzTCDXZop3l4vfV9xnpFk,680
captcha/locale/pl/LC_MESSAGES/django.po,sha256=sGEUv_GUFrlNyozScmtUrN_-ZH4voW8t6CaHWC0rSLo,1107
captcha/locale/pt_BR/LC_MESSAGES/django.mo,sha256=Y0dwoL_ndMq0YlAu0IxEoSU76YsOUl0MULUOvJCcaUY,613
captcha/locale/pt_BR/LC_MESSAGES/django.po,sha256=5xq-imJL5oNDDDRbQuEkny1bPS4RN3LY8H1GGV2unAs,1025
captcha/locale/ru/LC_MESSAGES/django.mo,sha256=PObABAXyLMMPrOM-_J3GqvcGlLN_raQCFuOvNOWvK78,724
captcha/locale/ru/LC_MESSAGES/django.po,sha256=7CEb3GZO1v2wh7A2pvx8bz0Pvzj0HI8Xw5reijJzTao,1053
captcha/locale/sk/LC_MESSAGES/django.mo,sha256=gWwIkEnmh7MsK0zGVdcDEqnqoNl81D4yu0kHvLra26o,606
captcha/locale/sk/LC_MESSAGES/django.po,sha256=1USBxP1l_jvIIHswSylarYJIrryz3F-ZqHlXLOTC-PM,994
captcha/locale/sv/LC_MESSAGES/django.mo,sha256=kiovetWFjPSKhhu0lpQ0tNPuySJXiiiOvX0LeliaZiw,631
captcha/locale/sv/LC_MESSAGES/django.po,sha256=GiRNXRUeFMK29rMKZW5iAwX9MJO9tJuWMRT5-AlWk_A,991
captcha/locale/tr/LC_MESSAGES/django.mo,sha256=Mw0UkeFXwFa_DxVTbfta488L-1pSJ-BW33jx69z2pZU,605
captcha/locale/tr/LC_MESSAGES/django.po,sha256=2dSjovuso9lnHyEMC0tFHzRlDh_Njh_VuRjWb4LdEI0,978
captcha/locale/uk/LC_MESSAGES/django.mo,sha256=FQoQB-hoVxdAeBQYwxMy_wxD3wXlOLxpwuakWIr5YQs,715
captcha/locale/uk/LC_MESSAGES/django.po,sha256=qGaetu4TkmMjCnHocFnN5fmYFqST5NpNgSHb3UH2TFA,1082
captcha/locale/zh_CN/LC_MESSAGES/django.mo,sha256=1KxboU5k5qQ0mTleJz7a3iLLhLLajHPAKl5sGwWxGVs,602
captcha/locale/zh_CN/LC_MESSAGES/django.po,sha256=jHpBPXBY5bn_IhhJAOj9EZPO8hnw3Uz_uQJvjnPQy9M,1004
captcha/locale/zh_Hans/LC_MESSAGES/django.mo,sha256=1KxboU5k5qQ0mTleJz7a3iLLhLLajHPAKl5sGwWxGVs,602
captcha/locale/zh_Hans/LC_MESSAGES/django.po,sha256=jHpBPXBY5bn_IhhJAOj9EZPO8hnw3Uz_uQJvjnPQy9M,1004
captcha/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
captcha/management/__pycache__/__init__.cpython-311.pyc,,
captcha/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
captcha/management/commands/__pycache__/__init__.cpython-311.pyc,,
captcha/management/commands/__pycache__/captcha_clean.cpython-311.pyc,,
captcha/management/commands/__pycache__/captcha_create_pool.cpython-311.pyc,,
captcha/management/commands/captcha_clean.py,sha256=w1PTkXEmdyf3xk7Q0uujqSBDaNao2poq-9b7yL751XQ,894
captcha/management/commands/captcha_create_pool.py,sha256=tYmZeN9JmBh0HF0Fni2Bt4RNRpRm_LM6U_QMcTWZTL0,1089
captcha/migrations/0001_initial.py,sha256=sQj9d3qSatZ0xBF8vOaWj_mCnaOvnQilwUA21XNtQFE,893
captcha/migrations/0002_alter_captchastore_id.py,sha256=x5KnL0ZgRJkmPdqDhct2yMyBG9qydQszH4Wy0_arOUY,398
captcha/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
captcha/migrations/__pycache__/0001_initial.cpython-311.pyc,,
captcha/migrations/__pycache__/0002_alter_captchastore_id.cpython-311.pyc,,
captcha/migrations/__pycache__/__init__.cpython-311.pyc,,
captcha/models.py,sha256=a0L5W6Pc_lFSwD8vPV7pX7uhs2xodKE7LNXmealJpdo,2817
captcha/templates/captcha/field.html,sha256=8IaHIZudDm3WP2y_AmA8AOsvXg4y6QSWqlOrbA1aGD4,40
captcha/templates/captcha/hidden_field.html,sha256=i5aTo_jXDRNr0nhzpQ13fFC_r-n0nE9gLhDzAfuz3ow,72
captcha/templates/captcha/image.html,sha256=HMATyrCVuw3RCONGqeY-uxeHRbxGq2E0LjoJmdq0_R0,232
captcha/templates/captcha/text_field.html,sha256=V9GfTCtTD4ByEZbuXPMNDL7JH6E-AXgI_P_94ohWSck,131
captcha/templates/captcha/widgets/captcha.html,sha256=RHOTJoRIdCheT2tQnf-99_eA_AXJctW9JJfhy4X04AM,314
captcha/tests/__init__.py,sha256=PnIM4lAxWFf0dhvr9DobN1qgW7-qmuwdWUwhOoqEFlE,58
captcha/tests/__pycache__/__init__.cpython-311.pyc,,
captcha/tests/__pycache__/tests.cpython-311.pyc,,
captcha/tests/__pycache__/urls.cpython-311.pyc,,
captcha/tests/__pycache__/views.cpython-311.pyc,,
captcha/tests/tests.py,sha256=RgOQxsnyw3__utoogJNeN2xXBT7uZ87k13AV5kh_fJ4,24079
captcha/tests/urls.py,sha256=9YwNDZ8zw_QooLiSn2niXIrJTrJcLXsV8AYI8onXzpw,878
captcha/tests/views.py,sha256=DA-zqplkVGd2IQn7wqP5A2aE1z_7EW6G5qeLnYjTbdw,3563
captcha/urls.py,sha256=r3e50R7iEd7NSW4p5jxC_TiX40Y3CqVIsksDLHHEO8k,524
captcha/views.py,sha256=zKIiAperFrkr9MXsR6Hyh7Xx6AJFHgZUGEvMZSI0188,7409
django_simple_captcha-0.5.20.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
django_simple_captcha-0.5.20.dist-info/LICENSE,sha256=7F9KedNcOJEE-RE8ATQT_b6ISsXIqVKoc03GdlkQzA8,1064
django_simple_captcha-0.5.20.dist-info/METADATA,sha256=ZQaTYeyPbeX5qhQeDpoqhhzaeJaMSpyBtin8mRIOuTs,2365
django_simple_captcha-0.5.20.dist-info/RECORD,,
django_simple_captcha-0.5.20.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
django_simple_captcha-0.5.20.dist-info/WHEEL,sha256=iYlv5fX357PQyRT2o6tw1bN-YcKFFHKqB_LwHO5wP-g,110
django_simple_captcha-0.5.20.dist-info/top_level.txt,sha256=krw23fzzUC0oTgsEakKehDWjBfNzFYPTtC0hVpktXyQ,8

View File

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.41.2)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

Some files were not shown because too many files have changed in this diff Show More