diff --git a/.gitignore b/.gitignore index 131f236781569ef3d47a82f46dffe63c5be48b02..5f846bdc44dc42490d2b3c88d19b3dc0f50533e3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ out smash/htmlcov/ run-coverage.bat +media diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fd2748cea54a607609c5c9468f6aa2ee75c9e208..7cf83b16820ded2b91b06faef75aad4f0645f8c5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ variables: POSTGRES_PASSWORD: password before_script: - - apt-get update && apt-get install -y libsasl2-dev libssl-dev + - apt-get update && apt-get install -y libsasl2-dev libssl-dev locales locales-all - pip install -r requirements.txt --default-timeout=180 - pip install -r requirements-dev.txt --default-timeout=180 diff --git a/readme.md b/readme.md index 42b1f415c237d3a92fa4a132e7b8e1e6cf5eb6eb..901118ed48af950c3283b7c5d12d01c951095e48 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [](https://git-r3lab.uni.lu/piotr.atyjaszyk/scheduling-system/commits/master) ## Required software (on ubuntu's OS family): - - `sudo apt-get install libpq-dev python-dev postgresql postgresql-contrib python virtualenv python-virtualenv gcc` + - `sudo apt-get install libpq-dev python-dev postgresql postgresql-contrib python virtualenv python-virtualenv gcc python-lxml libxml2-dev` ## Postgres installation - If you don't have postgres installed, complete step seven from [https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-django-with-postgres-nginx-and-gunicorn](https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-django-with-postgres-nginx-and-gunicorn) (remember to save all the credentials, they will be necessary to run the application). @@ -30,9 +30,9 @@ ## Production deployment - git pull and other project installation should be performed in a dir where this django app should be installed, in this tutorial it's /var/www/scheduling-system/ - install nginx: `apt-get install nginx` - - create gunicorn service in systemd (http://docs.gunicorn.org/en/stable/deploy.html#systemd): + - create gunicorn service in systemd (http://docs.gunicorn.org/en/stable/deploy.html#systemd): -### /etc/systemd/system/gunicorn.service +### /etc/systemd/system/gunicorn.service ``` [Unit] @@ -67,7 +67,7 @@ ListenStream=[::]:8000 [Install] WantedBy=sockets.target - + - modify nginx configuration # /etc/nginx/nginx.conf @@ -173,4 +173,4 @@ server { } ``` - extract static files and make them available via nginx: `./manage.py collectstatic` - - you start application by starting gunicorn and nginx: `service gunicorn start`, `service nginx start` \ No newline at end of file + - you start application by starting gunicorn and nginx: `service gunicorn start`, `service nginx start` diff --git a/requirements.txt b/requirements.txt index 72e849e28c5189314ff7943b733264979c9a153a..aec2cf772fd0df7d6d20ecc33e0b13cab0f8e621 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,6 @@ gunicorn==19.6.0 Pillow==3.4.2 psycopg2==2.6.2 pytz==2016.10 +lxml==3.7.3 +python-docx==0.8.6 +django-cleanup==0.4.2 \ No newline at end of file diff --git a/smash/smash/local_settings.py.template b/smash/smash/local_settings.py.template index ba1dcda662d69f97380d284555e5284a86898335..32397c94d8545705fce662d3e443f15b3cdf4a20 100644 --- a/smash/smash/local_settings.py.template +++ b/smash/smash/local_settings.py.template @@ -1,5 +1,5 @@ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'Paste long random string here' # Insert long random string +SECRET_KEY = 'Paste long random string here' # Insert long random string # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -21,11 +21,11 @@ DEFAULT_FROM_EMAIL = 'prc-scheduling-admin@uni.lu' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'smashdb', # Insert your database's name - 'USER': 'postgresmashuser', # Insert your database's user - 'PASSWORD': 'thePOSTGRESpassword', # Insert your user's password + 'NAME': 'smashdb', # Insert your database's name + 'USER': 'postgresmashuser', # Insert your database's user + 'PASSWORD': 'thePOSTGRESpassword', # Insert your user's password 'HOST': 'localhost', - 'PORT': '' # '' === default one # Empty string is OK + 'PORT': '' # '' === default one # Empty string is OK # If to use sqlite # 'ENGINE': 'django.db.backends.sqlite3', @@ -33,5 +33,7 @@ DATABASES = { } } -STATIC_ROOT = '/tmp/static' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one; e.g. ~/tmp/static -MEDIA_ROOT = '/tmp/media' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one, e.g. ~/tmp/media +STATIC_ROOT = '/tmp/static' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one; e.g. ~/tmp/static +MEDIA_ROOT = '/tmp/media' # Warning! `/tmp` directory can be flushed in any moment; use a persistent one, e.g. ~/tmp/media + +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' diff --git a/smash/smash/settings.py b/smash/smash/settings.py index 0e57c97649286fdad18830070e20f69a20303d03..48fbaf41986bb03af7835e85f076684e91bdbc90 100644 --- a/smash/smash/settings.py +++ b/smash/smash/settings.py @@ -38,6 +38,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_cleanup', 'debug_toolbar', 'web' ] diff --git a/smash/web/docx_helper.py b/smash/web/docx_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..fb78711c12cf62d7129610e3417785a2039ed796 --- /dev/null +++ b/smash/web/docx_helper.py @@ -0,0 +1,17 @@ +from docx import Document + + +def process_file(path_to_docx, path_to_new_docx, changes_to_apply): + """ + Tries to open the docx document using given path to file. + Then, applies the transformations- replaces template tags + in format of ##name## to values specified in the second + argument. + """ + doc = Document(path_to_docx) + for paragraph in doc.paragraphs: + for placeholder, replacement in changes_to_apply.items(): + if placeholder in paragraph.text: + paragraph.text = paragraph.text.replace(placeholder, replacement) + + doc.save(path_to_new_docx) diff --git a/smash/web/migrations/0034_mail_templates.py b/smash/web/migrations/0034_mail_templates.py new file mode 100644 index 0000000000000000000000000000000000000000..20dec068d4b2392361b8c76a112dd32828354401 --- /dev/null +++ b/smash/web/migrations/0034_mail_templates.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-04-07 07:10 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +def update_languages(apps, schema_editor): + Language = apps.get_model("web", "Language") + updates_to_perform = [ + ("French", 10, "fr_FR"), + ("Dutch", 100, "nl_NL"), + ("Swedish", 200, "se_SE"), + ("Spanish", 110, "es_ES"), + ("Slovak", 200, "es_ES"), + ("Romanian", 200, "ro_RO"), + ("Polish", 200, "pl_PL"), + ("Italian", 100, "it_IT"), + ("Hungarian", 200, "hu_HU"), + ("Greek", 200, "el_GR"), + ("Finnish", 200, "fi_FI"), + ("Danish", 200, "da_DK"), + ("Arabic", 200, "ar_DZ"), + ("Portuguese", 80, "pt_PT"), + ("Luxembourgish", 20, "lb_LU"), + ("English", 50, "en_GB"), + ("German", 30, "de_DE"), + ] + for update_to_perform in updates_to_perform: + name, order, locale = update_to_perform + language = Language.objects.filter(name=name).first() + if language is not None: + language.order = order + language.locale = locale + language.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0033_auto_20170406_1146'), + ] + + operations = [ + migrations.CreateModel( + name='MailTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('context', models.CharField(choices=[(b'A', b'Appointment'), (b'S', b'Subject'), (b'V', b'Visit')], + max_length=1)), + ('template_file', models.FileField(upload_to=b'templates/')), + ], + ), + migrations.AlterModelOptions( + name='language', + options={'ordering': ['order']}, + ), + migrations.AddField( + model_name='language', + name='locale', + field=models.CharField( + choices=[(b'af_ZA', b'af_ZA'), (b'am_ET', b'am_ET'), (b'ar_AE', b'ar_AE'), (b'ar_BH', b'ar_BH'), + (b'ar_DZ', b'ar_DZ'), (b'ar_EG', b'ar_EG'), (b'ar_IQ', b'ar_IQ'), (b'ar_JO', b'ar_JO'), + (b'ar_KW', b'ar_KW'), (b'ar_LB', b'ar_LB'), (b'ar_LY', b'ar_LY'), (b'ar_MA', b'ar_MA'), + (b'ar_OM', b'ar_OM'), (b'ar_QA', b'ar_QA'), (b'ar_SA', b'ar_SA'), (b'ar_SY', b'ar_SY'), + (b'ar_TN', b'ar_TN'), (b'ar_YE', b'ar_YE'), (b'arn_CL', b'arn_CL'), (b'as_IN', b'as_IN'), + (b'az_AZ', b'az_AZ'), (b'az_AZ', b'az_AZ'), (b'ba_RU', b'ba_RU'), (b'be_BY', b'be_BY'), + (b'bg_BG', b'bg_BG'), (b'bn_IN', b'bn_IN'), (b'bo_BT', b'bo_BT'), (b'bo_CN', b'bo_CN'), + (b'br_FR', b'br_FR'), (b'bs_BA', b'bs_BA'), (b'bs_BA', b'bs_BA'), (b'ca_ES', b'ca_ES'), + (b'co_FR', b'co_FR'), (b'cs_CZ', b'cs_CZ'), (b'cy_GB', b'cy_GB'), (b'da_DK', b'da_DK'), + (b'de_AT', b'de_AT'), (b'de_CH', b'de_CH'), (b'de_DE', b'de_DE'), (b'de_LI', b'de_LI'), + (b'de_LU', b'de_LU'), (b'div_MV', b'div_MV'), (b'dsb_DE', b'dsb_DE'), (b'el_GR', b'el_GR'), + (b'en_AU', b'en_AU'), (b'en_BZ', b'en_BZ'), (b'en_CA', b'en_CA'), (b'en_CB', b'en_CB'), + (b'en_GB', b'en_GB'), (b'en_IE', b'en_IE'), (b'en_IN', b'en_IN'), (b'en_IN', b'en_IN'), + (b'en_JA', b'en_JA'), (b'en_MY', b'en_MY'), (b'en_NZ', b'en_NZ'), (b'en_PH', b'en_PH'), + (b'en_TT', b'en_TT'), (b'en_US', b'en_US'), (b'en_ZA', b'en_ZA'), (b'en_ZW', b'en_ZW'), + (b'es_AR', b'es_AR'), (b'es_BO', b'es_BO'), (b'es_CL', b'es_CL'), (b'es_CO', b'es_CO'), + (b'es_CR', b'es_CR'), (b'es_DO', b'es_DO'), (b'es_EC', b'es_EC'), (b'es_ES', b'es_ES'), + (b'es_ES', b'es_ES'), (b'es_GT', b'es_GT'), (b'es_HN', b'es_HN'), (b'es_MX', b'es_MX'), + (b'es_NI', b'es_NI'), (b'es_PA', b'es_PA'), (b'es_PE', b'es_PE'), (b'es_PR', b'es_PR'), + (b'es_PY', b'es_PY'), (b'es_SV', b'es_SV'), (b'es_UR', b'es_UR'), (b'es_US', b'es_US'), + (b'es_VE', b'es_VE'), (b'et_EE', b'et_EE'), (b'eu_ES', b'eu_ES'), (b'fa_IR', b'fa_IR'), + (b'fi_FI', b'fi_FI'), (b'fil_PH', b'fil_PH'), (b'fo_FO', b'fo_FO'), (b'fr_BE', b'fr_BE'), + (b'fr_CA', b'fr_CA'), (b'fr_CH', b'fr_CH'), (b'fr_FR', b'fr_FR'), (b'fr_LU', b'fr_LU'), + (b'fr_MC', b'fr_MC'), (b'fy_NL', b'fy_NL'), (b'ga_IE', b'ga_IE'), (b'gbz_AF', b'gbz_AF'), + (b'gl_ES', b'gl_ES'), (b'gsw_FR', b'gsw_FR'), (b'gu_IN', b'gu_IN'), (b'ha_NG', b'ha_NG'), + (b'he_IL', b'he_IL'), (b'hi_IN', b'hi_IN'), (b'hr_BA', b'hr_BA'), (b'hr_HR', b'hr_HR'), + (b'hu_HU', b'hu_HU'), (b'hy_AM', b'hy_AM'), (b'id_ID', b'id_ID'), (b'ii_CN', b'ii_CN'), + (b'is_IS', b'is_IS'), (b'it_CH', b'it_CH'), (b'it_IT', b'it_IT'), (b'iu_CA', b'iu_CA'), + (b'iu_CA', b'iu_CA'), (b'ja_JP', b'ja_JP'), (b'ka_GE', b'ka_GE'), (b'kh_KH', b'kh_KH'), + (b'kk_KZ', b'kk_KZ'), (b'kl_GL', b'kl_GL'), (b'kn_IN', b'kn_IN'), (b'ko_KR', b'ko_KR'), + (b'kok_IN', b'kok_IN'), (b'ky_KG', b'ky_KG'), (b'lb_LU', b'lb_LU'), (b'lo_LA', b'lo_LA'), + (b'lt_LT', b'lt_LT'), (b'lv_LV', b'lv_LV'), (b'mi_NZ', b'mi_NZ'), (b'mk_MK', b'mk_MK'), + (b'ml_IN', b'ml_IN'), (b'mn_CN', b'mn_CN'), (b'mn_MN', b'mn_MN'), (b'moh_CA', b'moh_CA'), + (b'mr_IN', b'mr_IN'), (b'ms_BN', b'ms_BN'), (b'ms_MY', b'ms_MY'), (b'mt_MT', b'mt_MT'), + (b'nb_NO', b'nb_NO'), (b'ne_NP', b'ne_NP'), (b'nl_BE', b'nl_BE'), (b'nl_NL', b'nl_NL'), + (b'nn_NO', b'nn_NO'), (b'ns_ZA', b'ns_ZA'), (b'oc_FR', b'oc_FR'), (b'or_IN', b'or_IN'), + (b'pa_IN', b'pa_IN'), (b'pl_PL', b'pl_PL'), (b'ps_AF', b'ps_AF'), (b'pt_BR', b'pt_BR'), + (b'pt_PT', b'pt_PT'), (b'qut_GT', b'qut_GT'), (b'quz_BO', b'quz_BO'), (b'quz_EC', b'quz_EC'), + (b'quz_PE', b'quz_PE'), (b'rm_CH', b'rm_CH'), (b'ro_RO', b'ro_RO'), (b'ru_RU', b'ru_RU'), + (b'rw_RW', b'rw_RW'), (b'sa_IN', b'sa_IN'), (b'sah_RU', b'sah_RU'), (b'se_FI', b'se_FI'), + (b'se_NO', b'se_NO'), (b'se_SE', b'se_SE'), (b'si_LK', b'si_LK'), (b'sk_SK', b'sk_SK'), + (b'sl_SI', b'sl_SI'), (b'sma_NO', b'sma_NO'), (b'sma_SE', b'sma_SE'), (b'smj_NO', b'smj_NO'), + (b'smj_SE', b'smj_SE'), (b'smn_FI', b'smn_FI'), (b'sms_FI', b'sms_FI'), (b'sq_AL', b'sq_AL'), + (b'sr_BA', b'sr_BA'), (b'sr_BA', b'sr_BA'), (b'sr_SP', b'sr_SP'), (b'sr_SP', b'sr_SP'), + (b'sv_FI', b'sv_FI'), (b'sv_SE', b'sv_SE'), (b'sw_KE', b'sw_KE'), (b'syr_SY', b'syr_SY'), + (b'ta_IN', b'ta_IN'), (b'te_IN', b'te_IN'), (b'tg_TJ', b'tg_TJ'), (b'th_TH', b'th_TH'), + (b'tk_TM', b'tk_TM'), (b'tmz_DZ', b'tmz_DZ'), (b'tn_ZA', b'tn_ZA'), (b'tr_TR', b'tr_TR'), + (b'tt_RU', b'tt_RU'), (b'ug_CN', b'ug_CN'), (b'uk_UA', b'uk_UA'), (b'ur_IN', b'ur_IN'), + (b'ur_PK', b'ur_PK'), (b'uz_UZ', b'uz_UZ'), (b'uz_UZ', b'uz_UZ'), (b'vi_VN', b'vi_VN'), + (b'wen_DE', b'wen_DE'), (b'wo_SN', b'wo_SN'), (b'xh_ZA', b'xh_ZA'), (b'yo_NG', b'yo_NG'), + (b'zh_CHS', b'zh_CHS'), (b'zh_CHT', b'zh_CHT'), (b'zh_CN', b'zh_CN'), (b'zh_HK', b'zh_HK'), + (b'zh_MO', b'zh_MO'), (b'zh_SG', b'zh_SG'), (b'zh_TW', b'zh_TW'), (b'zu_ZA', b'zu_ZA')], + default=b'fr_FR', max_length=10), + ), + migrations.AddField( + model_name='language', + name='order', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='mailtemplate', + name='language', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Language'), + ), + migrations.RunPython(update_languages, reverse_code=lambda x, y: None), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index a2dc22415ab64d1292bb53ec1fbcdc4262fbbeaf..c7f68233137fd9a8cc95916081631f6d4936686e 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -19,6 +19,7 @@ from item import Item from language import Language from subject import Subject from contact_attempt import ContactAttempt +from mail_template import MailTemplate def get_current_year(): @@ -26,4 +27,4 @@ def get_current_year(): __all__ = [FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, - Visit, Worker, ContactAttempt, ConfigurationItem] + Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate] diff --git a/smash/web/models/appointment.py b/smash/web/models/appointment.py index 176dd9d0b9a0cbdb19c15a2d88aa7d5a6b4066a0..c955871a389486bd8674659cca05293707e97b55 100644 --- a/smash/web/models/appointment.py +++ b/smash/web/models/appointment.py @@ -5,7 +5,6 @@ from django.db import models from constants import APPOINTMENT_TYPE_DEFAULT_COLOR, APPOINTMENT_TYPE_DEFAULT_FONT_COLOR, \ CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE, NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE -from . import FlyingTeam, Location, Room, Visit, Worker from . import ConfigurationItem @@ -24,11 +23,11 @@ class Appointment(models.Model): APPOINTMENT_STATUS_NO_SHOW: 'No Show', } - flying_team = models.ForeignKey(FlyingTeam, + flying_team = models.ForeignKey("web.FlyingTeam", verbose_name='Flying team (if applicable)', null=True, blank=True ) - worker_assigned = models.ForeignKey(Worker, + worker_assigned = models.ForeignKey("web.Worker", verbose_name='Worker conducting the assessment (if applicable)', null=True, blank=True ) @@ -36,15 +35,15 @@ class Appointment(models.Model): verbose_name='Appointment types', blank=True ) - room = models.ForeignKey(Room, + room = models.ForeignKey("web.Room", verbose_name='Room ID', null=True, blank=True ) - location = models.ForeignKey(Location, + location = models.ForeignKey("web.Location", verbose_name='Location', ) - visit = models.ForeignKey(Visit, + visit = models.ForeignKey("web.Visit", verbose_name='Visit ID', editable=False, null=True, diff --git a/smash/web/models/appointment_type.py b/smash/web/models/appointment_type.py index 84304f466bc0da92703207852048dbc8f180f48d..a6a3ba76a64784212b1f1db2b32eba312aec0af1 100644 --- a/smash/web/models/appointment_type.py +++ b/smash/web/models/appointment_type.py @@ -7,6 +7,7 @@ from constants import APPOINTMENT_TYPE_DEFAULT_COLOR, APPOINTMENT_TYPE_DEFAULT_F class AppointmentType(models.Model): class Meta: app_label = 'web' + ordering = ['description'] required_equipment = models.ManyToManyField("web.Item", verbose_name='Required equipment', @@ -52,9 +53,6 @@ class AppointmentType(models.Model): default='ANY' ) - class Meta: - ordering = ['description'] - def __str__(self): return self.description diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py index 5d7ac193bc9e18e59b15e858bafb2a92ee8e15aa..6fb833e06cc3dd8748fed23716c384c2beecc891 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -1,4 +1,6 @@ # coding=utf-8 +import locale + BOOL_CHOICES = ((True, 'Yes'), (False, 'No')) SEX_CHOICES_MALE = 'M' SEX_CHOICES_FEMALE = 'F' @@ -34,3 +36,14 @@ NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE = "NO_SHOW_APPOINTMENT_COLOR" KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE = "KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE" KIT_EMAIL_HOUR_CONFIGURATION_TYPE = "KIT_DAILY_EMAIL_HOUR_CONFIGURATION_TYPE" KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE = "KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE" + +MAIL_TEMPLATE_CONTEXT_SUBJECT = 'S' +MAIL_TEMPLATE_CONTEXT_APPOINTMENT = 'A' +MAIL_TEMPLATE_CONTEXT_VISIT = 'V' + +MAIL_TEMPLATE_CONTEXT_CHOICES = ( + (MAIL_TEMPLATE_CONTEXT_APPOINTMENT, 'Appointment'), + (MAIL_TEMPLATE_CONTEXT_SUBJECT, 'Subject'), + (MAIL_TEMPLATE_CONTEXT_VISIT, 'Visit'), +) +LOCALE_CHOICES = [(value, value) for value in sorted(locale.windows_locale.values())] diff --git a/smash/web/models/language.py b/smash/web/models/language.py index 8e786111d9a4265f366f2f1389db98a9af1e50f0..1b0cb703255e0aa6f9f7c23e22d804ee901ec946 100644 --- a/smash/web/models/language.py +++ b/smash/web/models/language.py @@ -1,13 +1,19 @@ # coding=utf-8 + from django.db import models +from .constants import LOCALE_CHOICES + class Language(models.Model): class Meta: app_label = 'web' + ordering = ["order"] name = models.CharField(max_length=20) image = models.ImageField() + order = models.IntegerField(default=0) + locale = models.CharField(max_length=10, choices=LOCALE_CHOICES, null=False, blank=False, default="fr_FR") def __str__(self): return self.name diff --git a/smash/web/models/mail_template.py b/smash/web/models/mail_template.py new file mode 100644 index 0000000000000000000000000000000000000000..8e0316a9efa3a8e2fa348bf0b47f9eb71727cb4c --- /dev/null +++ b/smash/web/models/mail_template.py @@ -0,0 +1,222 @@ +# coding=utf-8 +import datetime +import locale +from contextlib import contextmanager + +from django.db import models + +from .constants import MAIL_TEMPLATE_CONTEXT_CHOICES, MAIL_TEMPLATE_CONTEXT_APPOINTMENT, MAIL_TEMPLATE_CONTEXT_SUBJECT, \ + MAIL_TEMPLATE_CONTEXT_VISIT +from ..docx_helper import process_file +from ..models import Appointment, Visit, Subject, Worker + +DATE_FORMAT_FULL = "%A %-d %B %Y" + +DATETIME_FORMAT = "%A %-d %B %Y, %H:%m" + +DATE_FORMAT_SHORT = "%d.%m.%Y" + +DATE_FORMAT_TIME = "%H:%M" + +now = datetime.datetime.now() + + +@contextmanager +def setlocale(name): + saved = locale.setlocale(locale.LC_TIME) + try: + yield locale.setlocale(locale.LC_TIME, name) + finally: + locale.setlocale(locale.LC_TIME, saved) + + +class MailTemplate(models.Model): + MAILS_TEMPLATE_GENERIC_TAGS = [ + ("##DATE_FULL##", "Current date when the mail will be generated (long format)", now.strftime(DATE_FORMAT_FULL)), + ("##DATE_SHORT##", "Current date when the mail will be generated (short format)", + now.strftime(DATE_FORMAT_SHORT)), + ("##WORKER##", "The full name of the currently logged in user", "") + ] + + MAILS_TEMPLATE_SUBJECT_TAGS = [ + ("##S_FULL_NAME##", "Subject's full name", "first_name last_name"), + ("##S_FIRST_NAME##", "Subject's first name", ""), + ("##S_LAST_NAME##", "Subject's last name", ""), + ("##S_ADDRESS##", "Subject's address", "street name and number"), + ("##S_CITY##", "Subject's city of residence", ""), + ("##S_POST_CODE##", "Subject's post code of residence", ""), + ("##S_COUNTRY##", "Subject's country of residence", ""), + ("##S_SEX##", "Subject's gender", "Male/Female"), + ("##S_TYPE##", "Subject's type", "CONTROL/PATIENT"), + ("##S_DATE_BORN##", "Subject's date of birth", now.strftime(DATE_FORMAT_SHORT)), + + ("##S_EMAIL##", "Subject's email address", ""), + ("##S_PHONE_NUMBER##", "Subject's phone number", ""), + ("##S_PHONE_NUMBER_2##", "Subject's second phone number", ""), + ("##S_PHONE_NUMBER_3##", "Subject's third phone number", ""), + ("##S_MAIL_LANGUAGE##", "Subject's preferred language for written communication", ""), + ("##S_KNOWN_LANGUAGES##", "List of languages known by the subject", "comma separated"), + + ("##S_SCREENING_NUMBER##", "Subject's screening number", ""), + ("##S_DIAGNOSIS##", "Subject's diagnosis", ""), + ("##S_DIAGNOSIS_YEAR##", "Subject's year of diagnosis", ""), + ("##S_MPOWER_ID##", "Subject's mPower identifier", ""), + ("##S_ND_NUMBER##", "Subject's ND number", ""), + ("##S_DATE_ADDED##", "Subject's date of creation", now.strftime(DATE_FORMAT_SHORT)), + ] + + MAILS_TEMPLATE_VISIT_TAGS = [ + ("##V_DATE_START_FULL##", "Visit's start date", now.strftime(DATETIME_FORMAT)), + ("##V_DATE_START_SHORT##", "Visit's start date", now.strftime(DATE_FORMAT_SHORT)), + ("##V_DATE_ENDS_FULL##", "Visit's end date", now.strftime(DATETIME_FORMAT)), + ("##V_DATE_ENDS_SHORT##", "Visit's end date", now.strftime(DATE_FORMAT_SHORT)), + ] + + MAILS_TEMPLATE_APPOINTMENT_TAGS = [ + ("##A_DATE_FULL##", "Appointment's date and time", now.strftime(DATETIME_FORMAT)), + ("##A_DATE_SHORT##", "Appointment's date", now.strftime(DATE_FORMAT_SHORT)), + ("##A_TIME##", "Appointment's time", now.strftime(DATE_FORMAT_TIME)), + ("##A_FLYING_TEAM##", "Appointment's flying team location", ""), + ("##A_LOCATION##", "Appointment's location", "value can be 'Flying Team'"), + ("##A_LOCATION_OR_FLYINGTEAM##", "Appointment's real location", + "if flying team then returns flying team exact location, otherwise returns location name"), + ("##A_STATUS##", "Appointment's status", ""), + ("##A_WORKER##", "Worker conducting the assessment", "first_name last_name"), + ("##A_WORKER_PHONE##", "Phone number of the worker conducting the assessment", ""), + ("##A_WORKER_EMAIL##", "Email address of the worker conducting the assessment", ""), + ("##A_ROOM##", "Appointment's room", 'room_number address city'), + ("##A_LENGTH##", "Appointment's duration", 'integer, value in minutes'), + ("##A_TYPES##", "Appointment's types", "comma separated"), + ] + + name = models.CharField(max_length=255) + context = models.CharField(max_length=1, choices=MAIL_TEMPLATE_CONTEXT_CHOICES) + language = models.ForeignKey("web.Language", on_delete=models.CASCADE) + template_file = models.FileField(upload_to='templates/') + + @staticmethod + def get_appointment_mail_templates(languages): + return MailTemplate.get_mail_templates_for_context(languages, MAIL_TEMPLATE_CONTEXT_APPOINTMENT) + + @staticmethod + def get_subject_mail_templates(languages): + return MailTemplate.get_mail_templates_for_context(languages, MAIL_TEMPLATE_CONTEXT_SUBJECT) + + @staticmethod + def get_visit_mail_templates(languages): + return MailTemplate.get_mail_templates_for_context(languages, MAIL_TEMPLATE_CONTEXT_VISIT) + + @staticmethod + def get_mail_templates_for_context(languages, context): + languages_names = [language.name for language in languages] + templates = list(MailTemplate.objects.filter(context=context).all()) + active_templates = [] + disabled_templates = [] + for template in templates: + if template.language.name in languages_names: + active_templates.append(template) + else: + disabled_templates.append(template) + active_templates.sort(key=lambda x: languages_names.index(x.language.name)) + return active_templates, disabled_templates + + def apply(self, instance, user, stream): + appointment = None + visit = None + subject = None + if isinstance(instance, Appointment): + appointment = instance + visit = instance.visit + subject = visit.subject + elif isinstance(instance, Visit): + visit = instance + subject = visit.subject + elif isinstance(instance, Subject): + subject = instance + # set locale to get correct date format + with setlocale(self.language.locale.encode('utf8')): + replacements = {} + self._add_generic_replacements(replacements, Worker.get_by_user(user)) + self._add_appointment_replacements(replacements, appointment) + self._add_visit_replacements(replacements, visit) + self._add_subject_replacements(replacements, subject) + process_file(self.template_file.path, stream, replacements) + return stream + + def _add_generic_replacements(self, replacements, worker): + current_datetime = datetime.datetime.now() + replacements.update({ + "##DATE_FULL##": current_datetime.strftime(DATE_FORMAT_FULL), + "##DATE_SHORT##": current_datetime.strftime(DATE_FORMAT_SHORT), + "##WORKER##": str(worker) + }) + + def _add_appointment_replacements(self, replacements, appointment): + if appointment is not None: + if appointment.worker_assigned is not None: + worker_phone_number = appointment.worker_assigned.phone_number + worker_email_address = appointment.worker_assigned.email + else: + worker_phone_number = "" + worker_email_address = "" + if appointment.datetime_when is not None: + appointment_date_full = appointment.datetime_when.strftime(DATETIME_FORMAT) + appointment_date_short = appointment.datetime_when.strftime(DATE_FORMAT_SHORT) + appointment_date_time = appointment.datetime_when.strftime(DATE_FORMAT_TIME) + else: + appointment_date_full = appointment_date_short = appointment_date_time = "" + replacements.update({ + "##A_DATE_FULL##": appointment_date_full, + "##A_DATE_SHORT##": appointment_date_short, + "##A_TIME##": appointment_date_time, + "##A_FLYING_TEAM##": appointment.flying_team, + "##A_STATUS##": appointment.get_status_display(), + "##A_LOCATION##": appointment.location.name, + "##A_LOCATION_OR_FLYINGTEAM##": appointment.flying_team or appointment.location.name, + "##A_WORKER##": str(appointment.worker_assigned), + '##A_WORKER_PHONE##': worker_phone_number, + '##A_WORKER_EMAIL##': worker_email_address, + "##A_ROOM##": str(appointment.room), + "##A_LENGTH##": appointment.length, + "##A_TYPES##": ", ".join([a.description for a in appointment.appointment_types.all()]) + }) + + def _add_visit_replacements(self, replacements, visit): + if visit is not None: + replacements.update({ + "##V_DATE_START_FULL##": visit.datetime_begin.strftime(DATETIME_FORMAT), + "##V_DATE_START_SHORT##": visit.datetime_begin.strftime(DATE_FORMAT_SHORT), + "##V_DATE_ENDS_FULL##": visit.datetime_end.strftime(DATETIME_FORMAT), + "##V_DATE_ENDS_SHORT##": visit.datetime_end.strftime(DATE_FORMAT_SHORT), + }) + + def _add_subject_replacements(self, replacements, subject): + if subject is not None: + if subject.date_born is not None: + date_born = subject.date_born.strftime(DATE_FORMAT_SHORT) + else: + date_born = None + replacements.update({ + "##S_FULL_NAME##": str(subject), + "##S_FIRST_NAME##": subject.first_name, + "##S_LAST_NAME##": subject.last_name, + "##S_ADDRESS##": subject.address, + "##S_CITY##": subject.city, + "##S_COUNTRY##": subject.country, + "##S_DIAGNOSIS_YEAR##": subject.year_of_diagnosis, + "##S_DATE_ADDED##": subject.date_added.strftime(DATE_FORMAT_SHORT), + "##S_DATE_BORN##": date_born, + "##S_DIAGNOSIS##": subject.diagnosis, + "##S_EMAIL##": subject.email, + "##S_SEX##": subject.get_sex_display(), + "##S_MPOWER_ID##": subject.mpower_id, + "##S_ND_NUMBER##": subject.nd_number, + "##S_PHONE_NUMBER##": subject.phone_number, + "##S_PHONE_NUMBER_2##": subject.phone_number_2, + "##S_PHONE_NUMBER_3##": subject.phone_number_3, + "##S_POST_CODE##": subject.postal_code, + "##S_SCREENING_NUMBER##": subject.screening_number, + "##S_TYPE##": subject.get_type_display(), + '##S_MAIL_LANGUAGE##': subject.default_written_communication_language, + '##S_KNOWN_LANGUAGES##': ", ".join([l.name for l in subject.languages.all()]) + }) diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py index d2425b84027e4f53d47b3187aa3a6cb6b832dcf8..53c798d39a212a4c27969344efa53a59c628b452 100644 --- a/smash/web/models/subject.py +++ b/smash/web/models/subject.py @@ -144,11 +144,6 @@ class Subject(models.Model): verbose_name='Year of diagnosis (YYYY)' ) - dead = models.BooleanField( - verbose_name='Deceased', - default=False, - editable=True - ) information_sent = models.BooleanField( verbose_name='Information sent', default=False @@ -157,6 +152,11 @@ class Subject(models.Model): verbose_name='PD in family', default=False, ) + dead = models.BooleanField( + verbose_name='Deceased', + default=False, + editable=True + ) resigned = models.BooleanField( verbose_name='Resigned', default=False, diff --git a/smash/web/email.py b/smash/web/smash_email.py similarity index 79% rename from smash/web/email.py rename to smash/web/smash_email.py index 6ed3208b503036cf40acde5bef6290fd7cad1b34..51d54ba029b0851bc44847db3e471a972487bde5 100644 --- a/smash/web/email.py +++ b/smash/web/smash_email.py @@ -1,12 +1,13 @@ # coding=utf-8 from django.conf import settings -from django.core.mail import send_mail from django.core.mail import EmailMessage class EmailSender(object): - def send_email(self, subject, body, recipients, cc_recipients=[]): + def send_email(self, subject, body, recipients, cc_recipients=None): + if cc_recipients is None: + cc_recipients = [] email_from = getattr(settings, "DEFAULT_FROM_EMAIL", None) recipient_list = [] for recipient in recipients.split(";"): @@ -23,4 +24,4 @@ class EmailSender(object): message.content_subtype = "html" message.send() -# send_mail(subject, "", email_from, recipient_list, cc=cc_recipients, html_message=body) +# send_mail(subject, "", email_from, recipient_list, cc=cc_recipients, html_message=body) diff --git a/smash/web/static/css/smash.css b/smash/web/static/css/smash.css index 90663bb9f408b41852301da3312b6fe08ba495a6..6a442120428ed1c27543238002cbb35227ead65c 100644 --- a/smash/web/static/css/smash.css +++ b/smash/web/static/css/smash.css @@ -16,4 +16,10 @@ .checkbox { margin-top: 10px !important; +} + +.table-separator { + background-color: #f4f4f4; + font-variant: small-caps; + font-size: 1.5em; } \ No newline at end of file diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js new file mode 100644 index 0000000000000000000000000000000000000000..59ce07adc38e5620daf4570dc4ee41119f0844a7 --- /dev/null +++ b/smash/web/static/js/smash.js @@ -0,0 +1,9 @@ +$(document).ready(function () { + $("#save-and-continue").click(function () { + var form = $(this).parents("form"); + var hidden_field = $("<input type='hidden' name='_continue' value='1' />"); + form.append(hidden_field); + form.submit(); + }); + +}); \ No newline at end of file diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index 0c30d7e84c00da6b6e12a64b0a2455f32fabbe7f..96bcfee3f919f36ce5e1602e799ce529466b53a2 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -366,10 +366,12 @@ desired effect <script src="{% static 'AdminLTE/js/bootstrap.min.js' %}"></script> <!-- AdminLTE Template Helpers (for example- left side bar) --> <script src="{% static 'AdminLTE/js/app.min.js' %}"></script> + <!-- Smash js --> + <script src="{% static 'js/smash.js' %}"></script> <script> var activate = function (page_to_activate) { - var $e = $(".sidebar-menu > li[data-desc='" + page_to_activate + "']"); + var $e = $(".sidebar-menu li[data-desc='" + page_to_activate + "']"); $e.addClass("active"); }; diff --git a/smash/web/templates/appointments/edit.html b/smash/web/templates/appointments/edit.html index f6f59db5cb55c8e8da1eb02492a4cd738d6497e9..c74b9c982ab89bbb7eba9ad2573ec21eb9869108 100644 --- a/smash/web/templates/appointments/edit.html +++ b/smash/web/templates/appointments/edit.html @@ -87,16 +87,22 @@ </fieldset> {% endif %} <div class="box-footer"> - <div class="col-sm-6"> + <div class="col-sm-4"> <button type="submit" class="btn btn-block btn-success">Save</button> </div> - <div class="col-sm-6"> + <div class="col-sm-4"> + <button id="save-and-continue" type="button" class="btn btn-block btn-success">Save and + Continue + </button> + </div> + <div class="col-sm-4"> <a href="{% url 'web.views.appointments' %}" class="btn btn-block btn-default" onclick="history.back()">Cancel</a> </div> </div><!-- /.box-footer --> </form> </div> + {% include 'includes/mail_templates_box.html' with instance_id=appointment.id %} {% endblock %} diff --git a/smash/web/templates/includes/mail_templates_box.html b/smash/web/templates/includes/mail_templates_box.html new file mode 100644 index 0000000000000000000000000000000000000000..392126cc5a27a8b65eec19b8cc258e5cf9e94218 --- /dev/null +++ b/smash/web/templates/includes/mail_templates_box.html @@ -0,0 +1,27 @@ +<div class="box box-success"> + <div class="box-header with-border"> + <h3>Generate Mail</h3> + </div> + <div class="box-body"> + <h4>Available templates:</h4> + <p> + {% for mail_template in mail_templates.0 %} + <a href="{% url "web.views.mail_template_generate" mail_template.id instance_id %}" + class="btn btn-app"> + <i class="fa"> {% autoescape off %}{{ mail_template.language.image_img }}{% endautoescape %}</i> + {{ mail_template.name }} + </a> + {% endfor %} + </p> + <h4>Unavailable templates:</h4> + <p> + {% for mail_template in mail_templates.1 %} + <span + class="btn btn-app disabled"> + <i class="fa"> {% autoescape off %}{{ mail_template.language.image_img }}{% endautoescape %}</i> + {{ mail_template.name }} + </span> + {% endfor %} + <p></p> + </div> +</div> \ No newline at end of file diff --git a/smash/web/templates/languages/add.html b/smash/web/templates/languages/add.html new file mode 100644 index 0000000000000000000000000000000000000000..48a17cd04565f678ccb9fb13114a0d5a7bcfe2d3 --- /dev/null +++ b/smash/web/templates/languages/add.html @@ -0,0 +1,9 @@ +{% extends "languages/add_edit.html" %} + +{% block page_header %}New language{% endblock page_header %} + +{% block title %}{{ block.super }} - Add new language{% endblock %} + +{% block form-title %}Enter language §details{% endblock %} + +{% block save-button %}Add{% endblock %} diff --git a/smash/web/templates/languages/add_edit.html b/smash/web/templates/languages/add_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..794ba7c565fd4f2ef48e60e68b7095cb4c1610c2 --- /dev/null +++ b/smash/web/templates/languages/add_edit.html @@ -0,0 +1,77 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "languages/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">{% block form-title %}Enter language details{% endblock %}</h3> + </div> + + + <form method="post" action="" class="form-horizontal" enctype="multipart/form-data"> + {% csrf_token %} + + <div class="box-body"> + {% for field in form %} + <div class="form-group {% if field.errors %}has-error{% endif %}"> + <label class="col-sm-4 col-lg-offset-1 col-lg-2 control-label"> + {{ field.label }} + </label> + + <div class="col-sm-8 col-lg-4"> + {{ field|add_class:'form-control' }} + {% if field.errors %} + <span class="help-block">{{ field.errors }}</span> + {% endif %} + </div> + + + </div> + {% endfor %} + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">{% block save-button %} + Add{% endblock %} + </button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.languages' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} \ No newline at end of file diff --git a/smash/web/templates/languages/breadcrumb.html b/smash/web/templates/languages/breadcrumb.html new file mode 100644 index 0000000000000000000000000000000000000000..b3194bfb0a32794c726a75682425d7ea4b17f30d --- /dev/null +++ b/smash/web/templates/languages/breadcrumb.html @@ -0,0 +1,2 @@ +<li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li class="active"><a href="{% url 'web.views.languages' %}">Languages</a></li> \ No newline at end of file diff --git a/smash/web/templates/languages/confirm_delete.html b/smash/web/templates/languages/confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..929c61370a2960ce8a0147249fdef6411f5d4a6c --- /dev/null +++ b/smash/web/templates/languages/confirm_delete.html @@ -0,0 +1,62 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block page_header %}Delete language{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block title %}{{ block.super }} - Delete language{% endblock %} + +{% block breadcrumb %} + {% include "languages/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Confirm deletion</h3> + </div> + + <form action="" method="post" class="form-horizontal">{% csrf_token %} + <div class="box-body"> + <p>Are you sure you want to delete language "{{ object.name }}"?</p> + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-danger">Delete</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.languages' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} + + diff --git a/smash/web/templates/languages/edit.html b/smash/web/templates/languages/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..ee96e0910981224b10d2ad0305cdebe40d5b63c9 --- /dev/null +++ b/smash/web/templates/languages/edit.html @@ -0,0 +1,10 @@ +{% extends "mail_templates/add_edit.html" %} + +{% block page_header %}Edit language "{{ language.name }}"{% endblock page_header %} + +{% block title %}{{ block.super }} - Edit language "{{ language.name }}"{% endblock %} + +{% block form-title %}Enter language details{% endblock %} + +{% block save-button %}Save{% endblock %} + diff --git a/smash/web/templates/languages/list.html b/smash/web/templates/languages/list.html new file mode 100644 index 0000000000000000000000000000000000000000..eeb9eb68a3fff742857578282926129f210be213 --- /dev/null +++ b/smash/web/templates/languages/list.html @@ -0,0 +1,76 @@ +{% extends "_base.html" %} +{% load static %} + +{% block styles %} + {{ block.super }} + <!-- DataTables --> + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> +{% endblock styles %} + +{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block page_header %}Languages{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "languages/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + <div> + <a class="btn btn-app" href="{% url 'web.views.language_add' %}"> + <i class="fa fa-plus"></i> Add new language + </a> + </div> + + <div class="box-body"> + <table id="table" class="table table-bordered table-striped"> + <thead> + <tr> + <th>Id</th> + <th>Name</th> + <th>Flag</th> + <th>Order</th> + <th>Locale</th> + <th>Edit</th> + <th>Delete</th> + </tr> + </thead> + <tbody> + {% for language in languages %} + <tr> + <td>{{ language.id }}</td> + <td>{{ language.name }}</td> + <td>{% autoescape off %}{{ language.image_img }}{% endautoescape %}</td> + <td>{{ language.order }}</td> + <td>{{ language.locale }}</td> + <td><a href="{% url 'web.views.language_edit' language.id %}"><i + class="fa fa-edit"></i></a></td> + <td><a href="{% url 'web.views.language_delete' language.id %}"><i + class="fa fa-trash text-danger"></i></a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> + <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> + + <script> + $(function () { + $('#table').DataTable({ + "paging": true, + "lengthChange": false, + "searching": true, + "ordering": true, + "info": true, + "autoWidth": false + }); + }); + </script> +{% endblock scripts %} diff --git a/smash/web/templates/mail_templates/add.html b/smash/web/templates/mail_templates/add.html new file mode 100644 index 0000000000000000000000000000000000000000..561eb1a989caa72dda2f5db7e2d6aabf77e34330 --- /dev/null +++ b/smash/web/templates/mail_templates/add.html @@ -0,0 +1,8 @@ +{% extends "mail_templates/add_edit.html" %} + +{% block page_header %}New mail template{% endblock page_header %} + +{% block title %}{{ block.super }} - Add new mail template{% endblock %} + +{% block form-title %}Enter mail template details{% endblock %} +{% block save-button %}Add{% endblock %} \ No newline at end of file diff --git a/smash/web/templates/mail_templates/add_edit.html b/smash/web/templates/mail_templates/add_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..4fbc937166272b85854f269c6b0d090cb677141a --- /dev/null +++ b/smash/web/templates/mail_templates/add_edit.html @@ -0,0 +1,76 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "mail_templates/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">{% block form-title %}Enter mail template details{% endblock %}</h3> + </div> + + + <form method="post" action="" class="form-horizontal" enctype="multipart/form-data"> + {% csrf_token %} + + <div class="box-body"> + {% for field in form %} + <div class="form-group {% if field.errors %}has-error{% endif %}"> + <label class="col-sm-4 col-lg-offset-1 col-lg-2 control-label"> + {{ field.label }} + </label> + + <div class="col-sm-8 col-lg-4"> + {{ field|add_class:'form-control' }} + {% if field.errors %} + <span class="help-block">{{ field.errors }}</span> + {% endif %} + </div> + + + </div> + {% endfor %} + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">{% block save-button %} + Add{% endblock %}</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.mail_templates' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} \ No newline at end of file diff --git a/smash/web/templates/mail_templates/confirm_delete.html b/smash/web/templates/mail_templates/confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..48b1967d7f4ae545ce63aed1474c707f80f405dc --- /dev/null +++ b/smash/web/templates/mail_templates/confirm_delete.html @@ -0,0 +1,62 @@ +{% extends "_base.html" %} +{% load static %} +{% load filters %} + +{% block styles %} + {{ block.super }} + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/> + +{% endblock styles %} + +{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} +{% block page_header %}Delete mail template{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block title %}{{ block.super }} - Delete mail template{% endblock %} + +{% block breadcrumb %} + {% include "mail_templates/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + {% block content %} + <div class="row"> + <div class="col-md-12"> + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Confirm deletion</h3> + </div> + + <form action="" method="post" class="form-horizontal">{% csrf_token %} + <div class="box-body"> + <p>Are you sure you want to delete mail template "{{ object.name }}"?</p> + </div><!-- /.box-body --> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-danger">Delete</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.mail_templates' %}" + class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </form> + </div> + + </div> + </div> + + {% endblock %} + + +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script> + +{% endblock scripts %} + + diff --git a/smash/web/templates/mail_templates/edit.html b/smash/web/templates/mail_templates/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..d166d86a3b9bd34d14fd6a792b46572e41a53f0a --- /dev/null +++ b/smash/web/templates/mail_templates/edit.html @@ -0,0 +1,9 @@ +{% extends "mail_templates/add_edit.html" %} + +{% block page_header %}Edit mail template "{{ mail_template.name }}"{% endblock page_header %} + +{% block title %}{{ block.super }} - Edit mail template "{{ mail_template.name }}"{% endblock %} + +{% block form-title %}Enter mail template details{% endblock %} + +{% block save-button %}Save{% endblock %} diff --git a/smash/web/templates/mail_templates/list.html b/smash/web/templates/mail_templates/list.html new file mode 100644 index 0000000000000000000000000000000000000000..f403957a7904b464a1a28ca7b817388b3254c9a7 --- /dev/null +++ b/smash/web/templates/mail_templates/list.html @@ -0,0 +1,115 @@ +{% extends "_base.html" %} +{% load static %} + +{% block styles %} + {{ block.super }} + <!-- DataTables --> + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> +{% endblock styles %} + +{% block ui_active_tab %}'mail_templates'{% endblock ui_active_tab %} +{% block page_header %}Mail templates{% endblock page_header %} +{% block page_description %}{% endblock page_description %} + +{% block breadcrumb %} + {% include "mail_templates/breadcrumb.html" %} +{% endblock breadcrumb %} + +{% block maincontent %} + + + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Templates</h3> + </div> + <div class="box-body"> + + <div> + <a class="btn btn-app" href="{% url 'web.views.mail_template_add' %}"> + <i class="fa fa-plus"></i> Add new template + </a> + </div> + <table id="table" class="table table-bordered table-striped"> + <thead> + <tr> + <th>No.</th> + <th>Context</th> + <th>Language</th> + <th>Name</th> + <th>Download</th> + <th>Edit</th> + <th>Delete</th> + </tr> + </thead> + <tbody> + {% for mail_template in mail_templates %} + <tr> + <td>{{ forloop.counter }}</td> + <td>{{ mail_template.get_context_display }}</td> + <td>{% autoescape off %}{{ mail_template.language.image_img }}{% endautoescape %}</td> + <td>{{ mail_template.name }}</td> + <td><a href="{{ mail_template.template_file.url }}"><i class="fa fa-download"></i></a></td> + <td><a href="{% url 'web.views.mail_template_edit' mail_template.id %}"><i + class="fa fa-edit"></i></a></td> + <td><a href="{% url 'web.views.mail_template_delete' mail_template.id %}"><i + class="fa fa-trash text-danger"></i></a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + + <div class="box box-success"> + <div class="box-header with-border"> + <h3 class="box-title">Tags <i class="fa fa-question-circle"></i></h3> + </div> + <div class="box-body"> + <p>The following tags can be used within the Word documents and will be replaced by actual values during the + mail generation.</p> + <table id="table" class="table table-bordered table-striped"> + <thead> + <tr> + <th>Placeholder</th> + <th>Description</th> + <th>Comment</th> + </tr> + </thead> + <tbody> + {% for name, tags in explanations.items %} + <tr> + <td class="table-separator" colspan="3">{{ name | title }}</td> + </tr> + {% for tag in tags %} + <tr> + <td>{{ tag.0 }}</td> + <td>{{ tag.1 }}</td> + <td>{{ tag.2 }}</td> + </tr> + {% endfor %} + {% endfor %} + </tbody> + </table> + </div> + </div> +{% endblock maincontent %} + +{% block scripts %} + {{ block.super }} + + <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> + <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> + + <script> + $(function () { + $('#table').DataTable({ + "paging": true, + "lengthChange": false, + "searching": true, + "ordering": true, + "info": true, + "autoWidth": false + }); + }); + </script> +{% endblock scripts %} diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html index 19cd2afa61d442d314a2d0d5e97871f7a1352ff9..de78cae857ebdc2301f2acd619776bc54fb54795 100644 --- a/smash/web/templates/sidebar.html +++ b/smash/web/templates/sidebar.html @@ -57,12 +57,19 @@ <span>Export</span> </a> </li> - - <li data-desc="configuration"> - <a href="{% url 'web.views.configuration' %}"> - <i class="fa fa-wrench"></i> - <span>Configuration</span> + <li data-desc="configuration" class="treeview"> + <a href="#"> + <i class="fa fa-wrench"></i> <span>Configuration</span> + <span class="pull-right-container"> + <i class="fa fa-angle-left pull-right"></i> + </span> </a> + <ul class="treeview-menu"> + <li><a href="{% url 'web.views.configuration' %}">General</a></li> + <li> + <a href="{% url 'web.views.languages' %}">Languages</a> + </li> + </ul> </li> </ul> \ No newline at end of file diff --git a/smash/web/templates/subjects/edit.html b/smash/web/templates/subjects/edit.html index 3e52807ee3294af7f56bc5956d8465adce753f7a..22c88550ac80adf6f9eb9a8e04e412b804314afb 100644 --- a/smash/web/templates/subjects/edit.html +++ b/smash/web/templates/subjects/edit.html @@ -70,10 +70,15 @@ <div class="box-footer"> - <div class="col-sm-6"> + <div class="col-sm-4"> <button type="submit" class="btn btn-block btn-success">Save</button> </div> - <div class="col-sm-6"> + <div class="col-sm-4"> + <button id="save-and-continue" type="button" class="btn btn-block btn-success">Save and + Continue + </button> + </div> + <div class="col-sm-4"> <a href="{% url 'web.views.subjects' %}" class="btn btn-block btn-default" onclick="history.back()">Cancel</a> </div> @@ -82,6 +87,9 @@ </div><!-- /.box --> </div><!-- /.col-md-12 --> </div><!-- /.row --> + + {% include 'includes/mail_templates_box.html' with instance_id=subject.id %} + <div class="row"> <div class="col-lg-12"> <div class="box box-success"> @@ -123,6 +131,8 @@ </div> </div> + + <div class="modal modal-danger fade" id="confirm-dead-resigned-mark-dialog" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> diff --git a/smash/web/templates/visits/details.html b/smash/web/templates/visits/details.html index d62679f19ab2cc314668c17c6b892f04cced51b3..db9aaab5303d7839042991a5b06bbfa3364231d7 100644 --- a/smash/web/templates/visits/details.html +++ b/smash/web/templates/visits/details.html @@ -80,7 +80,7 @@ </div><!-- /.box-body --> <div class="box-footer"> <div class="col-sm-12"> - <button type="submit" class="btn btn-block btn-success">Save</button> + <button type="submit" class="btn btn-block btn-success">Save and continue</button> </div> </div><!-- /.box-footer --> </form> @@ -182,11 +182,11 @@ </form> </div> - </form> + {% include 'includes/mail_templates_box.html' with instance_id=visit.id %} + {% endblock %} - </div> {% endblock maincontent %} diff --git a/smash/web/tests/__init__.py b/smash/web/tests/__init__.py index 72b9f9adc3193fb034ac7656aea51eae348acddc..6782804f7cfe81faf815c0d9679159eed66a3816 100644 --- a/smash/web/tests/__init__.py +++ b/smash/web/tests/__init__.py @@ -1,9 +1,13 @@ +import os +from django.conf import settings from django.contrib.auth.models import User from django.test import Client from django.test import TestCase from functions import create_worker +settings.MEDIA_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') + class LoggedInTestCase(TestCase): def setUp(self): diff --git a/smash/web/tests/data/template.docx b/smash/web/tests/data/template.docx new file mode 100644 index 0000000000000000000000000000000000000000..17aee71db3c1b8699e27daf1e0701420150976a0 Binary files /dev/null and b/smash/web/tests/data/template.docx differ diff --git a/smash/web/tests/data/upcoming_appointment_FR.docx b/smash/web/tests/data/upcoming_appointment_FR.docx new file mode 100644 index 0000000000000000000000000000000000000000..b118fadd9d2aa89b8a7596ffe1d534e17d33f2d3 Binary files /dev/null and b/smash/web/tests/data/upcoming_appointment_FR.docx differ diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index fea131ec68825d924ccc2e8ec60e6d0d5035d88d..24cd16c2bff10bebb7076afc7b14c00c05354fd7 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -1,8 +1,9 @@ import datetime +import os from django.contrib.auth.models import User -from web.models import Location, AppointmentType, Subject, Worker, Visit, Appointment, ConfigurationItem +from web.models import Location, AppointmentType, Subject, Worker, Visit, Appointment, ConfigurationItem, Language from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL from web.views.notifications import get_today_midnight_date @@ -42,7 +43,8 @@ def create_subject(id=1): sex=SEX_CHOICES_MALE, type=SUBJECT_TYPE_CHOICES_CONTROL, screening_number="piotr's number" + str(id), - country="france") + country="france" + ) def create_user(username=None, password=None): @@ -84,6 +86,8 @@ def create_visit(subject=None): def create_appointment(visit=None, when=None): if visit is None: visit = create_visit() + # if when is None: + # when = get_today_midnight_date() return Appointment.objects.create( visit=visit, length=30, @@ -97,4 +101,14 @@ def create_configuration_item(): item.value = "xxx" item.name = "yyy" item.save() - return item; + return item + + +def create_language(name="French", locale="fr_FR"): + language = Language(name=name, locale=locale) + language.save() + return language + + +def get_resource_path(filename): + return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', filename) diff --git a/smash/web/tests/test_email.py b/smash/web/tests/test_email.py index 879acc7e3b4164b87d40d6c7cbc150f8c3ac9349..460d681334d5d405cb928b4f16ca15a54079af6f 100644 --- a/smash/web/tests/test_email.py +++ b/smash/web/tests/test_email.py @@ -3,7 +3,7 @@ from django.core import mail from django.test import TestCase -from web.email import EmailSender +from web.smash_email import EmailSender class TestEmailSender(TestCase): diff --git a/smash/web/tests/test_model_mail_template.py b/smash/web/tests/test_model_mail_template.py new file mode 100644 index 0000000000000000000000000000000000000000..0460314373a98c926c4dcabf9b725cf9949efd81 --- /dev/null +++ b/smash/web/tests/test_model_mail_template.py @@ -0,0 +1,125 @@ +import StringIO + +from django.test import TestCase +from docx import Document + +from functions import create_language, get_resource_path, create_appointment, create_user, create_subject, \ + create_visit +from web.models import MailTemplate +from web.models.constants import MAIL_TEMPLATE_CONTEXT_APPOINTMENT, MAIL_TEMPLATE_CONTEXT_VISIT, \ + MAIL_TEMPLATE_CONTEXT_SUBJECT +from web.models.mail_template import DATE_FORMAT_SHORT + + +class MailTemplateModelTests(TestCase): + def setUp(self): + self.french_language = create_language("French", "fr_FR") + self.english_language = create_language("English", "en_GB") + self.template_file = get_resource_path('upcoming_appointment_FR.docx') + self.user = create_user() + + def test_get_appointment_mail_templates(self): + context = MAIL_TEMPLATE_CONTEXT_APPOINTMENT + function_to_test = MailTemplate.get_appointment_mail_templates + self.check_get_mail_templates(context, function_to_test) + + def test_get_visit_mail_templates(self): + context = MAIL_TEMPLATE_CONTEXT_VISIT + function_to_test = MailTemplate.get_visit_mail_templates + self.check_get_mail_templates(context, function_to_test) + + def test_get_subject_mail_templates(self): + context = MAIL_TEMPLATE_CONTEXT_SUBJECT + function_to_test = MailTemplate.get_subject_mail_templates + self.check_get_mail_templates(context, function_to_test) + + def check_get_mail_templates(self, context, function_to_test): + # create french template + template_name_french = "test_fr" + appointment_template_french = MailTemplate(name=template_name_french, language=self.french_language, + context=context, + template_file=self.template_file) + appointment_template_french.save() + # create english template + template_name_english = "test_en" + appointment_template_english = MailTemplate(name=template_name_english, language=self.english_language, + context=context, + template_file=self.template_file) + appointment_template_english.save() + active_templates, inactive_templates = function_to_test([self.french_language]) + self.assertEqual(1, len(active_templates), "only on active template should be returned (for french)") + self.assertEqual(1, len(inactive_templates), "only on inactive template should be returned (for english)") + self.assertEqual(template_name_french, active_templates[0].name) + self.assertEqual(template_name_english, inactive_templates[0].name) + # now tries with multiple languages + active_templates, inactive_templates = function_to_test( + [self.english_language, self.french_language]) + self.assertEqual(2, len(active_templates), "two active templates should be returned (for french and english)") + self.assertEqual(0, len(inactive_templates), "no inactive template should be returned") + # also checks the correct sorting + self.assertEqual(template_name_english, active_templates[0].name) + self.assertEqual(template_name_french, active_templates[1].name) + + # tries with no languages + active_templates, inactive_templates = function_to_test([]) + self.assertEqual(0, len(active_templates), "no active templates should be returned") + self.assertEqual(2, len(inactive_templates), "two inactive templates should be returned (french and english)") + + def test_apply_appointment(self): + template_name_french = "test_fr" + appointment = create_appointment() + appointment_template_french = MailTemplate(name=template_name_french, language=self.french_language, + context=MAIL_TEMPLATE_CONTEXT_APPOINTMENT, + template_file=self.template_file) + stream = StringIO.StringIO() + appointment_template_french.apply(appointment, self.user, stream) + doc = Document(stream) + worker_name = str(self.user.worker) + location = appointment.location.name + self.check_doc_contains(doc, [worker_name, location]) + + def test_apply_subject(self): + template_name_french = "test_fr" + subject = create_subject() + subject_template_french = MailTemplate(name=template_name_french, language=self.french_language, + context=MAIL_TEMPLATE_CONTEXT_SUBJECT, + template_file=self.template_file) + stream = StringIO.StringIO() + subject_template_french.apply(subject, self.user, stream) + doc = Document(stream) + worker_name = str(self.user.worker) + + self.check_doc_contains(doc, [worker_name, str(subject), subject.country, subject.nd_number, + subject.get_type_display()]) + + def test_apply_visit(self): + template_name_french = "test_fr" + visit = create_visit() + visit_template_french = MailTemplate(name=template_name_french, language=self.french_language, + context=MAIL_TEMPLATE_CONTEXT_VISIT, + template_file=self.template_file) + stream = StringIO.StringIO() + visit_template_french.apply(visit, self.user, stream) + doc = Document(stream) + worker_name = str(self.user.worker) + + self.check_doc_contains(doc, [worker_name, str(visit.subject), visit.subject.country, visit.subject.nd_number, + visit.subject.get_type_display(), + visit.datetime_begin.strftime(DATE_FORMAT_SHORT), + visit.datetime_end.strftime(DATE_FORMAT_SHORT)]) + + def check_doc_contains(self, doc, needles): + founds = [False] * len(needles) + all_founds = False + count_found = 0 + for paragraph in doc.paragraphs: + for i in range(0, len(needles)): + if not founds[i] and needles[i] in paragraph.text: + founds[i] = True + count_found += 1 + if count_found == len(needles): + all_founds = True + break + if not all_founds: + for i in range(0, len(needles)): + self.assertTrue(founds[i], "{} was not found in the generated Word document".format(needles[i])) diff --git a/smash/web/tests/test_process_file.py b/smash/web/tests/test_process_file.py new file mode 100644 index 0000000000000000000000000000000000000000..413530a2a102ef9fbf36510de2f8c4eddcc6b79a --- /dev/null +++ b/smash/web/tests/test_process_file.py @@ -0,0 +1,29 @@ +import datetime +import locale +import tempfile + +import os +from django.test import TestCase + +from functions import get_resource_path +from web.docx_helper import process_file + + +class TestDocxProcessor(TestCase): + def test_process_file(self): + template_path = get_resource_path('template.docx') + locale.setlocale(locale.LC_TIME, "fr_FR") + output_path = tempfile.mktemp() + changes = { + "##SIR##": "Mr", + "##NAME##": "Jan", + "##SURNAME##": "Weglarz", + "##ADDRESS1##": "Piotrowo 23", + "##ADDRESS2##": "61-234, Poznan", + "##COUNTRY##": "POLAND", + "##CONTENT##": "1", + "##DATE##": datetime.datetime.now().date().strftime("%A %-d %B %Y"), + } + process_file(template_path, output_path, changes) + self.assertTrue(os.path.isfile(output_path)) + os.remove(output_path) diff --git a/smash/web/urls.py b/smash/web/urls.py index 83a16c80987041c894842f77b1427cf6420aeb34..d57fa428213f97060c88501bb153cfd9becd1d4d 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -68,7 +68,8 @@ urlpatterns = [ # CONTACTS # #################### - url(r'^subjects/(?P<subject_id>\d+)/contacts/add$', views.contact_attempt.contact_add, name='web.views.contact_add'), + url(r'^subjects/(?P<subject_id>\d+)/contacts/add$', views.contact_attempt.contact_add, + name='web.views.contact_add'), #################### # DOCTORS # @@ -100,7 +101,26 @@ urlpatterns = [ # MAIL # #################### - url(r'^mail_templates$', views.mails.mail_templates, name='web.views.mail_templates'), + url(r'^mail_templates_old$', views.mails.mail_templates, name='web.views.mail_templates_old'), + url(r'^mail_templates$', views.mails.MailTemplatesListView.as_view(), name='web.views.mail_templates'), + url(r'^mail_templates/add$', views.mails.MailTemplatesCreateView.as_view(), name='web.views.mail_template_add'), + url(r'^mail_templates/(?P<pk>\d+)/delete$', views.mails.MailTemplatesDeleteView.as_view(), + name='web.views.mail_template_delete'), + url(r'^mail_templates/(?P<pk>\d+)/edit$', views.mails.MailTemplatesEditView.as_view(), + name='web.views.mail_template_edit'), + url(r'^mail_templates/(?P<mail_template_id>\d+)/generate/(?P<instance_id>\d+)$', views.mails.generate, + name="web.views.mail_template_generate"), + + #################### + # LANGUAGES # + #################### + + url(r'^languages$', views.language.LanguageListView.as_view(), name='web.views.languages'), + url(r'^languages/add$', views.language.LanguageCreateView.as_view(), name='web.views.language_add'), + url(r'^languages/(?P<pk>\d+)/delete$', views.language.LanguageDeleteView.as_view(), + name='web.views.language_delete'), + url(r'^languages/(?P<pk>\d+)/edit$', views.language.LanguageEditView.as_view(), + name='web.views.language_edit'), #################### # STATISTICS # diff --git a/smash/web/views/__init__.py b/smash/web/views/__init__.py index e78ca58cbcfbf9adab1177766bbcedea96fd51ca..4322cb370c651c5e613bf6f708f68a559aa7c315 100644 --- a/smash/web/views/__init__.py +++ b/smash/web/views/__init__.py @@ -2,6 +2,7 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render +from django.views.generic.base import ContextMixin from notifications import get_notifications from ..models import Worker @@ -36,18 +37,26 @@ def e400_bad_request(request, context=None): @login_required def wrap_response(request, template, params): - person, role = Worker.get_details(request.user) + final_params = extend_context(params, request) + return render(request, template, final_params) - notifications = get_notifications(request.user) +def extend_context(params, request): + person, role = Worker.get_details(request.user) + notifications = get_notifications(request.user) final_params = params.copy() final_params.update({ 'person': person, 'role': role, 'notifications': notifications }) + return final_params - return render(request, template, final_params) + +class WrappedView(ContextMixin): + def get_context_data(self, **kwargs): + context = super(WrappedView, self).get_context_data(**kwargs) + return extend_context(context, self.request) import auth @@ -62,3 +71,4 @@ import statistics import export import contact_attempt import configuration_item +import language diff --git a/smash/web/views/appointment.py b/smash/web/views/appointment.py index 760b83884517ec462f60df31e0dc51ed17bf9b41..25841372be897b94d191d2228010ceb7a772bb00 100644 --- a/smash/web/views/appointment.py +++ b/smash/web/views/appointment.py @@ -1,9 +1,10 @@ # coding=utf-8 +from django.contrib import messages from django.shortcuts import get_object_or_404, redirect from . import wrap_response from ..forms import AppointmentDetailForm, AppointmentAddForm, AppointmentEditForm, SubjectEditForm -from ..models import Appointment, Subject +from ..models import Appointment, Subject, MailTemplate APPOINTMENT_LIST_GENERIC = "GENERIC" APPOINTMENT_LIST_UNFINISHED = "UNFINISHED" @@ -81,6 +82,9 @@ def appointment_edit(request, id): subject.information_sent = True subject.save() + messages.success(request, "Modifications saved") + if '_continue' in request.POST: + return redirect('web.views.appointment_edit', id=the_appointment.id) if (the_appointment.status != Appointment.APPOINTMENT_STATUS_SCHEDULED) and ( the_appointment.visit is not None): return redirect('web.views.visit_details', id=the_appointment.visit.id) @@ -91,9 +95,16 @@ def appointment_edit(request, id): if the_appointment.visit is not None: subject_form = SubjectEditForm(instance=the_appointment.visit.subject, prefix="subject") + languages = [] + if the_appointment.visit is not None: + subject = the_appointment.visit.subject + if subject.default_written_communication_language: + languages.append(subject.default_written_communication_language) + languages.extend(subject.languages.all()) return wrap_response(request, 'appointments/edit.html', { 'form': appointment_form, 'subject_form': subject_form, 'id': id, - 'appointment': the_appointment + 'appointment': the_appointment, + 'mail_templates': MailTemplate.get_appointment_mail_templates(languages) }) diff --git a/smash/web/views/language.py b/smash/web/views/language.py new file mode 100644 index 0000000000000000000000000000000000000000..463210b74ece287296f76634dd29a4a41e8d4df9 --- /dev/null +++ b/smash/web/views/language.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from django.contrib import messages +from django.urls import reverse_lazy +from django.views.generic import CreateView +from django.views.generic import DeleteView +from django.views.generic import ListView +from django.views.generic import UpdateView + +from . import WrappedView +from ..models import Language + + +class LanguageListView(ListView, WrappedView): + model = Language + context_object_name = "languages" + template_name = 'languages/list.html' + + +class LanguageCreateView(CreateView, WrappedView): + model = Language + template_name = "languages/add.html" + fields = '__all__' + success_url = reverse_lazy('web.views.languages') + success_message = "Template created" + + +class LanguageDeleteView(DeleteView, WrappedView): + model = Language + success_url = reverse_lazy('web.views.languages') + template_name = 'languages/confirm_delete.html' + + def delete(self, request, *args, **kwargs): + messages.success(request, "Template deleted") + return super(LanguageDeleteView, self).delete(request, *args, **kwargs) + + +class LanguageEditView(UpdateView, WrappedView): + model = Language + success_url = reverse_lazy('web.views.languages') + fields = '__all__' + success_message = "Template edited" + template_name = "languages/edit.html" + context_object_name = "language" + + +def generate(request, mail_template_id, instance_id): + return None diff --git a/smash/web/views/mails.py b/smash/web/views/mails.py index 041daa092e41c729c55e0d0262d1f4e5352191d4..abe5d7f79cf1d12b087d2da1c5b66a9314f4ee1b 100644 --- a/smash/web/views/mails.py +++ b/smash/web/views/mails.py @@ -1,6 +1,86 @@ # coding=utf-8 -from . import wrap_response +import StringIO +from wsgiref.util import FileWrapper + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.views.generic import CreateView +from django.views.generic import DeleteView +from django.views.generic import ListView +from django.views.generic import UpdateView + +from . import wrap_response, WrappedView +from ..models import Subject, Visit, Appointment, MailTemplate +from ..models.constants import MAIL_TEMPLATE_CONTEXT_SUBJECT, MAIL_TEMPLATE_CONTEXT_VISIT, \ + MAIL_TEMPLATE_CONTEXT_APPOINTMENT + +MIMETYPE_DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + +CONTEXT_TYPES_MAPPING = { + MAIL_TEMPLATE_CONTEXT_SUBJECT: Subject, + MAIL_TEMPLATE_CONTEXT_VISIT: Visit, + MAIL_TEMPLATE_CONTEXT_APPOINTMENT: Appointment +} def mail_templates(request): return wrap_response(request, "mail_templates/index.html", {}) + + +class MailTemplatesListView(ListView, WrappedView): + model = MailTemplate + context_object_name = "mail_templates" + template_name = 'mail_templates/list.html' + + def get_context_data(self, **kwargs): + context = super(MailTemplatesListView, self).get_context_data() + context['explanations'] = {"generic": MailTemplate.MAILS_TEMPLATE_GENERIC_TAGS, + "subject": MailTemplate.MAILS_TEMPLATE_SUBJECT_TAGS, + "visit": MailTemplate.MAILS_TEMPLATE_VISIT_TAGS, + "appointment": MailTemplate.MAILS_TEMPLATE_APPOINTMENT_TAGS, + } + return context + + +class MailTemplatesCreateView(CreateView, WrappedView): + model = MailTemplate + template_name = "mail_templates/add.html" + fields = '__all__' + success_url = reverse_lazy('web.views.mail_templates') + success_message = "Template created" + + +class MailTemplatesDeleteView(DeleteView, WrappedView): + model = MailTemplate + success_url = reverse_lazy('web.views.mail_templates') + template_name = 'mail_templates/confirm_delete.html' + + def delete(self, request, *args, **kwargs): + messages.success(request, "Template deleted") + return super(MailTemplatesDeleteView, self).delete(request, *args, **kwargs) + + +class MailTemplatesEditView(UpdateView, WrappedView): + model = MailTemplate + success_url = reverse_lazy('web.views.mail_templates') + fields = '__all__' + success_message = "Template edited" + template_name = "mail_templates/edit.html" + context_object_name = "mail_template" + + +@login_required +def generate(request, mail_template_id, instance_id): + mail_template = get_object_or_404(MailTemplate, id=mail_template_id) + instance = get_object_or_404(CONTEXT_TYPES_MAPPING[mail_template.context], id=instance_id) + stream = StringIO.StringIO() + stream = mail_template.apply(instance, request.user, stream) + file_size = stream.tell() + stream.seek(0) + response = HttpResponse(FileWrapper(stream), content_type=MIMETYPE_DOCX) + response['Content-Length'] = file_size + response['Content-Disposition'] = 'attachment; filename={}.docx'.format(mail_template.name) + return response diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index 06615727752569a3978646cd48cea3dab812625f..f0e96b4a167646acfbf5cb2aada2b973ad30a3d6 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -2,15 +2,15 @@ from django.contrib import messages from django.shortcuts import redirect, get_object_or_404 -from notifications import get_subjects_with_no_visit, get_subjects_with_reminder from . import wrap_response from ..forms import SubjectAddForm, SubjectEditForm, VisitDetailForm, get_prefix_screening_number -from ..models import Subject, Worker +from ..models import Subject, Worker, MailTemplate SUBJECT_LIST_GENERIC = "GENERIC" SUBJECT_LIST_NO_VISIT = "NO_VISIT" SUBJECT_LIST_REQUIRE_CONTACT = "REQUIRE_CONTACT" + def subjects(request): context = { 'list_type': SUBJECT_LIST_GENERIC, @@ -61,14 +61,23 @@ def subject_edit(request, id): the_subject.mark_as_dead() if form.cleaned_data['resigned'] and not was_resigned: the_subject.mark_as_resigned() + messages.success(request, "Modifications saved") + if '_continue' in request.POST: + return redirect('web.views.subject_edit', id=the_subject.id) return redirect('web.views.subjects') else: form = SubjectEditForm(instance=the_subject, was_dead=was_dead, was_resigned=was_resigned) + languages = [] + if the_subject.default_written_communication_language: + languages.append(the_subject.default_written_communication_language) + languages.extend(the_subject.languages.all()) + return wrap_response(request, 'subjects/edit.html', { 'form': form, 'subject': the_subject, - 'contact_attempts': contact_attempts + 'contact_attempts': contact_attempts, + 'mail_templates': MailTemplate.get_subject_mail_templates(languages) }) diff --git a/smash/web/views/visit.py b/smash/web/views/visit.py index 4d2319cdd31642359eeb2a5fbc29189845fe9722..b4f11304a30f1d2417424c1779f98ff5682bac45 100644 --- a/smash/web/views/visit.py +++ b/smash/web/views/visit.py @@ -6,7 +6,7 @@ from notifications import get_active_visits_with_missing_appointments, get_unfin waiting_for_appointment from . import wrap_response from ..forms import VisitDetailForm, SubjectDetailForm, VisitAddForm -from ..models import Visit, Appointment, Subject +from ..models import Visit, Appointment, Subject, MailTemplate def visits(request): @@ -61,6 +61,10 @@ def visit_details(request, id): can_finish = False subject_form = SubjectDetailForm(instance=displayed_subject) + languages = [] + if displayed_subject.default_written_communication_language: + languages.append(displayed_subject.default_written_communication_language) + languages.extend(displayed_subject.languages.all()) return wrap_response(request, 'visits/details.html', { 'vform': visit_form, @@ -69,7 +73,8 @@ def visit_details(request, id): 'visFinished': visit_finished, 'canFinish': can_finish, 'vid': visit_id, - 'visit': displayed_visit}) + 'visit': displayed_visit, + 'mail_templates': MailTemplate.get_visit_mail_templates(languages)}) def visit_mark(request, id, as_what):