From 96c124f4377da825491cc14eefc63ce0efaa728c Mon Sep 17 00:00:00 2001 From: Ivan Redun Date: Tue, 19 Jan 2021 03:47:41 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=A1=D1=82=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Основная часть: - Сделаны базовые методы для отправки смс - Заведена в админку возможность смотреть за логами смс --- .gitignore | 118 +++ LICENSE | 29 + MANIFEST.in | 3 + README.md | 48 + setup.cfg | 31 + setup.py | 3 + smsru/__init__.py | 0 smsru/admin.py | 30 + smsru/apps.py | 5 + smsru/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 559 bytes smsru/locale/ru/LC_MESSAGES/django.po | 1206 ++++++++++++++++++++++ smsru/management/__init__.py | 0 smsru/management/commands/__init__.py | 0 smsru/management/commands/send-sms-ru.py | 26 + smsru/migrations/0001_initial.py | 33 + smsru/migrations/__init__.py | 0 smsru/models.py | 18 + smsru/service.py | 134 +++ smsru/tests.py | 3 + smsru/urls.py | 9 + smsru/views.py | 18 + 21 files changed, 1714 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 smsru/__init__.py create mode 100644 smsru/admin.py create mode 100644 smsru/apps.py create mode 100644 smsru/locale/ru/LC_MESSAGES/django.mo create mode 100644 smsru/locale/ru/LC_MESSAGES/django.po create mode 100644 smsru/management/__init__.py create mode 100644 smsru/management/commands/__init__.py create mode 100644 smsru/management/commands/send-sms-ru.py create mode 100644 smsru/migrations/0001_initial.py create mode 100644 smsru/migrations/__init__.py create mode 100644 smsru/models.py create mode 100644 smsru/service.py create mode 100644 smsru/tests.py create mode 100644 smsru/urls.py create mode 100644 smsru/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3649c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +.idea/* +.vscode/* + +# Distribution / packaging +.Python +env/ +venv/ +tmp/ +build/ +develop-eggs/ +dist/gi +static/ +!**/api/static +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +.vscode +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store +*.sqlite3 +media/ +*.pyc +*.db +*.pid + +.env.dev +.env.prod diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59f7126 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Redun Ivan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..88c8bf7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +recursive-include smsru/locale * \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a1aed9 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Django SMS.RU + + +Приложение Django для быстрой интеграции API сервиса [sms.ru](https://sms.ru/?panel=api) + +Быстрый старт +----------- + +1. Добавьте `smsru` в INSTALLED_APPS: +``` + INSTALLED_APPS = [ + ... + 'smsru', + ] +``` +2. В настройках так же следует добавить параметр `SMS_RU`: +``` +SMS_RU = { + "API_ID": '', # если указан API ключ, логин и пароль пропускаем + "LOGIN": '', # если нет API, то авторизуемся чезер логин и пароль + "PASSWORD": '', + "TEST": True, # отправка смс в тестовом режиме, по умолчанию False + "SENDER": 'sms', # отправитель - необязательно поле + "PARTNER_ID": 1111 # ID партнера - необязательно поле +} +``` + +3. Добавьте в свой `urls.py` импорт URL (для работы callback, по желанию): +``` + path('smsru/', include('smsru.urls')) +``` + +4. Запустите ``python manage.py migrate`` для создания необъодимых таблиц. + +5. В админ панели вы сможете увидеть лог сообщений и запросить статус любого из них. + +6. Так же добавилась консольная команда для отправки смс +``` +python manage.py send-sms-ru --phone +79888888888 --msg Тест +``` + +# Использование библиотеки в коде +```python +from smsru.service import SmsRuApi +api = SmsRuApi() +result = api.send_one_sms("+79888888888", "Test") +# result: {'79888888888': True} +``` diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b1582b8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +name = django-smsru +version = 0.1.0 +description = Django app for sms.ru. +long_description = file: README.md +url = 'https://github.com/iredun/django-smsru' +author = 'Redun Ivan' +author_email = redunivan@yandex.ru +license = BSD 3-Clause License +install_requires = + requests + Django +classifiers = + Environment :: Web Environment + Framework :: Django + Framework :: Django :: 3.0 + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Topic :: Internet :: WWW/HTTP + Topic :: Internet :: WWW/HTTP :: Dynamic Content + +[options] +include_package_data = true +packages = find: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/smsru/__init__.py b/smsru/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsru/admin.py b/smsru/admin.py new file mode 100644 index 0000000..770d9f3 --- /dev/null +++ b/smsru/admin.py @@ -0,0 +1,30 @@ +from typing import List + +from django.contrib import admin +from smsru.models import Log + +from django.utils.translation import gettext as _ + +from smsru.service import SmsRuApi + + +def update_sms_status(model, request, queryset): + api = SmsRuApi() + for item in queryset: + if item.sms_id: + result, data = api.get_status(item.sms_id) + if result: + item.status = data['status'] + item.status_code = data['status_code'] + item.status_text = data['status_text'] + item.save() + + +update_sms_status.short_description = _("Update selected sms status") + + +@admin.register(Log) +class LogAdmin(admin.ModelAdmin): + list_display = ['phone', 'sms_id', 'msg', 'status', 'status_text', 'created_at'] + date_hierarchy = 'created_at' + actions = [update_sms_status] diff --git a/smsru/apps.py b/smsru/apps.py new file mode 100644 index 0000000..d4cd628 --- /dev/null +++ b/smsru/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SmsruConfig(AppConfig): + name = 'smsru' diff --git a/smsru/locale/ru/LC_MESSAGES/django.mo b/smsru/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..8dad03b67d3632cd09d40693a5a4c5df2de678fb GIT binary patch literal 559 zcmYLEU2oGc6dhtudE}Xgiv$zZ@UYV&+BEJKT6aVwO_ipT;LWrq3(D-uanUC2Z|v{w zU+kTm?xR;a^0n_d=N{j`IlMc-dWCq4c#n9E_=ISyk9ff00MC~5Qgdtms+A-0np`N` zR7M9dkH$Zua{(73U&PtW%hkqMN7AOO*5uo^Y)D}O@a9=T##Tv(A~KOq1>jSk5y}V~ z0PP0;*JIkH6p18P|EkvfiT6`PIY~1ziQsz{r19~uY$m)!HjWfl>ZX>?*Z`)>B!O9! z3J66S#mQMTndD-z7$H@cTUjbnD0vk?63xz+ZR9XX&g$yK*mBRosA-&v{r}hr^~Od! zrE%TO26H=g?LKd6S?M9HE~Rahj*>yNTT=#^Plx(uNIyK>&V-db6csj*j0 z0D7~#HoP~4$CiK89~kAF!O;=4Eq%usgHxb@bi9wa|2ztLuWfe<8ZsYlZyzNi*6W~z kJxMsH?P$jv@h^M%p!4#MzPRRjXUNgv6nr%3c81=;fAk=cw*UYD literal 0 HcmV?d00001 diff --git a/smsru/locale/ru/LC_MESSAGES/django.po b/smsru/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..14cfe08 --- /dev/null +++ b/smsru/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,1206 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-01-19 03:27+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: .\venv\Lib\site-packages\django\contrib\messages\apps.py:7 +msgid "Messages" +msgstr "" + +#: .\venv\Lib\site-packages\django\contrib\sitemaps\apps.py:7 +msgid "Site Maps" +msgstr "" + +#: .\venv\Lib\site-packages\django\contrib\staticfiles\apps.py:9 +msgid "Static Files" +msgstr "" + +#: .\venv\Lib\site-packages\django\contrib\syndication\apps.py:7 +msgid "Syndication" +msgstr "" + +#: .\venv\Lib\site-packages\django\core\paginator.py:48 +msgid "That page number is not an integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\core\paginator.py:50 +msgid "That page number is less than 1" +msgstr "" + +#: .\venv\Lib\site-packages\django\core\paginator.py:55 +msgid "That page contains no results" +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:20 +msgid "Enter a valid value." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:91 +#: .\venv\Lib\site-packages\django\forms\fields.py:671 +msgid "Enter a valid URL." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:145 +msgid "Enter a valid integer." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:156 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: .\venv\Lib\site-packages\django\core\validators.py:230 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:237 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:246 +#: .\venv\Lib\site-packages\django\core\validators.py:266 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:251 +#: .\venv\Lib\site-packages\django\core\validators.py:267 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:261 +#: .\venv\Lib\site-packages\django\core\validators.py:265 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:295 +msgid "Enter only digits separated by commas." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:301 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:334 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:343 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:353 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\core\validators.py:368 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\core\validators.py:387 +#: .\venv\Lib\site-packages\django\forms\fields.py:292 +#: .\venv\Lib\site-packages\django\forms\fields.py:327 +msgid "Enter a number." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:389 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\core\validators.py:394 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\core\validators.py:399 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\core\validators.py:461 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\core\validators.py:513 +msgid "Null characters are not allowed." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\base.py:1190 +#: .\venv\Lib\site-packages\django\forms\models.py:760 +msgid "and" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\base.py:1192 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:100 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:101 +msgid "This field cannot be null." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:102 +msgid "This field cannot be blank." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:103 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:107 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:126 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:939 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:940 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:942 +msgid "Boolean (Either True or False)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:983 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1047 +msgid "Comma-separated integers" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1096 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1098 +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1101 +msgid "Date (without time)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1243 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1247 +msgid "Date (with time)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1395 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1397 +msgid "Decimal number" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1536 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1539 +msgid "Duration" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1589 +msgid "Email address" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1612 +msgid "File path" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1678 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1680 +msgid "Floating point number" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1718 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1720 +msgid "Integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1803 +msgid "Big (8 byte) integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1819 +msgid "IPv4 address" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1850 +msgid "IP address" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1930 +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1931 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1933 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1976 +msgid "Positive big integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:1989 +msgid "Positive integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2002 +msgid "Positive small integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2016 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2048 +msgid "Small integer" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2055 +msgid "Text" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2083 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2085 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2088 +msgid "Time" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2214 +msgid "URL" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2236 +msgid "Raw binary data" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2301 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\__init__.py:2303 +msgid "Universally unique identifier" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\files.py:231 +msgid "File" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\files.py:379 +msgid "Image" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\json.py:18 +msgid "A JSON object" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\json.py:20 +msgid "Value must be valid JSON." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:790 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:792 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:1045 +msgid "One-to-one relationship" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:1099 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:1100 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: .\venv\Lib\site-packages\django\db\models\fields\related.py:1142 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: .\venv\Lib\site-packages\django\forms\boundfield.py:150 +msgid ":?.!" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:54 +msgid "This field is required." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:247 +msgid "Enter a whole number." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:398 +#: .\venv\Lib\site-packages\django\forms\fields.py:1139 +msgid "Enter a valid date." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:422 +#: .\venv\Lib\site-packages\django\forms\fields.py:1140 +msgid "Enter a valid time." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:450 +msgid "Enter a valid date/time." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:484 +msgid "Enter a valid duration." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:485 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:545 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:546 +msgid "No file was submitted." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:547 +msgid "The submitted file is empty." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:549 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:552 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:613 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:775 +#: .\venv\Lib\site-packages\django\forms\fields.py:865 +#: .\venv\Lib\site-packages\django\forms\models.py:1296 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:866 +#: .\venv\Lib\site-packages\django\forms\fields.py:981 +#: .\venv\Lib\site-packages\django\forms\models.py:1295 +msgid "Enter a list of values." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:982 +msgid "Enter a complete value." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:1198 +msgid "Enter a valid UUID." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\fields.py:1228 +msgid "Enter a valid JSON." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: .\venv\Lib\site-packages\django\forms\forms.py:78 +msgid ":" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\forms.py:205 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\forms\formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\forms\formsets.py:379 +#: .\venv\Lib\site-packages\django\forms\formsets.py:386 +msgid "Order" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\formsets.py:391 +msgid "Delete" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:755 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:759 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:765 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:774 +msgid "Please correct the duplicate values below." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:1096 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:1180 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\models.py:1298 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\utils.py:167 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:398 +msgid "Clear" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:399 +msgid "Currently" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:400 +msgid "Change" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:709 +msgid "Unknown" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:710 +msgid "Yes" +msgstr "" + +#: .\venv\Lib\site-packages\django\forms\widgets.py:711 +msgid "No" +msgstr "" + +#. Translators: Please do not add spaces around commas. +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:790 +msgid "yes,no,maybe" +msgstr "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:819 +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:836 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:838 +#, python-format +msgid "%s KB" +msgstr "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:840 +#, python-format +msgid "%s MB" +msgstr "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:842 +#, python-format +msgid "%s GB" +msgstr "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:844 +#, python-format +msgid "%s TB" +msgstr "" + +#: .\venv\Lib\site-packages\django\template\defaultfilters.py:846 +#, python-format +msgid "%s PB" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:65 +msgid "p.m." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:66 +msgid "a.m." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:71 +msgid "PM" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:72 +msgid "AM" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:149 +msgid "midnight" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dateformat.py:151 +msgid "noon" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:6 +msgid "Monday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:6 +msgid "Tuesday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:6 +msgid "Wednesday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:6 +msgid "Thursday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:6 +msgid "Friday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:7 +msgid "Saturday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:7 +msgid "Sunday" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:10 +msgid "Mon" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:10 +msgid "Tue" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:10 +msgid "Wed" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:10 +msgid "Thu" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:10 +msgid "Fri" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:11 +msgid "Sat" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:11 +msgid "Sun" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "January" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "February" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "March" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "April" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "May" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:14 +msgid "June" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:15 +msgid "July" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:15 +msgid "August" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:15 +msgid "September" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:15 +msgid "October" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:15 +msgid "November" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:16 +msgid "December" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "jan" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "feb" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "mar" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "apr" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "may" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:19 +msgid "jun" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "jul" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "aug" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "sep" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "oct" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "nov" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:20 +msgid "dec" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:25 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:39 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:45 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\dates.py:48 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\ipv6.py:8 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\text.py:70 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\text.py:236 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: .\venv\Lib\site-packages\django\utils\text.py:255 +#: .\venv\Lib\site-packages\django\utils\timesince.py:83 +msgid ", " +msgstr "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\utils\timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:41 +msgid "No year specified" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:61 +#: .\venv\Lib\site-packages\django\views\generic\dates.py:111 +#: .\venv\Lib\site-packages\django\views\generic\dates.py:208 +msgid "Date out of range" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:90 +msgid "No month specified" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:142 +msgid "No day specified" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:188 +msgid "No week specified" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:338 +#: .\venv\Lib\site-packages\django\views\generic\dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\detail.py:54 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\generic\list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\static.py:42 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:384 +msgid "Django Documentation" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:397 +msgid "Get started with Django" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:408 +msgid "Django Community" +msgstr "" + +#: .\venv\Lib\site-packages\django\views\templates\default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "" diff --git a/smsru/management/__init__.py b/smsru/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsru/management/commands/__init__.py b/smsru/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsru/management/commands/send-sms-ru.py b/smsru/management/commands/send-sms-ru.py new file mode 100644 index 0000000..8616211 --- /dev/null +++ b/smsru/management/commands/send-sms-ru.py @@ -0,0 +1,26 @@ +from django.core.management import BaseCommand +from smsru.service import SmsRuApi + + +class Command(BaseCommand): + help = 'Send SMS' + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument( + '--phone', dest='phone', default=None, + help='Phone number', + ) + parser.add_argument( + '--msg', dest='msg', default=None, + help='Message text', + ) + + def handle(self, *args, **options): + phone = options.get('phone') + msg = options.get('msg') + + api = SmsRuApi() + result = api.send_one_sms(phone, msg) + + self.stdout.write(f"Result: {result}") diff --git a/smsru/migrations/0001_initial.py b/smsru/migrations/0001_initial.py new file mode 100644 index 0000000..9469cb1 --- /dev/null +++ b/smsru/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.5 on 2021-01-18 23:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Log', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', models.CharField(max_length=30, verbose_name='Phone number')), + ('msg', models.TextField(verbose_name='Message')), + ('status', models.CharField(max_length=10, verbose_name='Status')), + ('status_code', models.IntegerField(verbose_name='Status code')), + ('status_text', models.TextField(blank=True, null=True, verbose_name='Status text')), + ('sms_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='SMS ID')), + ('cost', models.FloatField(blank=True, null=True, verbose_name='Cost')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ], + options={ + 'verbose_name': 'Logs', + 'verbose_name_plural': 'Log', + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/smsru/migrations/__init__.py b/smsru/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smsru/models.py b/smsru/models.py new file mode 100644 index 0000000..a08ec9e --- /dev/null +++ b/smsru/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.utils.translation import gettext as _ + + +class Log(models.Model): + phone = models.CharField(verbose_name=_('Phone number'), max_length=30) + msg = models.TextField(verbose_name=_('Message')) + status = models.CharField(verbose_name=_('Status'), max_length=10) + status_code = models.IntegerField(verbose_name=_('Status code')) + status_text = models.TextField(verbose_name=_('Status text'), blank=True, null=True) + sms_id = models.CharField(verbose_name=_('SMS ID'), max_length=255, blank=True, null=True) + cost = models.FloatField(verbose_name=_('Cost'), blank=True, null=True) + created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True) + + class Meta: + verbose_name = _('Logs') + verbose_name_plural = _('Log') + ordering = ('-created_at', ) diff --git a/smsru/service.py b/smsru/service.py new file mode 100644 index 0000000..323abaf --- /dev/null +++ b/smsru/service.py @@ -0,0 +1,134 @@ +import hashlib +import re +import requests + +from django.conf import settings +from smsru.models import Log + + +class SmsRuApi: + __api_id = None + __login = None + __password = None + __is_test = False + __from = None + __partner_id = None + __api_url = 'https://sms.ru/sms/' + + def __init__(self): + self._get_settings() + + def _get_settings(self): + setting_key = 'SMS_RU' + sms_ru = getattr(settings, setting_key, None) + if sms_ru is None: + raise KeyError(f"Key {setting_key} not found in settings.py") + + self.__api_id = sms_ru.get('API_ID', None) + if self.__api_id is None: + self.__login = sms_ru.get('LOGIN', None) + self.__password = sms_ru.get('PASSWORD', None) + if self.__login is None or self.__password is None: + raise KeyError(f"LOGIN and PASSWORD not found in {setting_key}") + + self.__from = sms_ru.get('SENDER', None) + self.__is_test = sms_ru.get('TEST', False) + + @staticmethod + def beautify_phone(phone: str) -> str: + return re.sub(r"\D", "", phone) + + def __request(self, url: str, post_param: dict) -> dict: + if self.__api_id: + post_param['api_id'] = self.__api_id + else: + post_param['login'] = self.__login + post_param['password'] = self.__password + + post_param['json'] = 1 + + if self.__from: + post_param['from'] = self.__from + if self.__partner_id: + post_param['partner_id'] = self.__partner_id + + post_param['test'] = self.__is_test + + response = requests.post(url, data=post_param) + + data = response.json() + + return data + + def _sms_request(self, post_param: dict) -> dict: + if 'to' in post_param: + return_result = {k: False for k in post_param['to'].split(',')} + phone_msg = {k: post_param['msg'] for k in post_param['to'].split(',')} + else: + return_result = {k: False for k, v in post_param['multi'].items()} + phone_msg = post_param['multi'] + + url = self.__api_url + 'send' + data = self.__request(url, post_param) + + if isinstance(data, dict): + if data['status'] == 'OK' and data['status_code'] == 100: + for phone, result in data['sms'].items(): + return_result[phone] = result['status_code'] == 100 + + itm = Log( + phone=phone, + status=result["status"], + msg=phone_msg.get(phone, None), + status_code=result["status_code"], + status_text=result.get("status_text", None), + sms_id=result.get("sms_id", None), + cost=result.get("cost", None), + ) + itm.save() + + return return_result + + def send_one_sms(self, phone: str, msg: str) -> dict: + phone_beautify = self.beautify_phone(phone) + if phone_beautify: + post_param = { + 'to': phone_beautify, + 'msg': msg + } + return self._sms_request(post_param) + else: + raise Exception(f'Bad phone number {phone_beautify}') + + def get_status(self, sms_id: str): + post_param = { + 'sms_id': sms_id + } + url = self.__api_url + 'status' + data = self.__request(url, post_param) + + if isinstance(data, dict): + if data['status'] == 'OK' and data['status_code'] == 100: + if sms_id in data['sms']: + return True, data['sms'][sms_id] + return False, None + + def validate_callback(self, data: list, hash: str) -> bool: + my_hash = self.__api_id + "".join(data) + return hash == hashlib.sha256(my_hash.encode('utf-8')).hexdigest() + + def __send_multi_sms(self, phone_sms: dict) -> dict: + """ + Not tested func + """ + post_param = { + "multi": dict() + } + for phone, msg in phone_sms.items(): + phone_beautify = self.beautify_phone(phone) + if phone_beautify: + post_param['multi'][phone_beautify] = msg + + return self._sms_request(post_param) + + diff --git a/smsru/tests.py b/smsru/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/smsru/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/smsru/urls.py b/smsru/urls.py new file mode 100644 index 0000000..056513e --- /dev/null +++ b/smsru/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from smsru.views import sms_callback + +app_name = 'smsru' + +urlpatterns = [ + path('callback/sms/', sms_callback) +] diff --git a/smsru/views.py b/smsru/views.py new file mode 100644 index 0000000..ce7a634 --- /dev/null +++ b/smsru/views.py @@ -0,0 +1,18 @@ +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt + +from smsru.models import Log +from smsru.service import SmsRuApi + + +@csrf_exempt +def sms_callback(request): + if request.method == 'POST': + data = request.POST.getlist('data[]') + hash = request.POST.get('hash') + api = SmsRuApi() + if api.validate_callback(data, hash): + item = Log.objects.filter(sms_id=data[1]).first() + item.status_code = data[2] + item.save() + return HttpResponse(100)