diff --git a/cloud/endagaweb/admin.py b/cloud/endagaweb/admin.py index d6b10113..81365748 100644 --- a/cloud/endagaweb/admin.py +++ b/cloud/endagaweb/admin.py @@ -72,3 +72,4 @@ class BTSLogfileAdmin(admin.ModelAdmin): admin.site.register(models.UserProfile, UserProfileAdmin) admin.site.register(models.BTSLogfile, BTSLogfileAdmin) admin.site.register(models.FileUpload) +admin.site.register(models.Notification) diff --git a/cloud/endagaweb/forms/dashboard_forms.py b/cloud/endagaweb/forms/dashboard_forms.py index 2f9a6ddc..4c427a0d 100644 --- a/cloud/endagaweb/forms/dashboard_forms.py +++ b/cloud/endagaweb/forms/dashboard_forms.py @@ -368,3 +368,54 @@ def __init__(self, *args, **kwargs): self.helper.form_action = '/dashboard/staff/tower-monitoring' self.helper.add_input(Submit('submit', 'Select')) self.helper.layout = Layout('tower') + + +class NotificationForm(forms.Form): + types = ( + ('automatic', 'Automatic'), + ('mapped', 'Mapped') + ) + help_text = ( + 'Automatic: Sent to user automatically,
' + 'Mapped: Notification will be sent to mapped ' + 'users.') + type = forms.ChoiceField( + required=True, + label='', + help_text=help_text, choices=types, + widget=forms.RadioSelect(attrs={'title': 'Notification type'}),) + event = forms.CharField(widget=forms.TextInput( + attrs={'title': 'Event Type', 'style': 'width:300px'}), + required=True, label='Events') + message = forms.CharField( + label='Message', widget=forms.Textarea( + attrs={'title': 'Notification message', + 'placeholder': 'Enter Message...', + 'rows': '4'}), required=True, + min_length=20, + max_length=160) + number = forms.IntegerField(widget=forms.NumberInput( + attrs={'class': 'form-control', 'pattern': '[0-9]{3}', + 'title': 'Notification number', 'style': 'width:200px', + 'oninvalid': "setCustomValidity('Enter number (max: 3 digits)')", + 'onchange': "try{" + "setCustomValidity('')}catch(e){}" + }), + required=True, disabled=True, min_value=1, max_value=999) + pk = forms.CharField(widget=forms.HiddenInput()) + + def __init__(self, *args, **kwargs): + super(NotificationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_id = 'network-notification-form' + self.helper.form_method = 'POST' + self.helper.form_action = '/dashboard/network/notification' + + self.helper.layout = Layout( + 'type', + 'number', + 'event', + 'message', + 'pk', + Submit('submit', 'Submit', css_class='invisible'), + ) diff --git a/cloud/endagaweb/locale/en/LC_MESSAGES/django.po b/cloud/endagaweb/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..708b9cc0 --- /dev/null +++ b/cloud/endagaweb/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,18 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-19 08:17+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" diff --git a/cloud/endagaweb/locale/es/LC_MESSAGES/django.po b/cloud/endagaweb/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..fc6bdd24 --- /dev/null +++ b/cloud/endagaweb/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,19 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-19 08:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" diff --git a/cloud/endagaweb/locale/fil/LC_MESSAGES/django.po b/cloud/endagaweb/locale/fil/LC_MESSAGES/django.po new file mode 100644 index 00000000..6d304599 --- /dev/null +++ b/cloud/endagaweb/locale/fil/LC_MESSAGES/django.po @@ -0,0 +1,19 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-19 08:17+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + diff --git a/cloud/endagaweb/locale/id/LC_MESSAGES/django.po b/cloud/endagaweb/locale/id/LC_MESSAGES/django.po new file mode 100644 index 00000000..c42fcc84 --- /dev/null +++ b/cloud/endagaweb/locale/id/LC_MESSAGES/django.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-07-19 08:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + diff --git a/cloud/endagaweb/models.py b/cloud/endagaweb/models.py index 65b0780f..25976fed 100644 --- a/cloud/endagaweb/models.py +++ b/cloud/endagaweb/models.py @@ -1793,3 +1793,17 @@ class FileUpload(models.Model): created_time = models.DateTimeField(auto_now_add=True) modified_time = models.DateTimeField(auto_now_add=True) accessed_time = models.DateTimeField(auto_now=True) + + +class Notification(models.Model): + notification_type = ( + ('automatic', 'Automatic'), + ('mapped', 'Mapped') + ) + network = models.ForeignKey('Network', on_delete=models.CASCADE) + event = models.CharField(max_length=100, null=True, unique=True) + number = models.CharField(max_length=3, null=True, default=None, + unique=True) + message = models.TextField(max_length=160) + type = models.CharField(max_length=10, choices=notification_type, + default='automatic') diff --git a/cloud/endagaweb/settings/prod.py b/cloud/endagaweb/settings/prod.py index c94de28d..20392b8c 100644 --- a/cloud/endagaweb/settings/prod.py +++ b/cloud/endagaweb/settings/prod.py @@ -20,6 +20,8 @@ import dj_database_url +from django.utils.translation import ugettext, ugettext_lazy as translate + # inherit base Django settings (more general than the endagaweb app) from settings import * # noqa: F403 @@ -92,6 +94,7 @@ 'django.middleware.security.SecurityMiddleware', 'endagaweb.middleware.TimezoneMiddleware', 'endagaweb.middleware.MultiNetworkMiddleware', + 'django.middleware.locale.LocaleMiddleware', ) AUTHENTICATION_BACKENDS = ( @@ -129,6 +132,7 @@ 'guardian', 'rest_framework', 'rest_framework.authtoken', + 'rosetta' ] SITE_ID = 1 @@ -320,3 +324,20 @@ # Security middleware settings SECURE_CONTENT_TYPE_NOSNIFF = True + + +# I18 integration for internationalization and localization +USE_I18N = True +LANGUAGES = ( + ('en', translate('English')), + ('fil', translate('Filipino')), + ('es', translate('Spanish')), + ('id', translate('Indonesian')) +) +# Set the default language for your site. +LANGUAGE_CODE = 'en' + +LOCALE_PATHS = ( + os.path.join(os.environ["ENDAGA_BASE_PATH"], 'locale'), +) +TEMPLATES_PATH = os.path.join(os.environ["ENDAGA_BASE_PATH"], 'templates') diff --git a/cloud/endagaweb/tasks.py b/cloud/endagaweb/tasks.py index 405ca698..5c904bf9 100644 --- a/cloud/endagaweb/tasks.py +++ b/cloud/endagaweb/tasks.py @@ -17,6 +17,7 @@ import os import paramiko import zipfile +import subprocess try: # we only import zlib here to check that it is available # (why would it not be?), so we have to disable the 'unused' warning @@ -43,6 +44,7 @@ from endagaweb.models import SystemEvent from endagaweb.models import TimeseriesStat from endagaweb.ic_providers.nexmo import NexmoProvider +from endagaweb.settings.prod import TEMPLATES_PATH @app.task(bind=True) @@ -439,3 +441,24 @@ def req_bts_log(self, obj, retry_delay=60*10, max_retries=432): raise finally: obj.save() + +@app.task(bind=True) +def translate(self, message, retry_delay=60*10, max_retries=432): + """Tries to write notification message for translation. + + The default retry is every 10 min for 3 days. + """ + print "writing network notification message for translation '%s'" + try: + translation_file = "/dashboard/network_detail/translate.html" + handle = open(TEMPLATES_PATH + translation_file, 'a+') + handle.write('{% trans "' + message + '" %}\r\n') + handle.close() + subprocess.Popen( + ['python', 'manage.py', 'makemessages', '-l', 'en', '-l', 'fil']) + subprocess.Popen(['python', 'manage.py', 'compilemessages']) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + raise self.retry(countdown=retry_delay, max_retries=max_retries) + except Exception as exception: + raise self.retry(countdown=retry_delay, max_retries=max_retries) + print "Translation ERROR. Exception:- %s" % (exception) diff --git a/cloud/endagaweb/templates/dashboard/layout.html b/cloud/endagaweb/templates/dashboard/layout.html index c4567a7f..ca017211 100644 --- a/cloud/endagaweb/templates/dashboard/layout.html +++ b/cloud/endagaweb/templates/dashboard/layout.html @@ -120,6 +120,7 @@
  • {% if user_profile.user.is_staff %}
  •   Django Admin
  • +
  •   Language Translations
  •   All Numbers
  •   All Towers
  •   Margin Analysis
  • diff --git a/cloud/endagaweb/templates/dashboard/network_detail/header.html b/cloud/endagaweb/templates/dashboard/network_detail/header.html index 4e9d3965..87dd142c 100644 --- a/cloud/endagaweb/templates/dashboard/network_detail/header.html +++ b/cloud/endagaweb/templates/dashboard/network_detail/header.html @@ -13,6 +13,9 @@

    {% if network.name %} "{{ network.name }}" {% endif %} + {% if active_tab == 'network-notifications' %} + Add Notification + {% endif %}

    diff --git a/cloud/endagaweb/templates/dashboard/network_detail/nav.html b/cloud/endagaweb/templates/dashboard/network_detail/nav.html index d2c2d396..8b68f868 100644 --- a/cloud/endagaweb/templates/dashboard/network_detail/nav.html +++ b/cloud/endagaweb/templates/dashboard/network_detail/nav.html @@ -30,6 +30,13 @@ {% endif %} ">Denominations + + + +
  • + SMS Broadcast +
  • - + \ No newline at end of file diff --git a/cloud/endagaweb/templates/dashboard/network_detail/notifications.html b/cloud/endagaweb/templates/dashboard/network_detail/notifications.html new file mode 100644 index 00000000..b4e1bd31 --- /dev/null +++ b/cloud/endagaweb/templates/dashboard/network_detail/notifications.html @@ -0,0 +1,297 @@ +{% extends "dashboard/layout.html" %} +{% comment %} +Copyright (c) 2016-present, Facebook, Inc. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +{% endcomment %} +{% load apptags %} +{% load humanize %} +{% load crispy_forms_tags %} +{% load render_table from django_tables2 %} + + +{% block title %} +{% if network.name %} +{% tmpl_const "SITENAME" %} | "{{ network.name }}" +{% else %} +{% tmpl_const "SITENAME" %} | Network +{% endif %} +{% endblock %} + +{% block pagestyle %} + +{% endblock %} + +{% block content %} +{% include "dashboard/network_detail/header.html" with network=network active_tab='network-notifications' %} + +
    + {% include "dashboard/network_detail/nav.html" with active_tab='network-notifications' %} +
    + {% for message in messages %} +
    + × + {{ message }} +
    + {% endfor %} +
    +
    + {% csrf_token %} + {% if records > 0 %} + {% render_table notification_table %} + +
    + {% else %} +

    No notifications in the current network

    + {% endif %} +
    +
    + +
    + + + + +{% include "dashboard/subscriber_detail/broadcast.html" with target='network' %} +{% endblock %} +{% block js %} + + +{% endblock %} diff --git a/cloud/endagaweb/templates/dashboard/network_detail/translate.html b/cloud/endagaweb/templates/dashboard/network_detail/translate.html new file mode 100644 index 00000000..bd48b0d8 --- /dev/null +++ b/cloud/endagaweb/templates/dashboard/network_detail/translate.html @@ -0,0 +1,8 @@ +{# +Copyright (c) 2016-present, Facebook, Inc. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +#} \ No newline at end of file diff --git a/cloud/endagaweb/urls.py b/cloud/endagaweb/urls.py index d2b8d10c..c4d48b16 100644 --- a/cloud/endagaweb/urls.py +++ b/cloud/endagaweb/urls.py @@ -153,6 +153,9 @@ name='network-edit'), url(r'^dashboard/network/select/(?P[0-9]+)$', endagaweb.views.network.NetworkSelectView.as_view()), + url(r'^dashboard/network/notification$', + endagaweb.views.network.NetworkNotifications.as_view(), + name='network-notifications'), # The activity table. url(r'^dashboard/activity', endagaweb.views.dashboard.ActivityView.as_view(), @@ -211,6 +214,11 @@ urlpatterns += [ url(r'^django-admin/', include(admin.site.urls)), ] + # Only use rosetta when django-admin is used for security. + if 'rosetta' in settings.INSTALLED_APPS: + urlpatterns += [ + url(r'^rosetta/', include('rosetta.urls')), + ] # We only install the loginas app in the staff version of the site and we hide @@ -223,3 +231,5 @@ url(r'^file/(?P.+)$', endagaweb.views.file_upload.file_view, name='file-upload') ] + + diff --git a/cloud/endagaweb/views/django_tables.py b/cloud/endagaweb/views/django_tables.py index fe8daf18..068ab98d 100644 --- a/cloud/endagaweb/views/django_tables.py +++ b/cloud/endagaweb/views/django_tables.py @@ -365,3 +365,25 @@ def render_action(self, record): element += "Delete" % (record.id) return safestring.mark_safe(element) + + +class NotificationTable(tables.Table): + """Notification table """ + + class Meta: + model = models.Notification + fields = ('id','type', 'event', 'number', 'message') + attrs = {'class': 'table'} + + id = tables.CheckBoxColumn(accessor="pk", + attrs={"th__input":{"onclick": "toggle(this)"}}) + type = tables.Column(verbose_name='Type') + event = tables.Column(verbose_name='Event') + number = tables.Column(verbose_name='Number') + message = tables.Column(verbose_name='Message', orderable=False) + + def render_message(self, record): + message = record.message + if len(record.message) > 60: + message = message[:60]+'...(truncated)' + return message diff --git a/cloud/endagaweb/views/network.py b/cloud/endagaweb/views/network.py index 318335c4..50d4cfc2 100644 --- a/cloud/endagaweb/views/network.py +++ b/cloud/endagaweb/views/network.py @@ -16,9 +16,10 @@ from django import template from django.contrib import messages from django.core import urlresolvers -from django.db import transaction -from django.shortcuts import redirect +from django.db import transaction, IntegrityError +from django.shortcuts import redirect, render import django_tables2 as tables +from django.template.loader import get_template from guardian.shortcuts import get_objects_for_user from django.conf import settings @@ -28,7 +29,7 @@ from endagaweb.forms import dashboard_forms from endagaweb.views.dashboard import ProtectedView from endagaweb.views import django_tables - +from endagaweb import tasks NUMBER_COUNTRIES = { 'US': 'United States (+1)', @@ -651,3 +652,112 @@ def delete(self, request): extra_tags='alert alert-danger') return http.HttpResponse(json.dumps(response), content_type="application/json") + + +class NetworkNotifications(ProtectedView): + """Manage event notifications for network. """ + + def get(self, request): + """Handles GET requests. + Show event-notification listing page""" + user_profile = models.UserProfile.objects.get(user=request.user) + network = user_profile.network + notifications = models.Notification.objects.filter(network=network) + notification_table = django_tables.NotificationTable( + list(notifications)) + tables.RequestConfig(request, paginate={'per_page': 10}).configure( + notification_table) + + notification_id = request.GET.get('id', None) + if notification_id: + response = { + 'status': 'ok', + 'messages': [], + 'data': {} + } + notification = models.Notification.objects.get(id=notification_id) + notification_data = { + 'id': notification.id, + 'number': notification.number, + 'event': notification.event, + 'message': notification.message, + 'type': notification.type + } + response["data"] = notification_data + return http.HttpResponse(json.dumps(response), + content_type="application/json") + + # Set the response context. + context = { + 'networks': get_objects_for_user(request.user, 'view_network', + klass=models.Network), + 'user_profile': user_profile, + 'notification': dashboard_forms.NotificationForm( + initial={'type': 'automatic'}), + 'notification_table': notification_table, + 'records': len(list(notifications)), + 'network': network, + } + # Render template. + template = get_template( + 'dashboard/network_detail/notifications.html') + html = template.render(context, request) + return http.HttpResponse(html) + + def post(self, request): + """Handles POST requests. + Create/edit/edit notifications.""" + delete_notification = request.POST.getlist('id') or None + if delete_notification is None: + # Create/Edit the notifications + user_profile = models.UserProfile.objects.get(user=request.user) + network = user_profile.network + type = request.POST.get('type') + event = request.POST.get('event') + message = request.POST.get('message') + number = request.POST.get('number') + pk = request.POST.get('pk') or 0 + if type == 'automatic': + number = None + else: + event = None + # Format number to 3 digits + if int(number) < 10: + number = '00' + str(number) + elif int(number) < 100: + number = '0' + str(number) + try: + with transaction.atomic(): + try: + # Check for existing notification and update + notification = models.Notification.objects.get(id=pk) + alert_message = 'Notification updated!' + except models.Notification.DoesNotExist: + # Create new notification + notification = models.Notification.objects.create( + network=network) + alert_message = 'Notification added successfully!' + notification.type = type + notification.message = message + notification.event = event + notification.number = number + notification.save() + # Write message to template for parsing and translation + tasks.translate(message) + message = 'Notification added successfully!' + messages.success(request, message) + except IntegrityError: + alert_message = '{0} notification already exists!'.format( + str(type).title()) + messages.error(request, alert_message, + extra_tags="alert alert-danger") + return redirect(urlresolvers.reverse('network-notifications')) + else: + # Delete the notifications + records = models.Notification.objects.filter( + id__in=delete_notification) + for notification in records: + notification.delete() + alert_message = 'Selected notification(s) deleted successfully.' + messages.success(request, alert_message) + return redirect(urlresolvers.reverse('network-notifications')) diff --git a/cloud/requirements.txt b/cloud/requirements.txt index 84248734..124f8a29 100644 --- a/cloud/requirements.txt +++ b/cloud/requirements.txt @@ -47,3 +47,4 @@ tornado==3.2.2 uWSGI==2.0.12 urllib3[secure]>=1.18.1 xlrd==0.9.3 +django-rosetta \ No newline at end of file