Initial commit

This commit is contained in:
2021-12-04 14:34:14 +03:00
commit 6b0bda841d
38 changed files with 46057 additions and 0 deletions
+140
View File
@@ -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
View File
+16
View File
@@ -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()
+136
View File
@@ -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'
+25
View File
@@ -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'),
]
+16
View File
@@ -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()
Executable
+22
View File
@@ -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()
+4
View File
@@ -0,0 +1,4 @@
asgiref==3.4.1
Django==3.2.8
pytz==2021.3
sqlparse==0.4.2
View File
+119
View File
@@ -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)
+12
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class SmsVotingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sms_voting'
+58
View File
@@ -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),
),
]
+21
View File
@@ -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=''),
),
]
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -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>
+12
View File
@@ -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 %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+65
View File
@@ -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!")
+62
View File
@@ -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
- отправка писем очень медленная, нужно распараллелить
- Записать настройки в ансибл
- Сделать чтобы все поля показывались
- поставить условия на серверной части кнопок
- поставить условие суперюзер на рестарт клиент|сервер
- построить базовый шаблон для красоты