Initial commit
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
|||||||
|
docs/
|
||||||
|
config/production.py
|
||||||
|
|
||||||
|
# Django #
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
db.sqlite3
|
||||||
|
media
|
||||||
|
|
||||||
|
# Backup files #
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# If you are using PyCharm #
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Python #
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
.pytest_cache/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery
|
||||||
|
celerybeat-schedule.*
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Sublime Text #
|
||||||
|
*.tmlanguage.cache
|
||||||
|
*.tmPreferences.cache
|
||||||
|
*.stTheme.cache
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# sftp configuration file
|
||||||
|
sftp-config.json
|
||||||
|
|
||||||
|
# Package control specific files Package
|
||||||
|
Control.last-run
|
||||||
|
Control.ca-list
|
||||||
|
Control.ca-bundle
|
||||||
|
Control.system-ca-bundle
|
||||||
|
GitHub.sublime-settings
|
||||||
|
|
||||||
|
# Visual Studio Code #
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for config project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Django settings for config project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 3.2.8.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from config.production import *
|
||||||
|
except ImportError:
|
||||||
|
EMAIL_HOST = 'smtp.yandex.ru'
|
||||||
|
DEFAULT_FROM_EMAIL = 'EMAIL@yandex.ru'
|
||||||
|
EMAIL_HOST_USER = 'EMAIL@yandex.ru'
|
||||||
|
EMAIL_HOST_PASSWORD = 'PASSWORD'
|
||||||
|
EMAIL_PORT = 587
|
||||||
|
EMAIL_USE_TLS = True
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-@d#6&o^vc864mfq#lr@twkepg-j@_)u$=o73kp*+jb=z7ce92w'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'sms_voting',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'config.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""config URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from sms_voting.views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('2weh4EFLACKvneYZLb3n/', admin.site.urls),
|
||||||
|
path('', PollListView.as_view(), name='poll-list'),
|
||||||
|
path('poll/<int:pk>/', PollView.as_view(), name='poll-update'),
|
||||||
|
path('VLkK3JkuqLJNgeUpQWyu/', sms_endpoint, name='sms-endpoint'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for config project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
asgiref==3.4.1
|
||||||
|
Django==3.2.8
|
||||||
|
pytz==2021.3
|
||||||
|
sqlparse==0.4.2
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import random
|
||||||
|
from itertools import cycle
|
||||||
|
from io import StringIO
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.http import FileResponse, HttpResponse
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db.models.functions import Length
|
||||||
|
from .models import Poll, Bulletin, Word
|
||||||
|
|
||||||
|
def generate_yes_word(n=5):
|
||||||
|
word = random.choice(Word.objects.annotate(word_len=Length('word')).filter(in_use=False,
|
||||||
|
word_len=n))
|
||||||
|
word.in_use = True
|
||||||
|
word.save()
|
||||||
|
return word
|
||||||
|
|
||||||
|
def generate_no_word():
|
||||||
|
return generate_yes_word(n=7)
|
||||||
|
|
||||||
|
def emails(s):
|
||||||
|
return [x.strip() for x in s.strip().split('\n') if x.strip()]
|
||||||
|
|
||||||
|
def generate_bulletins(poll):
|
||||||
|
poll.bulletin_set.all().delete()
|
||||||
|
for _ in range(poll.face_participants):
|
||||||
|
Bulletin(
|
||||||
|
yes_word=generate_yes_word(),
|
||||||
|
no_word=generate_no_word(),
|
||||||
|
face_participant=True,
|
||||||
|
in_poll=poll
|
||||||
|
).save()
|
||||||
|
for email in emails(poll.remote_participants):
|
||||||
|
Bulletin(
|
||||||
|
yes_word=generate_yes_word(),
|
||||||
|
no_word=generate_no_word(),
|
||||||
|
remote_participant=email,
|
||||||
|
in_poll=poll
|
||||||
|
).save()
|
||||||
|
|
||||||
|
def send_memos(poll):
|
||||||
|
for bulletin in poll.bulletin_set.filter(remote_participant__isnull=False):
|
||||||
|
email = bulletin.remote_participant
|
||||||
|
topic = "Памятка для тайного электронного голосования"
|
||||||
|
body = render_to_string("sms_voting/memo.html",
|
||||||
|
{"yes_word": str(bulletin.yes_word), "no_word": str(bulletin.no_word), "voting_number": "+7-977-000-46-92"}
|
||||||
|
)
|
||||||
|
send_email(email, topic, body)
|
||||||
|
def send_instructions(poll):
|
||||||
|
for email in emails(poll.remote_participants):
|
||||||
|
topic = "Инструкции для тайного электронного голосования"
|
||||||
|
body = render_to_string("sms_voting/instructions.html", {})
|
||||||
|
send_email(email, topic, body)
|
||||||
|
def download_report(poll):
|
||||||
|
ctx = {
|
||||||
|
'poll_date': poll.start,
|
||||||
|
'person': poll.person,
|
||||||
|
'jury_full_count': poll.jury_full_count,
|
||||||
|
'jury_add_count': poll.jury_add_count,
|
||||||
|
'jury_sum_count': poll.face_participants + len(emails(poll.remote_participants)),
|
||||||
|
'jury_face_count': poll.face_participants,
|
||||||
|
'jury_remote_count': len(emails(poll.remote_participants)),
|
||||||
|
'jury_doctor_count': poll.doctor_count,
|
||||||
|
'yes_votes': poll.yes_votes,
|
||||||
|
'no_votes': poll.no_votes,
|
||||||
|
}
|
||||||
|
body = render_to_string("sms_voting/report.html", ctx)
|
||||||
|
return HttpResponse(body)
|
||||||
|
body = pypandoc.convert_text(body, 'pdf', format='html')
|
||||||
|
return FileResponse(StringIO(body), filename=f'{poll.title}.pdf')
|
||||||
|
def download_memos(poll):
|
||||||
|
ctx = {
|
||||||
|
'bulletins': poll.bulletin_set.filter(face_participant=True),
|
||||||
|
"voting_number": "+7-977-000-46-92",
|
||||||
|
}
|
||||||
|
body = render_to_string("sms_voting/memos_face.html", ctx)
|
||||||
|
return HttpResponse(body)
|
||||||
|
body = pypandoc.convert_text(body, 'pdf', format='html')
|
||||||
|
return FileResponse(StringIO(body), filename=f'memos_{poll.title}.pdf')
|
||||||
|
|
||||||
|
def send_email(email, topic, body):
|
||||||
|
send_mail(
|
||||||
|
topic,
|
||||||
|
"",
|
||||||
|
None,
|
||||||
|
[email],
|
||||||
|
fail_silently=False,
|
||||||
|
html_message=body
|
||||||
|
)
|
||||||
|
|
||||||
|
def count_vote(sms):
|
||||||
|
word = sms.text.strip().lower()
|
||||||
|
word = Word.objects.get(word=word)
|
||||||
|
#b = Bulletin.objects.get(yes_word__exact=word, checked_by__isnull=True)
|
||||||
|
try:
|
||||||
|
b = word.in_bulletin_yes.get()
|
||||||
|
except:
|
||||||
|
b = None
|
||||||
|
if b and b.checked_by is None:
|
||||||
|
b.checked_by = sms
|
||||||
|
b.save()
|
||||||
|
poll = b.in_poll
|
||||||
|
poll.yes_votes += 1
|
||||||
|
poll.save()
|
||||||
|
print('YES:', word)
|
||||||
|
print('report:', poll.yes_votes, poll.no_votes)
|
||||||
|
else:
|
||||||
|
#b = Bulletin.objects.get(no_word__exact=word, checked_by__isnull=True)
|
||||||
|
try:
|
||||||
|
b = word.in_bulletin_no.get()
|
||||||
|
except:
|
||||||
|
b = None
|
||||||
|
if b and b.checked_by is None:
|
||||||
|
b.checked_by = sms
|
||||||
|
b.save()
|
||||||
|
poll = b.in_poll
|
||||||
|
poll.no_votes += 1
|
||||||
|
poll.save()
|
||||||
|
print('NO:', word)
|
||||||
|
print('report:', poll.yes_votes, poll.no_votes)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Poll, Bulletin
|
||||||
|
|
||||||
|
@admin.register(Poll)
|
||||||
|
class PollAdmin(admin.ModelAdmin):
|
||||||
|
fields = ('title', 'face_participants')
|
||||||
|
|
||||||
|
@admin.register(Bulletin)
|
||||||
|
class BulletinAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SmsVotingConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'sms_voting'
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 13:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Poll',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=1000)),
|
||||||
|
('face_participants', models.IntegerField()),
|
||||||
|
('is_open', models.BooleanField(default=True)),
|
||||||
|
('start', models.DateTimeField()),
|
||||||
|
('end', models.DateTimeField()),
|
||||||
|
('yes_votes', models.IntegerField()),
|
||||||
|
('no_votes', models.IntegerField()),
|
||||||
|
('real_participants', models.IntegerField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SMS',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('timestamp', models.DateTimeField()),
|
||||||
|
('number_from', models.CharField(max_length=20)),
|
||||||
|
('number_to', models.CharField(max_length=20)),
|
||||||
|
('text', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RemoteParticipant',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('email', models.CharField(max_length=255)),
|
||||||
|
('in_poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sms_voting.poll')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Bulletin',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('result', models.BooleanField(default=None, null=True)),
|
||||||
|
('yes_word', models.CharField(max_length=20)),
|
||||||
|
('no_word', models.CharField(max_length=20)),
|
||||||
|
('checked_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='sms_voting.sms')),
|
||||||
|
('in_poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sms_voting.poll')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 13:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='end',
|
||||||
|
field=models.DateTimeField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='no_votes',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='real_participants',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='start',
|
||||||
|
field=models.DateTimeField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='yes_votes',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 13:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0002_auto_20211107_1344'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='checked_by',
|
||||||
|
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='sms_voting.sms'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='end',
|
||||||
|
field=models.DateTimeField(default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='no_votes',
|
||||||
|
field=models.IntegerField(default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='real_participants',
|
||||||
|
field=models.IntegerField(default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='start',
|
||||||
|
field=models.DateTimeField(default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='yes_votes',
|
||||||
|
field=models.IntegerField(default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 15:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0003_auto_20211107_1350'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poll',
|
||||||
|
name='is_open',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='remote_participants',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='RemoteParticipant',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 15:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0004_auto_20211107_1506'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='end',
|
||||||
|
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='face_participants',
|
||||||
|
field=models.IntegerField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='no_votes',
|
||||||
|
field=models.IntegerField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='real_participants',
|
||||||
|
field=models.IntegerField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='remote_participants',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='start',
|
||||||
|
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='yes_votes',
|
||||||
|
field=models.IntegerField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 16:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0005_auto_20211107_1539'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='face_participant',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='remote_participant',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-07 17:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0006_auto_20211107_1619'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poll',
|
||||||
|
name='real_participants',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='absent_face_participants',
|
||||||
|
field=models.IntegerField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='absent_remote_participants',
|
||||||
|
field=models.IntegerField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='remote_participant',
|
||||||
|
field=models.CharField(default=None, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-10 02:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0007_auto_20211107_1737'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poll',
|
||||||
|
name='absent_face_participants',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poll',
|
||||||
|
name='absent_remote_participants',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='sms',
|
||||||
|
name='number_to',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='doctor_count',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='jury_add_count',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='jury_full_count',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poll',
|
||||||
|
name='person',
|
||||||
|
field=models.CharField(blank=True, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='end',
|
||||||
|
field=models.DateTimeField(blank=True, default=None, editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='face_participants',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='no_votes',
|
||||||
|
field=models.IntegerField(default=0, editable=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='remote_participants',
|
||||||
|
field=models.TextField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='start',
|
||||||
|
field=models.DateTimeField(blank=True, default=None, editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='yes_votes',
|
||||||
|
field=models.IntegerField(default=0, editable=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sms',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(auto_now_add=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-10 04:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0008_auto_20211110_0227'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Word',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('word', models.CharField(max_length=20)),
|
||||||
|
('in_use', models.BooleanField(default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-10 04:33
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def load_words(apps, schema_editor):
|
||||||
|
Word = apps.get_model('sms_voting', 'Word')
|
||||||
|
with open('sms_voting/migrations/wordlist') as f:
|
||||||
|
Word.objects.bulk_create(Word(word=word.strip()) for word in f)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0009_word'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(load_words)
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-10 05:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0010_add_word_list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='no_word',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_bulletin_no', to='sms_voting.word'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='yes_word',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_bulletin_yes', to='sms_voting.word'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-10 05:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sms_voting', '0011_auto_20211110_0502'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='bulletin',
|
||||||
|
name='result',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poll',
|
||||||
|
name='remote_participants',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
),
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
class SMS(models.Model):
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
number_from = models.CharField(max_length=20)
|
||||||
|
text = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Poll(models.Model):
|
||||||
|
title = models.CharField(max_length=1000)
|
||||||
|
person = models.CharField(max_length=255, blank=True)
|
||||||
|
jury_full_count = models.IntegerField(default=0)
|
||||||
|
jury_add_count = models.IntegerField(default=0)
|
||||||
|
face_participants = models.IntegerField(default=0)
|
||||||
|
remote_participants = models.TextField(default='')
|
||||||
|
doctor_count = models.IntegerField(default=0)
|
||||||
|
start = models.DateTimeField(null=True, default=None, blank=True, editable=False)
|
||||||
|
end = models.DateTimeField(null=True, default=None, blank=True, editable=False)
|
||||||
|
yes_votes = models.IntegerField(default=0, editable=False)
|
||||||
|
no_votes = models.IntegerField(default=0, editable=False)
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('poll-update', kwargs={'pk': self.pk})
|
||||||
|
def is_planned(self):
|
||||||
|
return self.start is None and self.end is None
|
||||||
|
def is_started(self):
|
||||||
|
return self.start is not None and self.end is None
|
||||||
|
def is_ended(self):
|
||||||
|
return self.start is not None and self.end is not None
|
||||||
|
def restart(self):
|
||||||
|
self.start = None
|
||||||
|
self.end = None
|
||||||
|
self.yes_votes = 0
|
||||||
|
self.no_votes = 0
|
||||||
|
self.bulletin_set.all().delete()
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class Bulletin(models.Model):
|
||||||
|
yes_word = models.ForeignKey('Word', on_delete=models.CASCADE, related_name='in_bulletin_yes')
|
||||||
|
no_word = models.ForeignKey('Word', on_delete=models.CASCADE, related_name='in_bulletin_no')
|
||||||
|
face_participant = models.BooleanField(default=False)
|
||||||
|
remote_participant = models.CharField(max_length=255, null=True, default=None)
|
||||||
|
in_poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
|
||||||
|
checked_by = models.ForeignKey(SMS, on_delete=models.CASCADE, null=True, default=None)
|
||||||
|
def __str__(self):
|
||||||
|
p = 'face' if self.face_participant else self.remote_participant
|
||||||
|
return f'{self.in_poll} {p}'
|
||||||
|
|
||||||
|
class Word(models.Model):
|
||||||
|
word = models.CharField(max_length=20, db_index=True)
|
||||||
|
in_use = models.BooleanField(default=False)
|
||||||
|
def __str__(self):
|
||||||
|
return self.word
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
<p>Здравствуйте.</p>
|
||||||
|
|
||||||
|
<p>Вы указали этот адрес электронной почты для участия в тайном электронном голосовании. После начала голосования на этот адрес придет письмо с памяткой для голосования следующего содержания:</p>
|
||||||
|
|
||||||
|
{% include "sms_voting/memo.html" with yes_word="ПЕРВОЕ_СЛОВО" no_word="ВТОРОЕ_СЛОВО" voting_number="+7-9XX-XXX-XX-XX" %}
|
||||||
|
|
||||||
|
<p>Если памятка не пришла, проверьте папку Спам, затем сообщите ученому секретарю.</p>
|
||||||
|
<p>Пожалуйста, выполните предложенные действия в отведенное для голосования время.</p>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
<p>Для участия в тайном электронном голосовании:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Если хотите проголосовать <b>ЗА</b>: отправьте SMS с ключевым словом <b>{{ yes_word }}</b> на номер <b>{{ voting_number }}</b></li>
|
||||||
|
<li>Если хотите проголосовать <b>ПРОТИВ</b>: отправьте SMS с ключевым словом <b>{{ no_word }}</b> на номер <b>{{ voting_number }}</b></li>
|
||||||
|
<li>Ожидайте SMS от SMSC.RU с текстом "Спасибо за участие в голосовании БИН РАН!"</li>
|
||||||
|
<li>Если SMS не пришло в течении 2 минут, проверьте, правильно ли указан номер получателя и отправьте SMS снова.</li>
|
||||||
|
<li>Если после второго SMS подтверждения не пришло, сообщите ученому секретарю.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>Стоимость SMS - 3.5 рубля. Первое сообщение, полученное сервисом, будет считаться вашим голосом.</p>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "sms_voting/print.html" %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
body {
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
.no-break {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% for b in bulletins %}
|
||||||
|
<div class="no-break">
|
||||||
|
{% include "sms_voting/memo.html" with yes_word=b.yes_word no_word=b.no_word voting_number=voting_number %}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<form action="." method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="submit" name="save" value="Save">
|
||||||
|
<input type="submit" name="send_instructions" value="Send instructions">
|
||||||
|
{% if object.is_planned %}
|
||||||
|
<input type="submit" name="start_poll" value="Start poll">
|
||||||
|
{% endif %}
|
||||||
|
{% if object.is_started %}
|
||||||
|
<input type="submit" name="end_poll" value="End poll">
|
||||||
|
<input type="submit" name="resend_memos" value="Resend memos to remote participants">
|
||||||
|
<input type="submit" name="download_memos" value="Download memos for face participants">
|
||||||
|
{% endif %}
|
||||||
|
{% if object.is_ended %}
|
||||||
|
<input type="submit" name="show_results" value="Show results">
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" name="restart" value="Restart poll">
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<h1>Polls</h1>
|
||||||
|
<ul>
|
||||||
|
{% for poll in object_list %}
|
||||||
|
<li><a href="poll/{{ poll.pk }}">{{ poll.title }}</a></li>
|
||||||
|
{% empty %}
|
||||||
|
<li>No polls yet.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
.w3cm {
|
||||||
|
display: inline-block;
|
||||||
|
width: 3cm
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
display: inline;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
border-top: 3px solid #ccc;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
{% block style %}
|
||||||
|
{% endblock %}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{% extends "sms_voting/print.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Протокол №___</h2>
|
||||||
|
<h2>
|
||||||
|
о результатах тайного голосования по присуждению ученой степени кандидата биологических наук с
|
||||||
|
использованием информационно-коммуникационных технологий при проведении заседания диссертационного
|
||||||
|
совета 24.1.002.02 в удаленном интерактивном режиме от {{ poll_date | date:"d.m.Y" }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Тайное голосование с использованием информационно-коммуникационных технологий проводилось по
|
||||||
|
вопросу присуждения <b>{{ person }}</b> ученой степени кандидата биологических наук.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Состав диссертационного совета утвержден в количестве {{ jury_full_count }} человек на период
|
||||||
|
действия Номенклатуры научных специальностей, по которым присуждаются ученые степени. В состав
|
||||||
|
диссертационного совета дополнительно введены {{ jury_add_count }} человек. Присутствовало на
|
||||||
|
заседании {{ jury_sum_count }} членов диссертационного совета (очно {{ jury_face_count }},
|
||||||
|
дистанционно {{ jury_remote_count }}), в том числе докторов наук по профилю рассматриваемой
|
||||||
|
диссертации {{ jury_doctor_count }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Результаты тайного голосования с использованием информационно-коммуникационных технологий по
|
||||||
|
вопросу о присуждении <b>{{ person }}</b> ученой степени кандидата биологических наук по специальности
|
||||||
|
1.5.15. Экология
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>ЗА {{ yes_votes }}</p>
|
||||||
|
<p>ПРОТИВ {{ no_votes }}</p>
|
||||||
|
|
||||||
|
<p><b>Электронное голосование проводилось в ___ час. ___ мин.</b></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
По причине технических неполадок во время проведения электронного голосования по присуждению ученой
|
||||||
|
степени, не позволивших обеспечить принятие диссертационным советом решения в соответствии с
|
||||||
|
требованиями Положения о совете по защите диссертаций на соискание ученой степени кандидата наук,
|
||||||
|
на соискание ученой степени доктора наук, было проведено повторное электронное голосование после
|
||||||
|
устранения технических неполадок.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<b>Результаты повторного тайного голосования</b> с использованием информационно-коммуникационных
|
||||||
|
технологий по вопросу о присуждении {{ person }} ученой степени кандидата биологических наук по
|
||||||
|
специальности 1.5.15. Экология
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>ЗА ______</p>
|
||||||
|
<p>ПРОТИВ ______</p>
|
||||||
|
|
||||||
|
<p>Не участвующие в голосовании ______</p>
|
||||||
|
<p>
|
||||||
|
(в соответствии с п. 51(2) Положения о совете по защите диссертаций на соискание ученой степени
|
||||||
|
кандидата наук, на соискание ученой степени доктора наук).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><b>Повторное электронное голосование проводилось в ___ час. ___ мин.</b></p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="line">
|
||||||
|
<p style="display:inline-block">Председатель <br> диссертационного совета 24.1.002.02</p>
|
||||||
|
<p class="right">________________/<span class="w3cm"></span>/</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="line">
|
||||||
|
<p style="display:inline-block">Ученый секретарь <br> диссертационного совета 24.1.002.02</p>
|
||||||
|
<p class="right">________________/<span class="w3cm"></span>/</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
from django.views.generic.detail import DetailView
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
|
from .models import Poll, SMS
|
||||||
|
from .actions import *
|
||||||
|
|
||||||
|
|
||||||
|
class PollListView(LoginRequiredMixin, ListView):
|
||||||
|
raise_exception = True
|
||||||
|
model = Poll
|
||||||
|
|
||||||
|
class PollView(LoginRequiredMixin, UpdateView):
|
||||||
|
raise_exception = True
|
||||||
|
template_name = 'sms_voting/poll_detail.html'
|
||||||
|
model = Poll
|
||||||
|
fields = ['title', 'person', 'jury_full_count', 'jury_add_count', 'doctor_count', 'face_participants', 'remote_participants']
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
res = super().post(request, *args, **kwargs)
|
||||||
|
if res.status_code == 302:
|
||||||
|
if 'start_poll' in request.POST:
|
||||||
|
self.object.start = datetime.now()
|
||||||
|
self.object.save()
|
||||||
|
generate_bulletins(self.object)
|
||||||
|
send_memos(self.object)
|
||||||
|
elif 'end_poll' in request.POST:
|
||||||
|
self.object.end = datetime.now()
|
||||||
|
self.object.save()
|
||||||
|
self.object.bulletin_set.all().delete()
|
||||||
|
elif 'send_instructions' in request.POST:
|
||||||
|
send_instructions(self.object)
|
||||||
|
elif 'show_results' in request.POST:
|
||||||
|
res = download_report(self.object)
|
||||||
|
elif 'restart' in request.POST:
|
||||||
|
self.object.restart()
|
||||||
|
self.object.save()
|
||||||
|
elif 'resend_memos' in request.POST:
|
||||||
|
send_memos(self.object)
|
||||||
|
elif 'download_memos' in request.POST:
|
||||||
|
res = download_memos(self.object)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def sms_endpoint(request):
|
||||||
|
'''
|
||||||
|
{'phone': ['79999999999'], 'mes': ['test'], 'id': ['200'], 'to': ['79138977777'], 'time': ['1636381583'], 'sent': ['1636381583'], 'smsc': ['7900000000'], 'sms_id': ['100']}
|
||||||
|
'''
|
||||||
|
print('got sms')
|
||||||
|
try:
|
||||||
|
if request.method=='POST':
|
||||||
|
d = request.POST
|
||||||
|
sms = SMS(number_from=d['phone'], text=d['mes'])
|
||||||
|
sms.save()
|
||||||
|
count_vote(sms)
|
||||||
|
return HttpResponse("Thank you!")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return HttpResponse("Thank you!")
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
1. Подготовка голосования
|
||||||
|
1. Создание голосования
|
||||||
|
1. Написать название голосования
|
||||||
|
2. Вставить список электронных адресов удаленных участников
|
||||||
|
3. Отметить примерное количество очных участников
|
||||||
|
4. Сохранить
|
||||||
|
2. Внесение электронных адресов удаленных участников
|
||||||
|
1. Добавить электронные адреса
|
||||||
|
2. Сохранить
|
||||||
|
3. Добавление примерного количества очных участников
|
||||||
|
1. Изменить количество
|
||||||
|
2. Сохранить
|
||||||
|
4. Генерация ключевых слов голосования
|
||||||
|
1. Из списка неназначенных слов взять необходимое количество
|
||||||
|
2. Назначить
|
||||||
|
3. ИЛИ
|
||||||
|
4. Взять последние лишние слова из назначенных
|
||||||
|
5. Отменить назначение
|
||||||
|
5. Печать памяток для очных участников
|
||||||
|
1. Достать все пары ключевых слов для очных участников
|
||||||
|
2. Подставить в шаблон памятки
|
||||||
|
3. Памятки подставить в шаблон документа
|
||||||
|
2. Объявление начала голосования
|
||||||
|
1. Отправка электронных писем с памятками удаленным участникам
|
||||||
|
1. Достать все пары ключевых слов для удаленных участников
|
||||||
|
2. Подставить в шаблон памятки
|
||||||
|
3. Отправить письма на электронные адреса
|
||||||
|
2. Фиксация не использованных памяток
|
||||||
|
1. Отметить количество неиспользованных памяток для очного голосования
|
||||||
|
2. Отметить количество отсутствующих удаленных участников
|
||||||
|
3. Голосование
|
||||||
|
1. Получение голоса
|
||||||
|
1. Получить СМС, сохранить
|
||||||
|
2. Определить тип смс:
|
||||||
|
1. Если ЗА, заблокировать бюллетень, перевести бюллетень в состояние ЗА, отправить ответное СМС
|
||||||
|
2. Если ПРОТИВ, заблокировать бюллетень, перевести бюллетень в состояние ПРОТИВ, отправить ответное СМС
|
||||||
|
3. Иначе, отправить сообщение об ошибке
|
||||||
|
4. Объявление окончания голосования
|
||||||
|
1. Блокировка голосования
|
||||||
|
2. Подсчет результатов
|
||||||
|
1. Посчитать количество ЗА
|
||||||
|
2. Посчитать количество ПРОТИВ
|
||||||
|
3. Посчитать количество недействительных = ОБЩЕЕ - ЗА - ПРОТИВ
|
||||||
|
3. Печать результатов
|
||||||
|
1. Подставить результаты в шаблон документа
|
||||||
|
|
||||||
|
## Операторы SMS
|
||||||
|
- https://sigmasms.ru/priem/
|
||||||
|
- https://onlinesim.ru/ -- Не подходит, нет автоматизации
|
||||||
|
- https://www.smsfeedback.ru/priem-sms-na-federalnom-nomere.php -- Только для ЮрЛиц
|
||||||
|
- https://smsc.ru/tariffs/#phones -- Подходит, отправка только для ЮрЛиц
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
-
|
||||||
|
- поставить pypandoc
|
||||||
|
- отправка писем очень медленная, нужно распараллелить
|
||||||
|
- Записать настройки в ансибл
|
||||||
|
- Сделать чтобы все поля показывались
|
||||||
|
- поставить условия на серверной части кнопок
|
||||||
|
- поставить условие суперюзер на рестарт клиент|сервер
|
||||||
|
- построить базовый шаблон для красоты
|
||||||
|
|
||||||
Reference in New Issue
Block a user