├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── __pycache__ └── .gitignore ├── common.py ├── connector_manager.py ├── controllers.py ├── databases └── README.md ├── filter_manager.py ├── interceptor_manager.py ├── jtel.py ├── models.py ├── route_manager.py ├── settings.py ├── static ├── README.md ├── css │ └── no.css └── js │ └── utils.js ├── stats.py ├── super_admin.py ├── tasks.py ├── templates ├── README copy.md ├── README.md ├── auth.html ├── generic.html ├── groups_list.html ├── http_connector_list.html ├── httpapi_stats.html ├── index.html ├── layout.html ├── layout_auth.html ├── layout_sav.html ├── list_filters.html ├── list_interceptors.html ├── list_moroutes.html ├── mt_routes_list.html ├── record_content.html ├── s_users.html ├── show_record.html ├── smpp_connector_list.html ├── smppc_stats.html ├── smppcs_stats.html ├── smppsapi_stats.html ├── stats.html ├── superadmin_index.html ├── user_creds.html ├── user_stats.html ├── users_list.html └── users_stats.html ├── translations └── it.json ├── user_manager.py └── utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | databases/* 2 | uploads/* 3 | *.py[oc] 4 | password.txt 5 | *.log 6 | *.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 John Bannister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jasmin SMS GUI 2 | 3 | A [Py4web](https://py4web.com/) GUI application for managing and monitoring all aspects of a a single instance of the popular open source [Jasmin SMS Gateway](https://github.com/jookies/jasmin) as an alternative to the standard jcli command line interface. 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See the relevant deployment documentaion for notes on how to deploy the project on a live system. Installation and setup will vary depending on your operating system and preferred installation method and are covered in great detail in the respective documentation 8 | 9 | ### Prerequisites 10 | 11 | * Python 3 12 | * A working installation of the Jasmin SMS Gateway software with standard jcli port exposed (can be a docker container) 13 | * A working Py4Web installation (either dev or live deployment) which can access jcli port on Jasmin either locally or remotely 14 | 15 | ### Installing 16 | 17 | Depending on your requirements and OS installation will vary. 18 | 19 | Full documentation for the installation and usage for Jasmin SMS Gateway can be found at https://docs.jasminsms.com/en/latest/index.html 20 | 21 | Full documentation for the installation and usage for Py4Web can be found at https://py4web.com/_documentation/static/en/index.html 22 | 23 | Example: 24 | To set up a development environment on a Windows 10 machine with WSL2 (Ubuntu) and a dockerised Jasmin follow these steps: 25 | 26 | 27 | ``` 28 | $ docker-compose --version 29 | docker-compose version 1.28.5, build c4eb3a1f 30 | ``` 31 | If docker is not installed you will need to install it before proceeding 32 | 33 | Create a directory of your choice to store your docker-compose file 34 | 35 | ``` 36 | $ mkdir /opt/scripts/jasmin 37 | $ cd /opt/scripts/jasmin 38 | ``` 39 | 40 | Create a docker compose file in the directory usind your favourite editor 41 | 42 | ``` 43 | $ vi docker-compose.yml 44 | ``` 45 | Paste the following into the docker-compose.yml 46 | 47 | ``` 48 | version: "3" 49 | 50 | services: 51 | smppsim: 52 | image: eagafonov/smppsim 53 | container_name: smppsim 54 | ports: 55 | - 3785:2775 56 | redis: 57 | image: redis:alpine 58 | restart: unless-stopped 59 | 60 | rabbit-mq: 61 | image: rabbitmq:alpine 62 | restart: unless-stopped 63 | 64 | jasmin: 65 | image: jookies/jasmin:0.10 66 | restart: unless-stopped 67 | container_name: jasmin 68 | volumes: 69 | - /var/log/jasmin:/var/log/jasmin 70 | #- /etc/jasmin:/etc/jasmin 71 | - /etc/jasmin/store:/etc/jasmin/store 72 | 73 | ports: 74 | - 2775:2775 75 | - 8990:8990 76 | - 1401:1401 77 | depends_on: 78 | - redis 79 | - rabbit-mq 80 | environment: 81 | REDIS_CLIENT_HOST: redis 82 | AMQP_BROKER_HOST: rabbit-mq 83 | SMPPSIM: smppsim 84 | ``` 85 | 86 | This includes an smpp simulator in order to test connectivity etc. 87 | Save and close. 88 | 89 | Start the Jasmin container 90 | ``` 91 | $ docker-compose up 92 | ``` 93 | Check that Jasmin and dependancies are up and running 94 | ``` 95 | $ docker ps 96 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 97 | f4cc24abd695 jookies/jasmin:0.10 "/docker-entrypoint.…" 3 days ago Up 3 days 0.0.0.0:1401->1401/tcp, 0.0.0.0:2775->2775/tcp, 0.0.0.0:8990->8990/tcp jasmin 98 | 794d7b78b76a redis:alpine "docker-entrypoint.s…" 3 days ago Up 3 days 6379/tcp jasmin_redis_1 99 | 4febaf435594 rabbitmq:alpine "docker-entrypoint.s…" 3 days ago Up 3 days 4369/tcp, 5671-5672/tcp, 15691-15692/tcp, 25672/tcp jasmin_rabbit-mq_1 100 | 5c8b43d5cfc1 eagafonov/smppsim "/bin/sh -c /opt/loc…" 3 days ago Up 3 days 0.0.0.0:3785->2775/tcp smppsim 101 | ``` 102 | Please refer to the Jasmin manual for troubleshooting if needed 103 | 104 | Create a python3 virtual environment in the location of your choice or follow py4web installation procedures for deployment 105 | ``` 106 | $ cd /opt 107 | $ python3 -m venv py4web 108 | ``` 109 | Activate the virtual environment 110 | ``` 111 | $ source py4web/bin/activate 112 | (py4web)$ 113 | ``` 114 | Install Py4Web in the newly created virtual environment 115 | ``` 116 | (py4web)$ cd py4web 117 | (py4web)$ python3 -m pip install --upgrade py4web --no-cache-dir 118 | ``` 119 | Hint: If python3 doesnt work try using just pyhon instead. 120 | This will install py4web and all its dependencies on the system’s path only. The assets folder (that contains the py4web’s system apps) will also be created. After the installation you’ll be able to start py4web on any given working folder with 121 | ``` 122 | py4web setup apps 123 | py4web set_password 124 | py4web run apps 125 | ``` 126 | ## First Run 127 | ``` 128 | $ py4web run apps 129 | ``` 130 | Once py4web is running you can access a specific app at the following urls from your browser: 131 | ``` 132 | http://localhost:8000 133 | http://localhost:8000/_dashboard 134 | http://localhost:8000/{yourappname}/index 135 | ``` 136 | In order to stop py4web, you need to hit Control-C on the window where you run it. 137 | Please refer to the user documentation if you need to change the configs or wish to use different ports etc. 138 | 139 | ## Check Jasmin Configuration 140 | Depending on how Jasmin has been installed the following steps should be taken at this stage: 141 | 142 | ``` 143 | telnet 144 | $ telnet 0.0.0.0 8990 145 | username: 146 | ``` 147 | If you see the username: prompt Jasmin is configured correctly and we can proceed to install the GUI 148 | 149 | ## Install the application 150 | 151 | Now that we have both Jasmin and Py4web configured and running we need to install the Jasmin SMS GUI. This is a Py4Web app so should go in the py4web/apps folder. 152 | 153 | ``` 154 | cd apps 155 | git clone https://github.com/Eudorajab1/jasmin_smsc_gui.git . 156 | ``` 157 | 158 | Once you have cloned the app you need open the file apps/jasmin_smsc_gui/settings.py in your favourite text edior and change the following: 159 | 160 | * JASMIN_HOST host as per telnet command 161 | * JASMIN_PORT port as per telnet command 162 | * JASMIN_USER as per jasmin config default is "jcliuser" 163 | * JASMIN_PWD as per jasmin config default is "jclipwd" 164 | 165 | to reflect your setup. 166 | 167 | Once saved you can restart Py4Web and navigate to http://localhost:8000/jasmin_smsc_gui 168 | 169 | ## Initialize the database 170 | If you have an existing instance of Jasmin, once you have connected you can run the populate database function from the superadmin option which will import all current groups, users, connectors, filters etc from your working instance of Jasmin. 171 | 172 | ## Authors 173 | 174 | * **John Bannister** - *Initial work* - [Eurodrajab1](https://github.com/Eudorajab1) 175 | 176 | ## License 177 | 178 | This project is licensed under the MIT License 179 | 180 | ## Acknowledgments 181 | 182 | * Hat tip to the Jookies/Jasmin and Py4web teams for their stirling products 183 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # check compatibility 2 | import py4web 3 | 4 | assert py4web.check_compatible("0.1.20190709.1") 5 | 6 | # by importing db you expose it to the _dashboard/dbadmin 7 | from .models import db 8 | 9 | # by importing controllers you expose the actions defined in it 10 | from . import controllers 11 | from . import super_admin 12 | from . import filter_manager 13 | from . import connector_manager 14 | from . import route_manager 15 | from . import stats 16 | from . import interceptor_manager 17 | # optional parameters 18 | __version__ = "1.0.0.0" 19 | __author__ = "John Bannister " 20 | __license__ = "MIT" 21 | -------------------------------------------------------------------------------- /__pycache__/.gitignore: -------------------------------------------------------------------------------- 1 | databases/* 2 | uploads/* 3 | *.py[oc] 4 | password.txt 5 | *.log 6 | *.db -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines cache, session, and translator T object for the app 3 | These are fixtures that every app needs so probably you will not be editing this file 4 | """ 5 | import os 6 | import sys 7 | import logging 8 | from py4web import Session, Cache, Translator, Flash, DAL, Field, action 9 | from py4web.utils.mailer import Mailer 10 | from py4web.utils.auth import Auth 11 | from py4web.utils.downloader import downloader 12 | from py4web.utils.tags import Tags 13 | from py4web.utils.factories import ActionFactory 14 | from . import settings 15 | from .jtel import Jptelnet 16 | # ####################################################### 17 | # implement custom loggers form settings.LOGGERS 18 | # ####################################################### 19 | logger = logging.getLogger("py4web:" + settings.APP_NAME) 20 | formatter = logging.Formatter( 21 | "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" 22 | ) 23 | for item in settings.LOGGERS: 24 | level, filename = item.split(":", 1) 25 | if filename in ("stdout", "stderr"): 26 | handler = logging.StreamHandler(getattr(sys, filename)) 27 | else: 28 | handler = logging.FileHandler(filename) 29 | handler.setFormatter(formatter) 30 | logger.setLevel(getattr(logging, level.upper(), "DEBUG")) 31 | logger.addHandler(handler) 32 | 33 | # ####################################################### 34 | # connect to db 35 | # ####################################################### 36 | db = DAL( 37 | settings.DB_URI, 38 | folder=settings.DB_FOLDER, 39 | pool_size=settings.DB_POOL_SIZE, 40 | migrate=settings.DB_MIGRATE, 41 | fake_migrate=settings.DB_FAKE_MIGRATE, 42 | ) 43 | 44 | # ####################################################### 45 | # define global objects that may or may not be used by the actions 46 | # ####################################################### 47 | cache = Cache(size=1000) 48 | T = Translator(settings.T_FOLDER) 49 | flash = Flash() 50 | 51 | # ####################################################### 52 | # pick the session type that suits you best 53 | # ####################################################### 54 | if settings.SESSION_TYPE == "cookies": 55 | session = Session(secret=settings.SESSION_SECRET_KEY) 56 | elif settings.SESSION_TYPE == "redis": 57 | import redis 58 | 59 | host, port = settings.REDIS_SERVER.split(":") 60 | # for more options: https://github.com/andymccurdy/redis-py/blob/master/redis/client.py 61 | conn = redis.Redis(host=host, port=int(port)) 62 | conn.set = ( 63 | lambda k, v, e, cs=conn.set, ct=conn.ttl: cs(k, v, ct(k)) 64 | if ct(k) >= 0 65 | else cs(k, v, e) 66 | ) 67 | session = Session(secret=settings.SESSION_SECRET_KEY, storage=conn) 68 | elif settings.SESSION_TYPE == "memcache": 69 | import memcache, time 70 | 71 | conn = memcache.Client(settings.MEMCACHE_CLIENTS, debug=0) 72 | session = Session(secret=settings.SESSION_SECRET_KEY, storage=conn) 73 | elif settings.SESSION_TYPE == "database": 74 | from py4web.utils.dbstore import DBStore 75 | 76 | session = Session(secret=settings.SESSION_SECRET_KEY, storage=DBStore(db)) 77 | 78 | # ####################################################### 79 | # Instantiate the object and actions that handle auth 80 | # ####################################################### 81 | auth = Auth(session, db, define_tables=False) 82 | auth.use_username = True 83 | auth.param.registration_requires_confirmation = settings.VERIFY_EMAIL 84 | auth.param.registration_requires_approval = settings.REQUIRES_APPROVAL 85 | auth.param.allowed_actions = settings.ALLOWED_ACTIONS 86 | auth.param.login_expiration_time = 3600 87 | auth.param.password_complexity = {"entropy": 50} 88 | auth.param.block_previous_password_num = 3 89 | auth.define_tables() 90 | 91 | # ####################################################### 92 | # Configure email sender for auth 93 | # ####################################################### 94 | if settings.SMTP_SERVER: 95 | auth.sender = Mailer( 96 | server=settings.SMTP_SERVER, 97 | sender=settings.SMTP_SENDER, 98 | login=settings.SMTP_LOGIN, 99 | tls=settings.SMTP_TLS, 100 | ssl=settings.SMTP_SSL, 101 | ) 102 | 103 | # ####################################################### 104 | # Create a table to tag users as group members 105 | # ####################################################### 106 | if auth.db: 107 | groups = Tags(db.auth_user, "groups") 108 | 109 | # ####################################################### 110 | # Enable optional auth plugin 111 | # ####################################################### 112 | if settings.USE_PAM: 113 | from py4web.utils.auth_plugins.pam_plugin import PamPlugin 114 | 115 | auth.register_plugin(PamPlugin()) 116 | 117 | if settings.USE_LDAP: 118 | from py4web.utils.auth_plugins.ldap_plugin import LDAPPlugin 119 | 120 | auth.register_plugin(LDAPPlugin(db=db, groups=groups, **settings.LDAP_SETTINGS)) 121 | 122 | if settings.OAUTH2GOOGLE_CLIENT_ID: 123 | from py4web.utils.auth_plugins.oauth2google import OAuth2Google # TESTED 124 | 125 | auth.register_plugin( 126 | OAuth2Google( 127 | client_id=settings.OAUTH2GOOGLE_CLIENT_ID, 128 | client_secret=settings.OAUTH2GOOGLE_CLIENT_SECRET, 129 | callback_url="auth/plugin/oauth2google/callback", 130 | ) 131 | ) 132 | if settings.OAUTH2FACEBOOK_CLIENT_ID: 133 | from py4web.utils.auth_plugins.oauth2facebook import OAuth2Facebook # UNTESTED 134 | 135 | auth.register_plugin( 136 | OAuth2Facebook( 137 | client_id=settings.OAUTH2FACEBOOK_CLIENT_ID, 138 | client_secret=settings.OAUTH2FACEBOOK_CLIENT_SECRET, 139 | callback_url="auth/plugin/oauth2facebook/callback", 140 | ) 141 | ) 142 | 143 | if settings.OAUTH2OKTA_CLIENT_ID: 144 | from py4web.utils.auth_plugins.oauth2okta import OAuth2Okta # TESTED 145 | 146 | auth.register_plugin( 147 | OAuth2Okta( 148 | client_id=settings.OAUTH2OKTA_CLIENT_ID, 149 | client_secret=settings.OAUTH2OKTA_CLIENT_SECRET, 150 | callback_url="auth/plugin/oauth2okta/callback", 151 | ) 152 | ) 153 | 154 | # ####################################################### 155 | # Define a convenience action to allow users to download 156 | # files uploaded and reference by Field(type='upload') 157 | # ####################################################### 158 | if settings.UPLOAD_FOLDER: 159 | @action('download/') 160 | @action.uses(db) 161 | def download(filename): 162 | return downloader(db, settings.UPLOAD_FOLDER, filename) 163 | # To take advantage of this in Form(s) 164 | # for every field of type upload you MUST specify: 165 | # 166 | # field.upload_path = settings.UPLOAD_FOLDER 167 | # field.download_url = lambda filename: URL('download/%s' % filename) 168 | 169 | # ####################################################### 170 | # Optionally configure celery 171 | # ####################################################### 172 | if settings.USE_CELERY: 173 | from celery import Celery 174 | 175 | # to use "from .common import scheduler" and then use it according 176 | # to celery docs, examples in tasks.py 177 | scheduler = Celery( 178 | "apps.%s.tasks" % settings.APP_NAME, broker=settings.CELERY_BROKER 179 | ) 180 | 181 | 182 | # ####################################################### 183 | # Enable authentication 184 | # ####################################################### 185 | auth.enable(uses=(session, T, db), env=dict(T=T)) 186 | 187 | # ####################################################### 188 | # Define convenience decorators 189 | # ####################################################### 190 | unauthenticated = ActionFactory(db, session, T, flash, auth) 191 | authenticated = ActionFactory(db, session, T, flash, auth.user) 192 | 193 | jasmin = Jptelnet() 194 | -------------------------------------------------------------------------------- /connector_manager.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, Field, session, jasmin, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from py4web.utils.form import Form, FormStyleBulma 5 | from pydal.validators import * 6 | from . common import jasmin 7 | from .utils import cols_split 8 | 9 | @action('start_smpp_connector/') 10 | @action.uses(db, flash, session) 11 | def start_smpp_connector(cid): 12 | if not cid: 13 | flash.set('You need to select a connector to start') 14 | redirect(URL('manage_smpp_connectors')) 15 | con = cid 16 | t = jasmin.connector(['start',con]) 17 | flash.set ("Started Connector %s" % con) 18 | redirect(URL('manage_smpp_connectors')) 19 | 20 | @action('stop_smpp_connector/') 21 | @action.uses(db, flash, session) 22 | def stop_smpp_connector(cid): 23 | if not cid: 24 | flash.set('You need to select a connector to stop') 25 | redirect(URL('manage_smpp_connectors')) 26 | con = cid 27 | t = jasmin.connector(['stop',con]) 28 | flash.set('Stopped connector %s' % cid) 29 | redirect(URL('manage_smpp_connectors')) 30 | 31 | @action('edit_smpp_connector/', method=['GET', 'POST']) 32 | @action.uses(db, flash, session, 'record_content.html') 33 | def edit_smpp_connector(cid): 34 | if not cid: 35 | flash.set('No connector selected you need to select a connector') 36 | redirect(URL('manage_smpp_connecors')) 37 | con = cid 38 | back= '' 39 | title="Edit SMPP connector %s" % con 40 | query = db.connector.name == con 41 | cc = db(query).select().first() 42 | db.connector.id.readable=db.connector.id.writable=False 43 | db.connector.name.readable=db.connector.name.writable=False 44 | db.connector.name.default = con 45 | db.connector.c_logfile.default = '/var/log/jasmin/default-%s.log' % con 46 | back='' 47 | if cc: # we already have a record 48 | print('Inside have one') 49 | form = Form(db.connector, cc, deletable=False, formstyle=FormStyleBulma) 50 | else: 51 | print('inside new one') 52 | form = Form(db.connector, deletable=False, formstyle=FormStyleBulma) 53 | if form.accepted: 54 | ret = jasmin.connector(['update',con,\ 55 | form.vars['c_ripf'],form.vars['c_con_fail_delay'],form.vars['c_dlr_expiry'],\ 56 | form.vars['c_coding'],form.vars['c_logrotate'],form.vars['c_submit_throughput'],\ 57 | form.vars['c_elink_interval'],form.vars['c_bind_to'],form.vars['c_port'],form.vars['c_con_fail_retry'],\ 58 | form.vars['c_password'],form.vars['c_src_addr'],form.vars['c_bind_npi'],form.vars['c_addr_range'],\ 59 | form.vars['c_dst_ton'],form.vars['c_res_to'],form.vars['c_def_msg_id'],form.vars['c_priority'],\ 60 | form.vars['c_con_loss_retry'],form.vars['c_username'],form.vars['c_dst_npi'],form.vars['c_validity'],\ 61 | form.vars['c_requeue_delay'],form.vars['c_host'],form.vars['c_src_npi'],form.vars['c_trx_to'],form.vars['c_logfile'],\ 62 | form.vars['c_ssl'],form.vars['c_loglevel'],form.vars['c_bind'],form.vars['c_proto_id'],form.vars['c_dlr_msgid'],\ 63 | form.vars['c_con_loss_delay'],form.vars['c_bind_ton'],form.vars['c_pdu_red_to'],form.vars['c_src_ton'],]) 64 | if not ret: 65 | flash.set("Successfully updated connector %s" % con) 66 | redirect(URL('manage_smpp_connectors')) 67 | else: 68 | flash.set(ret) 69 | elif form.errors: 70 | flash.set('Form has errors') 71 | else: 72 | flash.set('Please refer to the Jasmin User Guide when updating values') 73 | return dict(content=form, back=back, title=title, caller='../manage_smpp_connectors') 74 | 75 | @action('show_smpp_connector/', method = ['GET','POST']) 76 | @action.uses(db, flash, session, 'show_record.html') 77 | def show_smpp_connector(cid): 78 | flash.set('Inside show smpp connector %s' % cid) 79 | import re 80 | if not cid: 81 | flash.set('Please select a connector to show') 82 | con = cid 83 | tt = jasmin.connector(['show',con]) 84 | connectors = [] 85 | for t in tt[1:-1]: 86 | connector = [] 87 | r = str.split(t) 88 | l = len(r) 89 | if l ==2: 90 | fld = r[0] 91 | val = r[1] 92 | connector.append(fld) 93 | connector.append(val) 94 | connectors.append(connector) 95 | else: 96 | pass 97 | return dict(caller = "../manage_smpp_connectors", rows=connectors) 98 | 99 | @action('remove_smpp_connector/') 100 | @action.uses(db, flash, session) 101 | def remove_smpp_connector(cid): 102 | flash.set('Inside remove smpp connector %s' % cid) 103 | con = cid 104 | t = jasmin.connector(['remove', con]) 105 | if not t: 106 | flash.set("Removed Connector %s" % con) 107 | query = db.connector.name == con 108 | db(query).delete() 109 | else: 110 | flash.set('ERROR: %s', t) 111 | redirect(URL('manage_smpp_connectors')) 112 | 113 | def list_smpp_connectors(): 114 | connectors = [] 115 | rows=(jasmin.list_it('smppcs')) 116 | lines=rows 117 | if not lines: 118 | return connectors 119 | tt = cols_split(lines[2:-2]) 120 | n = len(rows[0]) 121 | connector={} 122 | for row in tt: 123 | connector={} 124 | connector.update( 125 | cid=row[0][1:], 126 | status=row[1], 127 | session=row[2], 128 | starts=row[3], 129 | stops=row[4] 130 | ) 131 | connectors.append(connector) 132 | return connectors 133 | 134 | @action('manage_smpp_connectors', method=['GET', 'POST']) 135 | @action.uses(db, flash, session, 'smpp_connector_list.html') 136 | def manage_smpp_connectors(): 137 | headers = ['CID', 'Status', 'Host', 'Port', 'Username', 'Pasword', 'Session', 'Starts', 'Stops', 'Options'] 138 | for f in db['connector']: 139 | f.readable= f.writable = False 140 | db.connector.name.readable = db.connector.name.writable = True 141 | db.connector.c_host.readable = db.connector.c_host.writable = True 142 | db.connector.c_port.readable = db.connector.c_port.writable = True 143 | db.connector.c_username.readable = db.connector.c_username.writable = True 144 | db.connector.c_password.readable = db.connector.c_password.writable = True 145 | db.connector.c_submit_throughput.readable = db.connector.c_submit_throughput.writable = True 146 | 147 | form=Form(db.connector, dbio=False, formstyle=FormStyleBulma) 148 | if form.accepted: 149 | name = form.vars['name'] 150 | res = jasmin.connector(['create', name, form.vars['c_username'], form.vars['c_password'],\ 151 | form.vars['c_host'], form.vars['c_port'], form.vars['c_submit_throughput']]) 152 | if not res: 153 | form.vars['c_logfile'] = '/var/log/jasmin/default-%s.log' % name 154 | id = db.connector.insert(**db.connector._filter_fields(form.vars)) 155 | #id = db.connector.insert(**dict(form.vars)) 156 | if id: 157 | flash.set('Added new connector %s' % name) 158 | else: 159 | flash.set ('DB ERROR adding connector %s' % name) 160 | else: 161 | flash.set('ERROR: %s' % res) 162 | #redirect(URL('j_connectors', 'list_connectors')) 163 | elif form.errors: 164 | flash.set('ERRORS: %s' % form.errors) 165 | cons=list_smpp_connectors() 166 | return dict(form=form, cons=cons, db=db) 167 | 168 | @action('delete_http_con/', method=['GET', 'POST']) 169 | @action.uses(db,auth,session,flash) 170 | def delete_http_con(cid): 171 | if not cid: 172 | flash.set('Please select a connector to delete') 173 | redirect(URL('manage_http_connectors')) 174 | con = cid 175 | t = jasmin.http_cons(['remove',con]) 176 | flash.set('Removed Connector %s' % con) 177 | db(db.http_cons.hcon_cid == con).delete() 178 | redirect(URL('manage_http_connectors')) 179 | 180 | def http_cons(): 181 | connectors = [] 182 | rows=(jasmin.list_it('httpcs')) 183 | lines=rows 184 | if not rows: 185 | return connectors 186 | tt = cols_split(lines[2:-2]) 187 | n = len(rows[0]) 188 | connector={} 189 | for row in tt: 190 | connector={} 191 | connector = dict(cid=row[0][1:], c_type=row[1],method=row[2], baseurl=row[3]) 192 | connectors.append(connector) 193 | return connectors 194 | 195 | @action('manage_http_connectors', method=['GET','POST']) 196 | @action.uses(db, session, auth, flash, 'http_connector_list.html') 197 | def manage_http_connectors(): 198 | form=Form([ 199 | Field('hcon_cid','string',length=10,label='Connector ID', comment= 'Must be unique'), 200 | Field('hcon_method', label='Method', comment='GET/POST',requires = IS_IN_SET(('GET', 'POST'))), 201 | Field('hcon_url',label='Base URL', comment='URL for MO messages e.g http://10.10.20.125/receive-sms/mo.php'), 202 | ], dbio=False, formstyle=FormStyleBulma, deletable=False) 203 | if form.accepted: 204 | name = form.vars['hcon_cid'] 205 | resp = jasmin.http_cons(['create', form.vars['hcon_cid'],form.vars['hcon_method'], form.vars['hcon_url']]) 206 | if resp: # we have an error 207 | flash.set(resp) 208 | else: 209 | id = db.http_cons.update_or_insert(db.http_cons.hcon_cid == form.vars['hcon_cid'], 210 | hcon_cid = form.vars['hcon_cid'], 211 | hcon_method = form.vars['hcon_method'], 212 | hcon_url = form.vars['hcon_url']) 213 | if id: 214 | flash.set('Added HTTP connector %s' % form.vars['hcon_cid']) 215 | else: 216 | flash.set('Updated HTTP connector %s ' % form.vars['hcon_cid']) 217 | redirect(URL('manage_http_connectors')) 218 | cons= http_cons() 219 | return dict(form=form, cons=cons) 220 | 221 | -------------------------------------------------------------------------------- /controllers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines actions, i.e. functions the URLs are mapped into 3 | The @action(path) decorator exposed the function at URL: 4 | 5 | http://127.0.0.1:8000/{app_name}/{path} 6 | 7 | If app_name == '_default' then simply 8 | 9 | http://127.0.0.1:8000/{path} 10 | 11 | If path == 'index' it can be omitted: 12 | 13 | http://127.0.0.1:8000/ 14 | 15 | The path follows the bottlepy syntax. 16 | 17 | @action.uses('generic.html') indicates that the action uses the generic.html template 18 | @action.uses(session) indicates that the action uses the session 19 | @action.uses(db) indicates that the action uses the db 20 | @action.uses(T) indicates that the action uses the i18n & pluralization 21 | @action.uses(auth.user) indicates that the action requires a logged in user 22 | @action.uses(auth) indicates that the action requires the auth object 23 | 24 | session, db, T, auth, and tempates are examples of Fixtures. 25 | Warning: Fixtures MUST be declared with @action.uses({fixtures}) else your app will result in undefined behavior 26 | """ 27 | 28 | from py4web import action, request, abort, redirect, URL 29 | from yatl.helpers import A 30 | from .common import db, session, T, cache, auth, logger, authenticated, unauthenticated, flash 31 | 32 | 33 | @action("index", method=['GET', 'POST']) 34 | @action.uses(db, session, auth, flash, "index.html") 35 | def index(): 36 | tot_imos = 0 37 | tot_imts = 0 38 | tot_grps = 0 39 | tot_users = 0 40 | tot_filters = 0 41 | tot_smppcons = 0 42 | tot_httpcons = 0 43 | tot_mtroutes = 0 44 | tot_moroutes = 0 45 | tot_gateways = 0 46 | tot_imos = db(db.j_imo).count() 47 | tot_imts = db(db.j_imt).count() 48 | tot_grps = db(db.j_group).count() 49 | tot_users = db(db.j_user).count() 50 | tot_filters = db(db.mt_filter).count() 51 | tot_smppcons = db(db.connector).count() 52 | tot_httpcons = db(db.http_cons).count() 53 | tot_mtroutes = db(db.mtroute).count() 54 | tot_moroutes = db(db.moroute).count() 55 | return dict(tot_imos = tot_imos, 56 | tot_imts = tot_imts, 57 | tot_grps = tot_grps, 58 | tot_users = tot_users, 59 | tot_filters = tot_filters, 60 | tot_smppcons = tot_smppcons, 61 | tot_httpcons = tot_httpcons, 62 | tot_mtroutes = tot_mtroutes, 63 | tot_moroutes = tot_moroutes, 64 | ) 65 | 66 | user = auth.get_user() 67 | message = T("Hello {first_name}".format(**user) if user else "Hello") 68 | return dict(message=message) 69 | -------------------------------------------------------------------------------- /databases/README.md: -------------------------------------------------------------------------------- 1 | 2 | This is just a placeholder for the needed 'jasmin_smsc_gui/databases' folder 3 | -------------------------------------------------------------------------------- /filter_manager.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, Field, session, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from py4web.utils.form import Form, FormStyleBulma 5 | from .models import MT_FILTER_TYPES 6 | from pydal.validators import * 7 | from . common import jasmin 8 | from .utils import cols_split 9 | 10 | @action('db_filters', method=['GET', 'POST']) 11 | @action.uses(db, auth, session, flash, 'generic.html') 12 | def db_filters(): 13 | rows = db(db.mt_filter.id >0).select() 14 | return dict(rows=rows) 15 | 16 | 17 | @action('delete_filter/') 18 | @action.uses(db,auth,session,flash) 19 | def delete_filter(fid): 20 | if not fid: 21 | flash.set('You need to select a filter to delet') 22 | redirect(URL('manage_filters')) 23 | res = jasmin.filters(['delete', fid]) 24 | flash.set('Removed Filter %s' % fid) 25 | query = db.mt_filter.fid == fid 26 | db(query).delete() 27 | redirect(URL('manage_filters')) 28 | 29 | def list_filters(): 30 | filters = [] 31 | rows=jasmin.list_it('filters') 32 | if rows: 33 | tt = cols_split(rows[2:-2]) 34 | for t in tt: 35 | j_filter={} 36 | description = '' 37 | if t[1] =='UserFilter': 38 | description=t[4][5:-2] 39 | elif t[1] == 'GroupFilter': 40 | description=t[4][5:-2] 41 | elif t[1] == 'DestinationAddrFilter': 42 | description=t[5][10:-2] 43 | elif t[1] == 'SourceAddrFilter': 44 | description=t[5][10:-2] 45 | elif t[1] == 'ShortMessageFilter': 46 | description=t[5][5:-2] 47 | elif t[1] == 'DateIntervalFilter': 48 | description=t[5][1:-2] 49 | elif t[1] == 'TimeIntervalFilter': 50 | description=t[5][1:-2] 51 | elif t[1] == 'TagFilter': 52 | description=t[5][5:-2] 53 | elif t[1] == 'TransparentFilter': 54 | description='' 55 | else: 56 | description = '' 57 | if len(t) == 5: 58 | j_filter.update( 59 | filter_id=t[0][1:], 60 | filter_type=t[1], 61 | route=t[2], 62 | description=description 63 | ) 64 | else: 65 | j_filter.update( 66 | filter_id=t[0][1:], 67 | filter_type=t[1], 68 | route=t[2]+"/"+t[3], 69 | description=description 70 | ) 71 | 72 | filters.append(j_filter) 73 | return filters 74 | 75 | @action('manage_filters', method=['GET', 'POST']) 76 | @action.uses(db, auth, session, flash, 'list_filters.html') 77 | def manage_filters(): 78 | db.mt_filter.filter_route.readable = db.mt_filter.filter_route.writable = False 79 | form=Form([Field('fid', 'string', length=15, label='FID', comment='Filter ID must be unique'), 80 | Field('filter_type', requires=IS_IN_SET(MT_FILTER_TYPES), comment='Select from list of available types'), 81 | Field('f_value', 'string', length = 50, label='Filter Value', comment='Values must correspond to filter type'), 82 | ], dbio=False, formstyle=FormStyleBulma, deletable=False) 83 | if form.accepted: 84 | response=jasmin.filters(['create', form.vars['fid'], form.vars['filter_type'], form.vars['f_value']]) 85 | if not response: 86 | ret = db.mt_filter.update_or_insert(db.mt_filter.fid == form.vars['fid'], 87 | fid = form.vars['fid'], 88 | filter_type = form.vars['filter_type'], 89 | f_value = form.vars['f_value']) 90 | 91 | flash.set("Added a new filter %s" % form.vars['fid']) 92 | else: 93 | flash.set('Problem adding filter %s' % response) 94 | redirect(URL('manage_filters')) 95 | if form.errors: 96 | flash.set('Form has errors') 97 | filters=list_filters() 98 | return dict(form=form, filters=filters) 99 | -------------------------------------------------------------------------------- /interceptor_manager.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, Field, session, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from py4web.utils.form import Form, FormStyleBulma 5 | from pydal.validators import * 6 | from .models import MTROUTE_TYPES, MOROUTE_TYPES 7 | from . common import jasmin 8 | from .utils import cols_split 9 | 10 | @action('imt_remove/', method=['GET', 'POST']) 11 | def imt_remove(order): 12 | script = '' 13 | filters = '' 14 | order = order 15 | resp= jasmin.interceptor(['mt','remove', order, script,filters[:-1]]) 16 | if resp: 17 | flash.set(resp) 18 | else: 19 | flash.set("Removed MO Interceptor with order %s" % order) 20 | query = (db.j_imt.mtorder == order)&(db.j_imt.gw == session.g_id) 21 | db(query).delete() 22 | redirect(URL('manage_imt')) 23 | 24 | def get_imts(): 25 | imts = [] 26 | rows=jasmin.list_it('imts') 27 | 28 | if rows: 29 | tt = cols_split(rows[2:-2]) 30 | imt={} 31 | for t in tt: 32 | imt={'i_order':'','i_type':'', 'i_script':'','i_filter':''} 33 | n = len(t) 34 | imt.update( 35 | i_order=t[0][1:], 36 | i_type=t[1], 37 | i_script=t[2] 38 | ) 39 | i = 3 40 | if t[1]=='StaticMTInterceptor': 41 | while t[i][0] != '<': 42 | test = t[i][0] 43 | imt.update( 44 | i_script = imt['i_script'] +" "+t[i] 45 | ) 46 | i += 1 47 | while i < n: 48 | imt.update( 49 | i_filter = imt['i_filter']+ " "+t[i]) 50 | i += 1 51 | imts.append(imt) 52 | else: 53 | while i < n: 54 | imt.update( 55 | i_script = imt['i_script'] +" "+t[i] 56 | ) 57 | i += 1 58 | imts.append(imt) 59 | return imts 60 | 61 | @action('manage_imts', method=['GET', 'POST']) 62 | @action.uses(db, session, auth, flash, 'list_interceptors.html') 63 | def manage_imts(): 64 | title='Manage MT Interceptors' 65 | form = Form(db.j_imt, formstyle=FormStyleBulma) 66 | if form.accepted: 67 | order = form.vars.mtorder 68 | i_type = form.vars.mttype 69 | script = 'python3('+form.vars.mtscript+')' 70 | if i_type != 'DefaultInterceptor': 71 | ff='' 72 | for f in form.vars.mtfilters: 73 | ff += get_f_name(f) 74 | ff+=';' 75 | filters = ff 76 | else: 77 | filters= '' 78 | order = "0" 79 | form.vars.mtorder = '0' 80 | form.vars.mtfilters = '' 81 | 82 | resp= jasmin.interceptor(['mt',i_type, order, script,filters[:-1]]) 83 | if not resp: 84 | id = db.j_imt.insert(**db.j_imt._filter_fields(form.vars)) 85 | flash.set("Added a %s with order %s" % (form.vars.mttype, order)) 86 | else: 87 | flash.set(resp) 88 | imts = get_imts() 89 | return dict(form=form, imts=imts, type="mt", title=title) 90 | 91 | @action('imo_remove/', method=['GET', 'POST']) 92 | def imo_remove(order): 93 | script = '' 94 | filters = '' 95 | order = order 96 | resp= jasmin.interceptor(['mo','remove', order, script,filters[:-1]]) 97 | if not resp: 98 | session.set("Removed MO Interceptor with order %s" % order) 99 | query = (db.j_imo.moorder == order)&(db.j_imo.gw == session.g_id) 100 | db(query).delete() 101 | else: 102 | flash.set(resp) 103 | redirect(URL('manage_imos')) 104 | 105 | def get_imos(): 106 | imos = [] 107 | rows=jasmin.list_it('imos') 108 | if rows: 109 | tt = cols_split(rows[2:-2]) 110 | imo={} 111 | for t in tt: 112 | n = len(t) 113 | imo={'i_order':'','i_type':'', 'i_script':'','i_filter':''} 114 | imo.update( 115 | i_order=t[0][1:], 116 | i_type=t[1], 117 | i_script=t[2] 118 | ) 119 | i = 3 120 | if t[1]=='StaticMOInterceptor': 121 | while t[i][0] != '<': 122 | test = t[i][0] 123 | imo.update( 124 | i_script = imo['i_script'] +" "+t[i] 125 | ) 126 | i += 1 127 | while i < n: 128 | imo.update( 129 | i_filter = imo['i_filter']+ " "+t[i]) 130 | i += 1 131 | imos.append(imo) 132 | else: 133 | while i < n: 134 | imo.update( 135 | i_script = imo['i_script'] +" "+t[i] 136 | ) 137 | i += 1 138 | imos.append(imo) 139 | return imos 140 | 141 | @action('manage_imos', method=['GET', 'POST']) 142 | @action.uses(db, session, auth, flash, 'list_interceptors.html') 143 | def manage_imos(): 144 | title = 'Manage MO Interceptors' 145 | 146 | form = Form(db.j_imo, formstyle=FormStyleBulma) 147 | if form.accepted: 148 | order = form.vars.moorder 149 | i_type = form.vars.motype 150 | script = 'python3('+form.vars.moscript+')' 151 | if i_type != 'DefaultInterceptor': 152 | ff='' 153 | for f in form.vars.mofilters: 154 | ff += get_f_name(f) 155 | ff+=';' 156 | filters = ff 157 | else: 158 | filters = '' 159 | form.vars.moorder = '0' 160 | order = "0" 161 | form.vars.mofilters = '' 162 | resp= jasmin.interceptor(['mo',i_type, order, script,filters[:-1]]) 163 | if not resp: 164 | id = db.j_imo.insert(**db.j_imo._filter_fields(form.vars)) 165 | flash.set("Added a MO %s with order %s" % (form.vars.motype,order)) 166 | else: 167 | flash.set(resp) 168 | imos = get_imos() 169 | return dict(form=form, imts=imos, type = "mo", title=title) 170 | 171 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines the database models 3 | """ 4 | 5 | from .common import db, Field 6 | from pydal.validators import * 7 | from yatl.helpers import * 8 | from py4web.utils.form import Form, FormStyleBulma 9 | 10 | MTROUTE_TYPES = ('DefaultRoute', 'StaticMTRoute', 'RandomRoundrobinMTRoute','FailoverMTRoute') 11 | MOROUTE_TYPES = ('DefaultRoute', 'StaticMORoute', 'RandomRoundrobinMORoute','FailoverMORoute') 12 | HTTP_CON_TYPE =('GET', 'POST') 13 | MT_CON_TYPES =('smppc', 'httpc') 14 | MT_FILTER_TYPES=('DestinationAddrFilter','UserFilter','GroupFilter','SourceAddrFilter','ShortMessageFilter','DateIntervalFilter','TimeIntervalFilter','TagFilter','TransparentFilter') 15 | MO_FILTER_TYPES=('DestinationAddrFilter','SourceAddrFilter','ConnectorFilter','ShortMessageFilter','DateIntervalFilter','TimeIntervalFilter','TagFilter','EvalPyFilter','TransparentFilter') 16 | IMO_TYPES=('DefaultInterceptor', 'StaticMOInterceptor') 17 | IMT_TYPES=('DefaultInterceptor', 'StaticMTInterceptor') 18 | 19 | db.define_table('mt_filter', 20 | Field('fid', 'string', length=15, label='FID', comment='Filter ID must be unique'), 21 | Field('filter_type', requires=IS_IN_SET(MT_FILTER_TYPES), comment='Select from list of available types'), 22 | Field('filter_route'), 23 | Field('f_value', 'string', length = 50, label='Filter Value', comment='Values must correspond to filter type'), 24 | format='%(fid)s') 25 | 26 | 27 | db.define_table('j_imo', 28 | Field('motype', label='Type',requires=IS_IN_SET(IMO_TYPES),comment='Type of interceptor'), 29 | Field('moorder',label='Order',comment='Interceptor will evaluate in descending order'), 30 | Field('mofilters', 'list:reference mt_filter', requires=IS_IN_DB(db,'mt_filter.id','mt_filter.fid',multiple=True),label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 31 | Field('moscript', label='Script',comment='Path and script must exist. Only python 3 scripts allowed now')) 32 | 33 | db.define_table('j_imt', 34 | Field('mttype', requires=IS_IN_SET(IMT_TYPES), label='Type', comment='Type of interceptor'), 35 | Field('mtorder', label='Order', comment='Interceptor will evaluate in descending order'), 36 | Field('mtfilters', 'list:reference mt_filter',requires=IS_IN_DB(db,db.mt_filter._id, db.mt_filter.fid ,multiple=True),label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 37 | Field('mtscript', label='Script', comment='Path and script must exist. Only python 3 scripts allowed now')) 38 | 39 | db.define_table('j_group', 40 | Field('name','string',length = 10, comment='Must be a string with no spaces or special characters'), 41 | format='%(name)s') 42 | 43 | db.define_table('j_user', 44 | Field('username', 'string', length=10, comment="Jasmin User Name for HTTP and SMPP connecting. Must not include any spaces and can not be longer than 10 characters"), 45 | Field('password', 'string', length=10, comment='Jasmin Password for HTTP and SMPP connecting. Must not include any spaces and can not be longer than 10 characters'), 46 | Field('j_uid','string',label='Jasmin UID',length=12, comment='Jasmin UID cannot be longer than 12 characters and reccoment all in UPPER case. No spaces allowed. Suggest USER_1 etc.'), 47 | Field('j_group','reference j_group',label = 'Jasim GID', comment='Select a Group', requires=IS_IN_DB(db,'j_group.id','j_group.name')), 48 | format='%(username)s') 49 | 50 | db.define_table('j_user_cred', 51 | Field('juser', 'string',label='Jasmin UID', length = 10), 52 | Field('default_src_addr', default='None', comment='Default source address of SMS-MT'), 53 | Field('quota_http_throughput',default='ND', comment='Max. number of messages per second to accept through HTTP API'), 54 | Field('quota_balance',default = 'ND', comment='c.f. 1. Balance quota'), 55 | Field('quota_smpps_throughput',default = 'ND', comment='Max. number of messages per second to accept through SMPP Server'), 56 | Field('quota_sms_count', default='ND', comment='c.f. 2. sms_count quota'), 57 | Field('quota_early_percent', default='ND', comment='c.f. Asynchronous billing'), 58 | Field('value_priority',default='^[0-3]$', comment='Regex pattern to validate priority of SMS-MT'), 59 | Field('value_content',default='.*', comment='Regex pattern to validate content of SMS-MT'), 60 | Field('value_src_addr', default='.*', comment='Regex pattern to validate source address of SMS-MT'), 61 | Field('value_dst_addr', default='.*', comment='Regex pattern to validate destination address of SMS-MT'), 62 | Field('value_validity_period', default='^\d+$', comment='Regex pattern to validate validity_period of SMS-MT'), 63 | Field('author_http_send',default=True, comment='Privilege to send SMS through Sending SMS-MT'), 64 | Field('author_http_dlr_method', default=True, comment='Privilege to set dlr-method HTTP parameter (default is GET)'), 65 | Field('author_http_balance', default= True, comment='Privilege to check balance through Checking account balance'), 66 | Field('author_smpps_send',default= True, comment='Privilege to send SMS through SMPP Server API'), 67 | Field('author_priority', default= True, comment='Privilege to defined priority of SMS-MT (default is 0)'), 68 | Field('author_http_long_content', default= True, comment='Privilege to send long content SMS through Sending SMS-MT'), 69 | Field('author_src_addr', default= True, comment='Privilege to defined source address of SMS-MT'), 70 | Field('author_dlr_level', default= True, comment='Privilege to set dlr-level parameter (default is 1)'), 71 | Field('author_http_rate', default =True, comment='Privilege to check a message rate through Checking rate price'), 72 | Field('author_validity_period', default=True, comment='Privilege to defined validity_period of SMS-MT (default is NOT SET)'), 73 | Field('author_http_bulk', default= False, comment='Privilege to send bulks through http api (Not implemented yet)'), 74 | format = '%(juser)s') 75 | 76 | 77 | db.define_table('mo_filter', 78 | Field('fid', 'string', length=15, unique=True), 79 | Field('filter_type', requires=IS_IN_SET(MO_FILTER_TYPES)), 80 | Field('f_value', 'string', length = 50), 81 | format='%(name)s') 82 | 83 | db.define_table('connector', 84 | Field('name','string',length=15, label='Connector ID',comment='Connector ID must be unique on each gateway', requires=[IS_LENGTH(minsize=1,maxsize=15),IS_NOT_IN_DB(db, 'connector.name')]), 85 | Field('c_logfile', label = 'Logfile',default='/var/log/jasmin/default-.log'), 86 | Field('c_logrotate', label = 'Log Rotate', default='midnight', comment='When to rotate the log file, possible values: S=Seconds, M=Minutes, H=Hours, D=Days, W0-W6=Weekday (0=Monday) and midnight=Roll over at midnight'), 87 | Field('c_loglevel', label = 'Log Level',default='20', comment='Logging numeric level: 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICCAL'), 88 | Field('c_host', label = 'Host',default='127.0.0.1', comment='Server that runs SMSC'), 89 | Field('c_port', label = 'Port',default='2775', comment='The port number for the connection to the SMSC'), 90 | Field('c_ssl', label = 'SSL', default='no', comment='Activate ssl connection'), 91 | Field('c_username', 'string', label = 'User name',length=15, comment='User name max 12 characters with no spaces'), 92 | Field('c_password', 'string', length=15, label = 'Password', comment='Password max 12 characters with no spaces'), 93 | Field('c_bind', label = 'Bind Type', requires=IS_IN_SET(('transceiver', 'transmitter', 'receiver')), default='transceiver', comment='Bind type: transceiver, receiver or transmitter'), 94 | Field('c_bind_to', label = 'Bind To', default='30', comment='Timeout for response to bind request'), 95 | Field('c_trx_to', label = 'Transmit Timeout',default='300', comment='Maximum time lapse allowed between transactions, after which, the connection is considered as inactive and will reconnect'), 96 | Field('c_res_to', label = 'Response Timeout',default='60', comment='Timeout for responses to any request PDU'), 97 | Field('c_pdu_red_to', label = 'PDU Read Timeout',default='10', comment='Timeout for reading a single PDU, this is the maximum lapse of time between receiving PDU’s header and its complete read, if the PDU reading timed out, the connection is considered as ‘corrupt’ and will reconnect'), 98 | Field('c_con_loss_retry', label = 'Coonection Loss Retry', default='yes', comment='Reconnect on connection loss ? (yes, no)'), 99 | Field('c_con_loss_delay', label = 'Connection Loss Delay',default='10', comment='Reconnect delay on connection loss (seconds)'), 100 | Field('c_con_fail_retry', label = 'Connection Fail Retry',default='yes', comment='Reconnect on connection failure ? (yes, no)'), 101 | Field('c_con_fail_delay', label = 'Connection Fail Delay',default='10', comment='Reconnect delay on connection failure (seconds)'), 102 | Field('c_src_addr', label = 'Default Source Address',default='Not defined', comment='Default source adress of each SMS-MT if not set while sending it, can be numeric or alphanumeric, when not defined it will take SMSC default'), 103 | Field('c_src_ton', label = 'Source TON',default='2', comment='Source address TON setting for the link: 0=Unknown, 1=International, 2=National, 3=Network specific, 4=Subscriber number, 5=Alphanumeric, 6=Abbreviated'), 104 | Field('c_src_npi', label = 'Source NPI',default='1', comment='Source address NPI setting for the link: 0=Unknown, 1=ISDN, 3=Data, 4=Telex, 6=Land mobile, 8=National, 9=Private, 10=Ermes, 14=Internet, 18=WAP Client ID'), 105 | Field('c_dst_ton', label = 'Destination TON',default='1', comment='Destination address TON setting for the link: 0=Unknown, 1=International, 2=National, 3=Network specific, 4=Subscriber number, 5=Alphanumeric, 6=Abbreviated'), 106 | Field('c_dst_npi', label = 'Destination NPI',default='1', comment='Destination address NPI setting for the link: 0=Unknown, 1=ISDN, 3=Data, 4=Telex, 6=Land mobile, 8=National, 9=Private, 10=Ermes, 14=Internet, 18=WAP Client ID'), 107 | Field('c_bind_ton', label = 'Bind TON',default='0', comment='Bind address TON setting for the link: 0=Unknown, 1=International, 2=National, 3=Network specific, 4=Subscriber number, 5=Alphanumeric, 6=Abbreviated'), 108 | Field('c_bind_npi', label = 'Bind NPI',default='1', comment='Bind address NPI setting for the link: 0=Unknown, 1=ISDN, 3=Data, 4=Telex, 6=Land mobile, 8=National, 9=Private, 10=Ermes, 14=Internet, 18=WAP Client ID'), 109 | Field('c_validity', label = 'Validtiy',default='Not defined', comment='Default validity period of each SMS-MT if not set while sending it, when not defined it will take SMSC default (seconds)'), 110 | Field('c_priority', label = 'Priority',default='0', comment='SMS-MT default priority if not set while sending it: 0, 1, 2 or 3'), 111 | Field('c_requeue_delay', label = 'Requeue Delay',default='120', comment='Delay to be considered when requeuing a rejected message'), 112 | Field('c_addr_range', label = 'Address Range',default='Not defined', comment='Indicates which MS’s can send messages to this connector, seems to be an informative value'), 113 | Field('c_systype', label = 'System Type',default='Not defined', comment='The system_type parameter is used to categorize the type of ESME that is binding to the SMSC. Examples include “VMS” (voice mail system) and “OTA” (over-the-air activation system)'), 114 | Field('c_dlr_expiry', label = 'DLR Expiry',default='86400', comment='When a SMS-MT is not acked, it will remain waiting in memory for expiry seconds, after this period, any received ACK will be ignored'), 115 | Field('c_submit_throughput', label = 'Submit Throughput',default='1', comment='Active SMS-MT throttling in MPS (Messages per second), set to 0 (zero) for unlimited throughput'), 116 | Field('c_proto_id', label = 'Protocol',default='0', comment='Used to indicate protocol id in SMS-MT and SMS-MO'), 117 | Field('c_coding',label = 'Coding',default='0', comment='Default coding of each SMS-MT if not set while sending it: 0=SMSC Default, 1=IA5 ASCII, 2=Octet unspecified, 3=Latin1, 4=Octet unspecified common, 5=JIS, 6=Cyrillic, 7=ISO-8859-8, 8=UCS2, 9=Pictogram, 10=ISO-2022-JP, 13=Extended Kanji Jis, 14=KS C 5601'), 118 | Field('c_elink_interval',label = 'Elink',default='30', comment='Enquire link interval (seconds)'), 119 | Field('c_def_msg_id',label = 'Default Msg ID',default='0', comment='Specifies the SMSC index of a pre-defined (‘canned’) message'), 120 | Field('c_ripf',label = 'Replace If Present',default='0', comment='Replace if present flag: 0=Do not replace, 1=Replace'), 121 | Field('c_dlr_msgid',label = 'DLR MsgID',default='0', comment='Indicates how to read msg id when receiving a receipt: 0=msg id is identical in submit_sm_resp and deliver_sm, 1=submit_sm_resp msg-id is in hexadecimal base, deliver_sm msg-id is in decimal base, 2=submit_sm_resp msg-id is in decimal base'), 122 | format='%(name)s') 123 | 124 | db.define_table('http_cons', 125 | Field('hcon_cid','string',length=10,label='Connector ID', comment= 'Must be unique'), 126 | Field('hcon_method', label='Method', comment='GET/POST',requires = IS_IN_SET(HTTP_CON_TYPE)), 127 | Field('hcon_url',label='Base URL', comment='URL for MO messages e.g http://10.10.20.125/receive-sms/mo.php'), 128 | format='%(hcon_cid)s') 129 | 130 | db.define_table('mtroute', 131 | Field('mt_order', 'string', length=10, label='Route order', requires=IS_NOT_EMPTY(), comment='Routes will be assesd in descending order based on filters and matches'), 132 | Field('mt_type', requires = IS_IN_SET(MTROUTE_TYPES), label='Route type'), 133 | Field('mt_connectors', 'list:reference connector', label='SMPP Connector(s)', comment='SMPP connector needs to be available'), 134 | Field('mt_filters', 'list:reference mt_filter',label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 135 | Field('mt_rate','string',length = 10, label='Rate', comment='Decimal rate value for the connector. All messages going over this connector will be charged at the rate specified'), 136 | 137 | format='%(mt_order)s') 138 | 139 | db.define_table('moroute', 140 | Field('mo_order', 'string', length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 141 | Field('mo_type', requires = IS_IN_SET(MOROUTE_TYPES), label='Route type'), 142 | Field('mo_connectors', 'list:reference connector', requires=IS_IN_DB(db,'connector.id','connector.name',multiple=True), label='SMPP Connector(s)', comment='SMPP connector needs to be available'), 143 | Field('mo_http_cons', 'list:reference http_cons', requires=IS_IN_DB(db,'http_cons.id','http_hcons-hcons_cid', multiple=True), label='HTTP Connector(s)', comment='HTTP connector needs to be available'), 144 | Field('mo_filters', 'list:reference mt_filter', requires=IS_IN_DB(db,'mt_filter.id','mt_filter.fid',multiple=True), label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 145 | format='%(mo_order)s') 146 | db.commit() -------------------------------------------------------------------------------- /route_manager.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, Field, session, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from py4web.utils.form import Form, FormStyleBulma 5 | from pydal.validators import * 6 | from .models import MTROUTE_TYPES, MOROUTE_TYPES 7 | from . common import jasmin 8 | from .utils import cols_split 9 | 10 | def index(): 11 | response.flash='Welcome to the rotuing manager' 12 | return dict() 13 | 14 | @action('show_mt_route/', method = ['GET','POST']) 15 | @action.uses(db, flash, session, 'record_content.html') 16 | def show_mt_route(order): 17 | if not order: 18 | flash.set('Please select a route to show') 19 | redirect(URL('manage_mt_routes')) 20 | title = 'Show MT route for order %s' % order 21 | qry = db(db.mtroute.mt_order == order).select().first() 22 | if qry: 23 | form = Form(db.mtroute, qry.id, dbio=False, formstyle=FormStyleBulma) 24 | return dict(caller = "../manage_mt_routes", title= title, content=form) 25 | 26 | @action('remove_mt_route/', method = ['GET','POST']) 27 | @action.uses(db, flash, session, 'record_content.html') 28 | def remove_mtroute(order): 29 | route = order 30 | res = jasmin.mtrouter(['remove',route]) 31 | flash.set('Removed Route %s' % route) 32 | db(db.mtroute.mt_order == route).delete() 33 | redirect(URL('manage_mt_routes')) 34 | 35 | @action('mt_static/', method=['GET', 'POST']) 36 | @action.uses(db, session, flash, 'record_content.html') 37 | def mt_static(route_type=None): 38 | if not route_type: 39 | flash.set('You need to select a route type') 40 | redirect(URL('manage_mt_routes')) 41 | title= 'New Static MT Route' 42 | form = Form([ 43 | Field('connector','reference connector',requires=IS_IN_DB(db,'connector.id','connector.name'), 44 | comment='SMPP or HTTP connector needs to be available'), 45 | Field('mt_order', 'string', length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 46 | Field('mt_filters', 'list:reference mt_filter', requires=IS_IN_DB(db, db.mt_filter._id, db.mt_filter.fid, multiple=True),label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 47 | Field('mt_rate','string',length = 10, label='Rate', comment='Decimal rate value for the connector. All messages going over this connector will be charged at the rate specified'), 48 | ], 49 | dbio=False, 50 | formstyle=FormStyleBulma) 51 | if form.accepted: 52 | f = form.vars['connector'] 53 | form.vars['mt_connectors'] = [f] 54 | form.vars['mt_type'] = route_type 55 | print('Connector ', form.vars['mt_connectors']) 56 | con = db.connector[f].name 57 | cons = 'smppc('+con+')' 58 | order = form.vars['mt_order'] 59 | ff='' 60 | for f in form.vars['mt_filters']: 61 | ff += db.mt_filter[f].fid 62 | ff+=';' 63 | filters = ff 64 | rate = form.vars['mt_rate'] 65 | have_one = db(db.mtroute.mt_order == form.vars['mt_order']).select().first() 66 | if not have_one: 67 | resp = jasmin.mtrouter(['StaticMTRoute',order, cons, filters,rate]) 68 | if not resp: 69 | db.mtroute.insert(**db.mtroute._filter_fields(form.vars)) 70 | flash.set('Added new Static MT route with order %s' % order) 71 | redirect(URL('manage_mt_routes')) 72 | else: 73 | flash.set('Problem adding route to Jamin. %s' % resp) 74 | redirect(URL('manage_mt_routes')) 75 | else: 76 | flash.set('Already have a MT connecor for order %s' % order) 77 | redirect(URL('manage_mt_routes')) 78 | return dict(content=form, title=title, caller='../manage_mt_routes') 79 | 80 | @action('mt_default/', method=['GET', 'POST']) 81 | @action.uses(db, session, flash, 'record_content.html') 82 | def mt_default(route_type=None): 83 | if not route_type: 84 | flash.set('You need to select a route type') 85 | redirect(URL('manage_mt_routes')) 86 | t = route_type 87 | title= 'New Default Route' 88 | form = Form([ 89 | Field('mt_connectors', 'reference connector', requires=IS_IN_DB(db,'connector.id','connector.name'), label='SMPP Connector', comment='SMPP connector needs to be available'), 90 | Field('mt_rate','string',length = 10, label='Rate', comment='Decimal rate value for the connector. All messages going over this connector will be charged at the rate specified'), 91 | ], 92 | dbio=False, 93 | formstyle=FormStyleBulma) 94 | if form.accepted: 95 | con = db.connector[form.vars['mt_connectors']].name 96 | cons = 'smppc('+con+')' 97 | order = '0' 98 | rate=form.vars['mt_rate'] 99 | connectors = [form.vars['mt_connectors']] 100 | print('Connectors', connectors) 101 | resp= jasmin.mtrouter(['DefaultRoute', cons, rate]) 102 | print('resp', resp) 103 | if not resp: 104 | id = db.mtroute.update_or_insert(db.mtroute.mt_order == 0, 105 | mt_order = 0, 106 | mt_type = t, 107 | mt_connectors = connectors, 108 | mt_rate = form.vars['mt_rate']) 109 | 110 | if id: 111 | flash.set('Added new %s with order %s' % (t, '0')) 112 | else: 113 | flash.set('Updated %s with order %s' % (t, '0')) 114 | else: 115 | flash.set('Problems adding route %s' % resp) 116 | redirect(URL('manage_mt_routes')) 117 | return dict(content=form, title=title, caller='../manage_mt_routes') 118 | 119 | @action('mt_random/', method=['GET', 'POST']) 120 | @action.uses(db, session, auth, flash, 'record_content.html') 121 | def mt_random(route_type): 122 | if not route_type: 123 | flash.set('You need to select a route type') 124 | redirect(URL('manage_mt_routes')) 125 | else: 126 | t = route_type 127 | title= 'New Round Robin Route' 128 | form = Form([ 129 | Field('mt_order', 'string', requires=IS_NOT_EMPTY(), length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 130 | Field('mt_connectors','list:reference connector',requires=IS_IN_DB(db,'connector.id','connector.name', multiple=True), 131 | comment='SMPP or HTTP connector needs to be available. Minimum 2 connectors required'), 132 | Field('mt_filters', 'list:reference mt_filter', requires=IS_IN_DB(db, db.mt_filter._id, db.mt_filter.fid, multiple=True),label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 133 | Field('mt_rate','string',length = 10, label='Rate', comment='Decimal rate value for the connector. All messages going over this connector will be charged at the rate specified'), 134 | ],dbio=False, formstyle=FormStyleBulma) 135 | if form.accepted: 136 | if len(form.vars['mt_connectors']) > 1: 137 | order = form.vars['order'] 138 | ff='' 139 | for f in form.vars['mt_filters']: 140 | ff += db.mt_filter[f].fid 141 | ff+=';' 142 | filters = ff 143 | cc='' 144 | for f in form.vars['mt_connectors']: 145 | cc += 'smppc('+ db.connector[f].name+');' 146 | cons = cc 147 | order = form.vars['mt_order'] 148 | rate = form.vars['mt_rate'] 149 | form.vars['mt_type'] = t 150 | have_one = db(db.mtroute.mt_order == form.vars['mt_order']).select().first() 151 | if not have_one: 152 | resp= jasmin.mtrouter(['RandomRoundrobinMTRoute',order,cons[:-1], filters[:-1],rate]) 153 | if not resp: 154 | db.mtroute.insert(**db.mtroute._filter_fields(form.vars)) 155 | flash.set('Added new Round Robin MT route with order %s' % order) 156 | redirect(URL('manage_mt_routes')) 157 | else: 158 | flash.set('Problem adding route to Jasmin. %s' % resp) 159 | redirect(URL('manage_mt_routes')) 160 | else: 161 | flash.set('Already have a MT connecor for order %s' % order) 162 | redirect(URL('manage_mt_routes')) 163 | else: 164 | flash.set('You need to select at least 2 connectors for round robin route') 165 | elif form.errors: 166 | flash.set('Form has errors %s' % form.errors) 167 | return dict(content=form, title=title, caller='../manage_mt_routes') 168 | 169 | @action('mt_failover/', method=['GET', 'POST']) 170 | @action.uses(db, session, auth, flash, 'record_content.html') 171 | def mt_failover(route_type): 172 | if not route_type: 173 | flash.set('You need to select a route type') 174 | redirec(URL('manage_mt_routes')) 175 | t = route_type 176 | title= 'New MT Failover Route' 177 | 178 | form = Form([Field('mt_order', 'string', length=10, label='Route order', requires=IS_NOT_EMPTY(), comment='Routes will be assesd in descending order based on filters and matches'), 179 | Field('mt_connectors','list:reference connector',requires=IS_IN_DB(db,'connector.id','connector.name', multiple=True), 180 | comment='SMPP or HTTP connector needs to be available. Minimum 2 connecrors required'), 181 | Field('mt_filters', 'list:reference mt_filter', requires=IS_IN_DB(db, db.mt_filter._id, db.mt_filter.fid, multiple=True),label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 182 | Field('mt_rate','string',length = 10, label='Rate', comment='Decimal rate value for the connector. All messages going over this connector will be charged at the rate specified'), 183 | ], dbio=False, formstyle=FormStyleBulma) 184 | if form.accepted: 185 | if len(form.vars['mt_connectors']) > 1: 186 | form.vars['mt_type'] = t 187 | order = form.vars['order'] 188 | ff='' 189 | for f in form.vars['mt_filters']: 190 | ff += db.mt_filter[f].fid 191 | ff+=';' 192 | filters = ff 193 | cc='' 194 | for f in form.vars['mt_connectors']: 195 | cc += 'smppc('+db.connector[f].name+');' 196 | cons = cc 197 | order = form.vars['mt_order'] 198 | rate = form.vars['mt_rate'] 199 | have_one = db(db.mtroute.mt_order == form.vars['mt_order']).select().first() 200 | if not have_one: 201 | resp= jasmin.mtrouter(['FailoverMTRoute',order, cons[:-1], filters[:-1],rate]) 202 | if not resp: 203 | db.mtroute.insert(**db.mtroute._filter_fields(form.vars)) 204 | flash.set('Added new MT Failover route with order %s' % order) 205 | redirect(URL('manage_mt_routes')) 206 | else: 207 | flash.set('Problems adding route %s' % resp) 208 | redirect(URL('manage_mt_routes')) 209 | else: 210 | flash.set('Already have a MT connecor for order %s' % order) 211 | redirect(URL('manage_mt_routes')) 212 | else: 213 | flash.set('You need to select more than one connector for this type of route') 214 | elif form.errors: 215 | flash.set('ERRORS: %s' % form.errors) 216 | 217 | return dict(content=form, title=title, caller='../manage_mt_routes') 218 | 219 | @action('manage_mt_routes', method=['GET', 'POST']) 220 | @action.uses(db, session, flash, 'mt_routes_list.html') 221 | def manage_mt_routes(): 222 | form = Form([ 223 | Field('r_tpe',label='Mt Route Type', requires=IS_IN_SET(MTROUTE_TYPES)) 224 | ], 225 | formstyle=FormStyleBulma) 226 | 227 | if form.accepted: 228 | if form.vars['r_tpe'] =='DefaultRoute': 229 | redirect(URL('mt_default', 'DefaultRoute')) 230 | flash.set('Add a new Default Route') 231 | elif form.vars['r_tpe'] =='StaticMTRoute': 232 | redirect(URL('mt_static', 'StaticMtRoute')) 233 | flash.set=('Static MT Route') 234 | elif form.vars['r_tpe'] =='RandomRoundrobinMTRoute': 235 | redirect(URL('mt_random', 'RandomRoundrobinMTRoute')) 236 | flash.set('Random Round Robin Route') 237 | else: 238 | redirect(URL('mt_failover', 'FailoverMTRoute')) 239 | flash.set('FailoverMTRoute') 240 | routes=mt_routes() 241 | ##JAB need to check if route in the db here 242 | return dict(form=form, routes=routes) 243 | 244 | def mt_routes(): 245 | routes = [] 246 | rows = jasmin.list_it('mtrouter') 247 | lines=rows 248 | if not rows: 249 | return routes 250 | tt = cols_split(lines[2:-2]) 251 | n = len(rows[0]) 252 | filters = '' 253 | connectors= '' 254 | for t in tt: 255 | route={} 256 | if len(t) == 4 or len(t) == 5: # default route 257 | if t[2] == '0': 258 | route.update( 259 | r_order=t[0][1:], 260 | r_type=t[1], 261 | r_rate=t[2]+t[3], 262 | r_connectors=t[4], 263 | r_filters='', 264 | ) 265 | else: 266 | route.update( 267 | r_order=t[0][1:], 268 | r_type=t[1], 269 | r_rate = t[2], 270 | r_connectors = t[3], 271 | r_filters='', 272 | ) 273 | elif len(t) >= 7 and t[1][0] == 'R': #Random route can have many connectors and filters 274 | if t[2] == '0': 275 | i = 4 276 | else: 277 | i = 3 278 | filters="" 279 | connectors = "" 280 | while i < len(t) and t[i][0] == 's': 281 | connectors += t[i] 282 | connectors += " " 283 | i +=1 284 | while i < len(t): 285 | filters+= t[i] 286 | filters+=" " 287 | i +=1 288 | if t[2] == '0': 289 | route.update( 290 | r_order=t[0][1:], 291 | r_type=t[1], 292 | r_rate=t[2]+t[3], 293 | r_connectors=connectors, 294 | r_filters=filters, 295 | ) 296 | else: 297 | route.update( 298 | r_order=t[0][1:], 299 | r_type=t[1], 300 | r_rate = t[2], 301 | r_connectors = connectors, 302 | r_filters=filters, 303 | ) 304 | 305 | elif len(t) >= 7 and t[1][0] == 'F': #Failover route can have many connectors and filters 306 | if t[2] == '0': 307 | i = 4 308 | else: 309 | i = 3 310 | filters='' 311 | connectors = '' 312 | while i < len(t) and t[i][0] == 's': 313 | connectors += t[i] 314 | connectors += " " 315 | i +=1 316 | while i < len(t): 317 | filters+= t[i] 318 | filters+= " " 319 | i +=1 320 | if t[2] == '0': 321 | route.update( 322 | r_order=t[0][1:], 323 | r_type=t[1], 324 | r_rate=t[2]+t[3], 325 | r_connectors=connectors, 326 | r_filters=filters, 327 | ) 328 | else: 329 | route.update( 330 | r_order=t[0][1:], 331 | r_type=t[1], 332 | r_rate = t[2], 333 | r_connectors = connectors, 334 | r_filters=filters, 335 | ) 336 | 337 | elif len(t) >= 6 and t[1][0] == 'S': #Static route can have many filters 338 | if t[2] == '0': 339 | i = 5 340 | else: 341 | i = 4 342 | filters='' 343 | while i < len(t): 344 | filters += t[i] 345 | filters += " " 346 | i +=1 347 | if t[2] == '0': 348 | route.update( 349 | r_order=t[0][1:], 350 | r_type=t[1], 351 | r_rate=t[2]+t[3], 352 | r_connectors=t[4], 353 | r_filters=filters, 354 | ) 355 | else: 356 | route.update( 357 | r_order=t[0][1:], 358 | r_type=t[1], 359 | r_rate = t[2], 360 | r_connectors = t[3], 361 | r_filters=filters, 362 | ) 363 | if route: 364 | routes.append(route) 365 | else: 366 | pass 367 | test=1 368 | return routes 369 | 370 | def route_exists(order): 371 | query = db.moroute.mo_order == order 372 | ret = db(query).select().first() 373 | return ret 374 | 375 | @action('mo_create/', method=['GET', 'POST']) 376 | @action.uses(db, session, auth, flash, 'record_content.html') 377 | def mo_create(route_type): 378 | resp=None 379 | cons = '' 380 | filters = '' 381 | if not route_type: 382 | flash.set('You need to select a route type to create') 383 | redirect(URL('manage_mo_routes')) 384 | if route_type == 'DefaultRoute': 385 | title= 'New Default MO Route' 386 | form = Form([Field('mo_connectors','reference connector',label = 'SMPP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'connector.id','connector.name')),comment='SMPP connector needs to be available'), 387 | Field('mo_http_cons','reference http_cons',label = 'HTTP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'http_cons.id','http_cons.hcon_cid')),comment='SMPP connector needs to be available') 388 | ], dbio=False, formstyle=FormStyleBulma) 389 | if form.accepted: 390 | if route_exists(0): 391 | flash.set('Route with order 0 already exists') 392 | redirect(URL('manage_mo_routes')) 393 | if not form.vars['mo_connectors'] and not form.vars['mo_http_cons']: 394 | flash.set('please select either HTTP or SMPP connector') 395 | redirect(URL('mo_create', 'DefaultRoute')) 396 | if form.vars['mo_connectors'] and form.vars['mo_http_cons']: 397 | flash.set('please select either HTTP or SMPP connector not both') 398 | redirect(URL('mo_create', 'DefaultRoute')) 399 | if form.vars['mo_connectors']: 400 | con = db.connector[form.vars['mo_connectors']].name 401 | cons='smpps('+con+')' 402 | else: 403 | con = db.http_cons[form.vars['mo_http_cons']].hcon_cid 404 | cons='http('+con+')' 405 | form.vars['mo_type'] = route_type 406 | form.vars['mo_order'] = '0' 407 | resp= jasmin.morouter(['DefaultRoute', cons]) 408 | if not resp: 409 | id = db.moroute.insert(**db.moroute._filter_fields(form.vars)) 410 | if id: 411 | flash.set('New Default MO route added with order %s' % form.vars['mo_order']) 412 | redirect(URL('manage_mo_routes')) 413 | else: 414 | flash.set('New Default MO route with order %s NOT ADDED TO DB' % form.vars['mo_order']) 415 | else: 416 | flash.set('ERROR: %s ' % resp) 417 | elif form.errors: 418 | flash.set('Form has erros: %s' % form.errors) 419 | elif route_type == 'StaticMORoute': 420 | title= 'New Static MO Route' 421 | form = Form([Field('mo_order', 'string', length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 422 | Field('mo_connectors','reference connector',label = 'SMPP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'connector.id','connector.name')),comment='SMPP connector needs to be available'), 423 | Field('mo_http_cons','reference http_cons',label = 'HTTP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'http_cons.id','http_cons.hcon_cid')),comment='HTTP connector needs to be available'), 424 | Field('mo_filters', 'list:reference mt_filter', requires=IS_IN_DB(db, db.mt_filter._id, db.mt_filter.fid, multiple=True), label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 425 | ], dbio=False, formstyle=FormStyleBulma) 426 | if form.accepted: 427 | if route_exists(form.vars['mo_order']): 428 | flash.set('MO Route with order %s already exists' % form.vars['mo_order']) 429 | redirect(URL('manage_mo_routes')) 430 | if not form.vars['mo_connectors'] and not form.vars['mo_http_cons']: 431 | flash.set('please select either HTTP or SMPP connector') 432 | redirect(URL('mo_create', 'StaticMORoute')) 433 | if form.vars['mo_connectors'] and form.vars['mo_http_cons']: 434 | flash.set('Please select either HTTP or SMPP connector not both') 435 | redirect(URL('mo_create', 'StaticMORoute')) 436 | if form.vars['mo_connectors']: 437 | con = db.connector[form.vars['mo_connectors']].name 438 | cons='smpps('+con+')' 439 | else: 440 | con = db.http_cons[form.vars['mo_http_cons']].hcon_cid 441 | cons='http('+con+')' 442 | ff='' 443 | for f in form.vars['mo_filters']: 444 | ff += db.mt_filter[f].fid 445 | ff+=';' 446 | filters = ff 447 | order = form.vars['mo_order'] 448 | form.vars['mo_type'] = route_type 449 | 450 | resp= jasmin.morouter(['StaticMORoute', order, cons, filters[:-1]]) 451 | if not resp: 452 | id = db.moroute.insert(**db.moroute._filter_fields(form.vars)) 453 | if id: 454 | flash.set('New Static MO route added with order %s' % form.vars['mo_order']) 455 | redirect(URL('manage_mo_routes')) 456 | else: 457 | flash.set('New Static MO route with order %s NOT ADDED TO DB' % form.vars['mo_order']) 458 | else: 459 | flash.set('ERROR: %s ' % resp) 460 | elif form.errors: 461 | flash.set('Form has erros: %s' % form.errors) 462 | redirect(URL('mo_create', 'StaticMORoute')) 463 | 464 | elif route_type == 'RandomRoundrobinMORoute': 465 | title= 'New Random Roundrobin MO Route' 466 | form = Form([Field('mo_order', 'string', length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 467 | Field('mo_connectors','list:reference connector',label = 'SMPP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'connector.id','connector.name', multiple=True)),comment='SMPP connector needs to be available'), 468 | Field('mo_http_cons','list:reference http_cons',label = 'HTTP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'http_cons.id','http_cons.hcon_cid',multiple=True)),comment='HTTP connector needs to be available'), 469 | Field('mo_filters', 'list:reference mt_filter', requires=IS_IN_DB(db,'mt_filter.id','mt_filter.fid',multiple=True), label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 470 | ], dbio=False, formstyle=FormStyleBulma) 471 | if form.accepted: 472 | if route_exists(form.vars['mo_order']): 473 | flash.set('MO Route with order %s already exists' % form.vars['mo_order']) 474 | redirect(URL('manage_mo_routes')) 475 | if not form.vars['mo_connectors'] and not form.vars['mo_http_cons']: 476 | flash.set('please select either HTTP or SMPP connector') 477 | redirect(URL('mo_create', 'StaticMORoute')) 478 | if form.vars['mo_connectors'] and form.vars['mo_http_cons']: 479 | flash.set('Please select either HTTP or SMPP connector not both') 480 | redirect(URL('mo_create', 'RandomRoundrobinMORoute')) 481 | cc='' 482 | if form.vars['mo_connectors']: 483 | for f in form.vars['mo_connectors']: 484 | cc += 'smpps('+db.connector[f].name+');' 485 | else: 486 | for f in form.vars['mo_http_cons']: 487 | cc += 'http('+db.http_cons[f].hcon_cid+');' 488 | cons = cc 489 | ff='' 490 | for f in form.vars['mo_filters']: 491 | ff += db.mt_filter[f].fid 492 | ff+=';' 493 | filters = ff 494 | form.vars['mo_type'] = route_type 495 | order = form.vars['mo_order'] 496 | resp= jasmin.morouter(['RandomRoundrobinMORoute', order, cons[:-1], filters[:-1]]) 497 | if not resp: 498 | id = db.moroute.insert(**db.moroute._filter_fields(form.vars)) 499 | if id: 500 | flash.set('New Random Roundrobin MO route added with order %s' % form.vars['mo_order']) 501 | redirect(URL('manage_mo_routes')) 502 | else: 503 | flash.set('New Random Roundrobin MO route with order %s NOT ADDED TO DB' % form.vars['mo_order']) 504 | else: 505 | flash.set('ERROR: %s ' % resp) 506 | elif form.errors: 507 | flash.set('Form has erros: %s' % form.errors) 508 | redirect(URL('mo_create', 'RandomRoundrobinMORoute')) 509 | 510 | elif route_type=='FailoverMORoute': 511 | title= 'New Failover MO Route' 512 | form = Form([Field('mo_order', 'string', length=10, label='Route order',comment='Routes will be assesd in descending order based on filters and matches'), 513 | Field('mo_connectors','list:reference connector',label = 'SMPP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'connector.id','connector.name', multiple=True)),comment='SMPP connector needs to be available'), 514 | Field('mo_http_cons','list:reference http_cons',label = 'HTTP connector', requires=IS_EMPTY_OR(IS_IN_DB(db,'http_cons.id','http_cons.hcon_cid',multiple=True)),comment='HTTP connector needs to be available'), 515 | Field('mo_filters', 'list:reference mt_filter', requires=IS_IN_DB(db,'mt_filter.id','mt_filter.fid',multiple=True), label='Filter(s)', comment='Filters need to be added prior to adding routes. Please see filter management'), 516 | ], dbio=False, formstyle=FormStyleBulma) 517 | if form.accepted: 518 | if not form.vars['mo_connectors'] and not form.vars['mo_http_cons']: 519 | flash.set('please select either HTTP or SMPP connector') 520 | redirect(URL('mo_create', 'StaticMORoute')) 521 | if form.vars['mo_connectors'] and form.vars['mo_http_cons']: 522 | flash.set('Please select either HTTP or SMPP connector not both') 523 | redirect(URL('mo_create', 'FailoverMORoute')) 524 | cc='' 525 | cc='' 526 | if form.vars['mo_connectors']: 527 | for f in form.vars['mo_connectors']: 528 | cc += 'smpps('+db.connector[f].name+');' 529 | else: 530 | for f in form.vars['mo_http_cons']: 531 | cc += 'http('+db.http_cons[f].hcon_cid+');' 532 | cons = cc 533 | ff='' 534 | for f in form.vars['mo_filters']: 535 | ff += db.mt_filter[f].fid 536 | ff+=';' 537 | filters = ff 538 | form.vars['mo_type'] = route_type 539 | order = form.vars['mo_order'] 540 | resp= jasmin.morouter(['FailoverMORoute', order, cons[:-1], filters[:-1]]) 541 | if not resp: 542 | id = db.moroute.insert(**db.moroute._filter_fields(form.vars)) 543 | if id: 544 | flash.set('New Failorver MO route added with order %s' % form.vars['mo_order']) 545 | redirect(URL('manage_mo_routes')) 546 | else: 547 | flash.set('New Failorver MO route with order %s NOT ADDED TO DB' % form.vars['mo_order']) 548 | else: 549 | flash.set('ERROR: %s ' % resp) 550 | elif form.errors: 551 | flash.set('Form has erros: %s' % form.errors) 552 | redirect(URL('mo_create', 'FailoverMORoute')) 553 | else: 554 | flash.set('No such type of MO route detected') 555 | return dict(content=form, title=title, caller='../manage_mo_routes') 556 | 557 | @action('remove_moroute/', method=['GET', 'POST']) 558 | @action.uses(db,session,auth,flash) 559 | def remove_moroute(route_order): 560 | route = route_order 561 | t = jasmin.morouter(['remove', route]) 562 | flash.set("Removed MO Route with order %s" % route) 563 | query = db.moroute.mo_order == route 564 | db(query).delete() 565 | redirect(URL('manage_mo_routes')) 566 | 567 | def mo_routes(): 568 | routes = [] 569 | rows = jasmin.list_it('morouter') 570 | if not rows: 571 | return routes #we dont have any mo routes 572 | else: 573 | lines=rows 574 | tt = cols_split(lines[2:-2]) 575 | n = len(rows[0]) 576 | routes = [] 577 | route={} 578 | filters = '' 579 | connectors= '' 580 | for t in tt: 581 | route={} 582 | if len(t) == 3: # default route 583 | route.update( 584 | r_order=t[0][1:], 585 | r_type=t[1], 586 | r_connectors=t[2], 587 | r_filters='' 588 | ) 589 | elif len(t) > 3 and t[1][0] == 'R': #Random route can have many connectors and filters 590 | i = 2 591 | filters='' 592 | connectors = '' 593 | while i < len(t) and t[i][0] == 's': 594 | connectors += t[i] 595 | i +=1 596 | while i < len(t) and t[i][0] == 'h': 597 | connectors += t[i] 598 | i +=1 599 | while i < len(t): 600 | filters+= t[i] 601 | i +=1 602 | route.update( 603 | r_order=t[0][1:], 604 | r_type=t[1], 605 | r_connectors=connectors, 606 | r_filters=filters, 607 | ) 608 | elif len(t) > 3 and t[1][0] == 'F': #Failover route can have many connectors and filters 609 | i = 2 610 | filters='' 611 | connectors = '' 612 | while i < len(t) and t[i][0] == 's': 613 | connectors += t[i] 614 | i +=1 615 | while i < len(t) and t[i][0] == 'h': 616 | connectors += t[i] 617 | i +=1 618 | while i < len(t): 619 | filters+= t[i] 620 | i +=1 621 | route.update( 622 | r_order=t[0][1:], 623 | r_type=t[1], 624 | r_connectors=connectors, 625 | r_filters=filters, 626 | ) 627 | elif len(t) > 3 and t[1][0] == 'S': #Static route can have many filters 628 | i = 4 629 | 630 | filters='' 631 | while i < len(t): 632 | filters += t[i] 633 | i +=1 634 | route.update( 635 | r_order=t[0][1:], 636 | r_type=t[1], 637 | r_connectors=t[2], 638 | r_filters=filters, 639 | ) 640 | else: 641 | i = 1 642 | routes.append(route) 643 | return routes 644 | 645 | @action('manage_mo_routes', method=['GET', 'POST']) 646 | @action.uses(db, session, auth, flash, 'list_moroutes.html') 647 | def manage_mo_routes(): 648 | form = Form([ 649 | Field('r_tpe',label='Mo Route Type', requires=IS_IN_SET(MOROUTE_TYPES)) 650 | ], dbio=False, formstyle = FormStyleBulma) 651 | if form.accepted: 652 | redirect(URL('mo_create', form.vars['r_tpe'])) 653 | routes=mo_routes() 654 | return dict(form=form, routes=routes) 655 | 656 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an optional file that defined app level settings such as: 3 | - database settings 4 | - session settings 5 | - i18n settings 6 | This file is provided as an example: 7 | """ 8 | import os 9 | from py4web.core import required_folder 10 | 11 | # db settings 12 | APP_FOLDER = os.path.dirname(__file__) 13 | APP_NAME = os.path.split(APP_FOLDER)[-1] 14 | # DB_FOLDER: Sets the place where migration files will be created 15 | # and is the store location for SQLite databases 16 | DB_FOLDER = required_folder(APP_FOLDER, "databases") 17 | DB_URI = "sqlite://storage.db" 18 | DB_POOL_SIZE = 1 19 | DB_MIGRATE = True 20 | DB_FAKE_MIGRATE = False # maybe? 21 | 22 | # location where static files are stored: 23 | STATIC_FOLDER = required_folder(APP_FOLDER, "static") 24 | 25 | # location where to store uploaded files: 26 | UPLOAD_FOLDER = required_folder(APP_FOLDER, "uploads") 27 | 28 | # jasmin settings: 29 | JASMIN_HOST = '0.0.0.0' 30 | JASMIN_PORT = 8990 31 | JASMIN_USER = 'jcliadmin' 32 | JASMIN_PWD = 'jclipwd' 33 | 34 | # send email on regstration 35 | VERIFY_EMAIL = True 36 | 37 | # account requires to be approved ? 38 | REQUIRES_APPROVAL = False 39 | 40 | # ALLOWED_ACTIONS: 41 | # ["all"] 42 | # ["login", "logout", "request_reset_password", "reset_password", "change_password", "change_email", "update_profile"] 43 | # if you add "login", add also "logout" 44 | ALLOWED_ACTIONS = ["all"] 45 | 46 | 47 | # email settings 48 | SMTP_SSL = False 49 | SMTP_SERVER = None 50 | SMTP_SENDER = "you@example.com" 51 | SMTP_LOGIN = "username:password" 52 | SMTP_TLS = False 53 | 54 | # session settings 55 | SESSION_TYPE = "cookies" 56 | SESSION_SECRET_KEY = "" # replace this with a uuid 57 | MEMCACHE_CLIENTS = ["127.0.0.1:11211"] 58 | REDIS_SERVER = "localhost:6379" 59 | 60 | # logger settings 61 | LOGGERS = [ 62 | "warning:stdout" 63 | ] # syntax "severity:filename" filename can be stderr or stdout 64 | 65 | # single sign on Google (will be used if provided) 66 | OAUTH2GOOGLE_CLIENT_ID = None 67 | OAUTH2GOOGLE_CLIENT_SECRET = None 68 | 69 | # single sign on Okta (will be used if provided. Please also add your tenant 70 | # name to py4web/utils/auth_plugins/oauth2okta.py. You can replace the XXX 71 | # instances with your tenant name.) 72 | OAUTH2OKTA_CLIENT_ID = None 73 | OAUTH2OKTA_CLIENT_SECRET = None 74 | 75 | # single sign on Google (will be used if provided) 76 | OAUTH2FACEBOOK_CLIENT_ID = None 77 | OAUTH2FACEBOOK_CLIENT_SECRET = None 78 | 79 | # enable PAM 80 | USE_PAM = False 81 | 82 | # enable LDAP 83 | USE_LDAP = False 84 | LDAP_SETTINGS = { 85 | "mode": "ad", 86 | "server": "my.domain.controller", 87 | "base_dn": "ou=Users,dc=domain,dc=com", 88 | } 89 | 90 | # i18n settings 91 | T_FOLDER = required_folder(APP_FOLDER, "translations") 92 | 93 | # Celery settings 94 | USE_CELERY = False 95 | CELERY_BROKER = "redis://localhost:6379/0" 96 | 97 | # try import private settings 98 | try: 99 | from .settings_private import * 100 | except (ImportError, ModuleNotFoundError): 101 | pass 102 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/css/no.css: -------------------------------------------------------------------------------- 1 | /***************************************************** 2 | no.css version 2020-08-09.1 3 | 4 | Designed to style pages without need for custom classes. 5 | headers, paragraphs, buttons, tables, forms, 6 | nav menus, alerts, and dialogs are styled automatically. 7 | The only custom classes are color names, grid column sizes, 8 | and a few convenience ones. 9 | 10 | Grid: 11 | columns, col, c25, c33, c50, c66, c75 12 | Colors: 13 | black, white, default, success, warning, error, info, transparent 14 | Effects: 15 | accordion, close, tags-list 16 | Utils: 17 | fill, padded 18 | 19 | License: MIT 20 | *****************************************************/ 21 | 22 | /**************************************************** 23 | global style 24 | ****************************************************/ 25 | 26 | *, *:after, *:before { 27 | border:0; 28 | margin:0; 29 | padding:0; 30 | box-sizing: inherit; 31 | color: inherit; 32 | } 33 | 34 | html, body { 35 | max-width: 100vw; 36 | overflow-x: hidden; 37 | box-sizing: border-box; 38 | } 39 | 40 | body { 41 | font-family: "Roboto", Helvetica, Arial, sans-serif; 42 | line-height: 1.8em; 43 | min-height: 100vh; 44 | display: grid; 45 | grid-template-rows: auto 1fr auto; 46 | } 47 | 48 | /**************************************************** 49 | elements style 50 | ****************************************************/ 51 | 52 | p { 53 | text-align:justify 54 | } 55 | 56 | b, label, strong { 57 | font-weight:bold 58 | } 59 | 60 | ul { 61 | list-style-type:none; 62 | padding-left:20px 63 | } 64 | 65 | a { 66 | text-decoration:none; 67 | color:#0074d9; 68 | white-space:nowrap 69 | } 70 | 71 | a:hover { 72 | cursor:pointer 73 | } 74 | 75 | h1,h2,h3,h4,h5,h6{ 76 | font-weight:bold; 77 | line-height: 1em; 78 | } 79 | 80 | h1{ 81 | font-size: 4em; 82 | margin:1.0em 0 0.25em 0 83 | } 84 | 85 | h2{ 86 | font-size: 2.4em; 87 | margin:0.9em 0 0.25em 0 88 | } 89 | 90 | h3{ 91 | font-size:1.8em; 92 | margin:0.8em 0 0.25em 0 93 | } 94 | 95 | h4{ 96 | font-size:1.6em; 97 | margin:0.7em 0 0.30em 0 98 | } 99 | 100 | h5{ 101 | font-size:1.4em; 102 | margin:0.6em 0 0.40em 0 103 | } 104 | 105 | h6{ 106 | font-size:1.2em; 107 | margin:0.5em 0 0.50em 0 108 | } 109 | 110 | header, footer { 111 | display:block; 112 | width:100%; 113 | } 114 | 115 | code { 116 | background: #f4f5f6; 117 | border-radius: .4rem; 118 | font-size: 90; 119 | margin: 0 .2rem; 120 | padding: .2rem .5rem; 121 | white-space: nowrap; 122 | } 123 | 124 | p,li,button,fieldset,input,select,textarea,blockquote,table { 125 | margin-bottom: 1.0rem; 126 | } 127 | 128 | /**************************************************** 129 | table 130 | ****************************************************/ 131 | 132 | table { 133 | border-collapse:collapse; 134 | width: 100% 135 | } 136 | 137 | tbody tr:hover { 138 | background-color:#fbf6d9 139 | } 140 | 141 | thead tr { 142 | background-color:#f1f1f1 143 | } 144 | 145 | tbody tr { 146 | border-bottom:2px solid #f1f1f1 147 | } 148 | 149 | td, th { 150 | padding: 4px 8px; 151 | text-align: left; 152 | vertical-align:top 153 | } 154 | 155 | thead th { 156 | vertical-align:bottom 157 | } 158 | 159 | @media (min-width: 40rem) { 160 | table { 161 | display: table; 162 | overflow-x: initial; 163 | } 164 | } 165 | 166 | /**************************************************** 167 | buttons 168 | ****************************************************/ 169 | 170 | [role="button"], button, input[type='button'], input[type='reset'], input[type='submit'] { 171 | background-color: #0074d9; 172 | border-radius: 5px; 173 | margin-right: 10px; 174 | margin-bottom: 10px; 175 | color: #fff; 176 | cursor: pointer; 177 | display: inline-block; 178 | font-size: 1.1rem; 179 | font-weight: 300; 180 | height: 1.8rem; 181 | line-height: 1.8rem; 182 | padding: 0 1.0rem; 183 | text-align: center; 184 | text-decoration: none; 185 | white-space: nowrap; 186 | min-width: 100px; 187 | } 188 | 189 | [role="button"]:focus, [role="button"]:hover, button:focus, button:hover, input[type='button']:focus, input[type='button']:hover, input[type='reset']:focus, input[type='reset']:hover, input[type='submit']:focus, input[type='submit']:hover { 190 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 191 | } 192 | 193 | /**************************************************** 194 | forms 195 | ****************************************************/ 196 | 197 | input[type='color'], input[type='date'], input[type='datetime'], input[type='time'], input[type='datetime-local'], input[type='email'], input[type='month'], input[type='number'], input[type='password'], input[type='search'], input[type='tel'], input[type='text'], input[type='url'], input[type='week'], input:not([type]), textarea, select { 198 | -webkit-appearance: none; 199 | background-color: transparent; 200 | border: 0.1rem solid #d1d1d1; 201 | border-radius: 5px; 202 | box-shadow: none; 203 | box-sizing: inherit; 204 | font-family: monospace; 205 | font-size: 1.2em; 206 | padding: .5em 1.0em .5em; 207 | width: 100%; 208 | } 209 | 210 | input[type='color']:focus, input[type='date']:focus, input[type='time']:focus, input[type='datetime']:focus, input[type='datetime-local']:focus, input[type='email']:focus, input[type='month']:focus, input[type='number']:focus, input[type='password']:focus, input[type='search']:focus, input[type='tel']:focus, input[type='text']:focus, input[type='url']:focus, input[type='week']:focus, input:not([type]):focus, textarea:focus, select:focus { 211 | border-color: #0074d9; 212 | outline: 0; 213 | } 214 | 215 | select { 216 | background: url('data:image/svg+xml;utf8,') center right no-repeat; 217 | } 218 | 219 | select[multiple] { 220 | background: none; 221 | height: auto; 222 | } 223 | 224 | textarea { 225 | min-height: 6.5rem; 226 | } 227 | 228 | fieldset { 229 | border-width: 0; 230 | padding: 0; 231 | } 232 | 233 | input[type='checkbox'], input[type='radio'] { 234 | display: inline; 235 | } 236 | 237 | [disabled] { 238 | cursor: default; 239 | opacity: .5; 240 | } 241 | 242 | /**************************************************** 243 | grid and page formatting 244 | ****************************************************/ 245 | 246 | body > center > * { 247 | text-align: initial; 248 | max-width: 900px; 249 | margin-left: auto; 250 | margin-right: auto; 251 | } 252 | 253 | .columns { 254 | display: table; 255 | width: 100%; 256 | } 257 | 258 | .columns .columns { 259 | margin: 0 -1.5em; 260 | } 261 | 262 | @media (min-width:600px) { 263 | .col,.c25,.c33,.c50,.c66,.c75 { 264 | padding: 1.5em; 265 | display: table-cell; 266 | vertical-align: top; 267 | } 268 | .c25 { width: 24.9%; } 269 | .c33 { width: 33.3%; } 270 | .c50 { width: 49.9%; } 271 | .c66 { width: 66.6%; } 272 | .c75 { width: 74.9%; } 273 | } 274 | 275 | @media (max-width:600px) { 276 | .col,.c25,.c33,.c50,.c66,.c75 { 277 | padding: 20px; 278 | display: block; 279 | vertical-align: top; 280 | } 281 | } 282 | 283 | .columns:after { 284 | content: ""; 285 | clear: both; 286 | display: table; 287 | } 288 | 289 | /**************************************************** 290 | colors 291 | ****************************************************/ 292 | 293 | .transparent{background-color:transparent;color:#111} 294 | .default{background-color:#0074d9;color:white} 295 | .success{background-color:#2ecc40;color:white} 296 | .warning{background-color:#ffdc00;color:#111} 297 | .error{background-color:#cc1f00;color:white} 298 | .info{background-color:#f1f1f1;color:#111} 299 | .white{background-color:white;color:#111} 300 | .black{background-color:#111;color:white} 301 | 302 | /**************************************************** 303 | navigation and nested menu 304 | ****************************************************/ 305 | 306 | nav { 307 | position:relative; 308 | padding: 0 1.5em; 309 | display: table; 310 | width: 100vw; 311 | height: 40px; 312 | } 313 | 314 | nav ul { 315 | list-style:none; 316 | position:relative; 317 | padding:0 318 | } 319 | 320 | nav > input[type=checkbox], nav > label { 321 | display: none; 322 | } 323 | 324 | @media (min-width:600px) { 325 | nav > * { 326 | display: table-cell; 327 | vertical-align: middle; 328 | } 329 | nav > ul:last-child { 330 | float:right; 331 | } 332 | nav > ul > li { 333 | padding: 1.5em 0.5em 334 | } 335 | } 336 | 337 | @media (max-width:600px) { 338 | nav > ul { 339 | display: table-column; 340 | vertical-align: middle; 341 | } 342 | nav > label { 343 | position: absolute; 344 | display: inline-block; 345 | top: 5px; 346 | right: 20px; 347 | font-size: 2em; 348 | } 349 | nav > a { 350 | display: inline-block; 351 | padding-top: 8px !important; 352 | } 353 | nav > ul { 354 | display: block; 355 | } 356 | nav > input[type=checkbox]:not(:checked) ~ ul { 357 | display: none; 358 | } 359 | nav > ul > li { 360 | display: block; 361 | text-align: center; 362 | padding: 0.5em 0.2em; 363 | } 364 | } 365 | 366 | nav li:hover { 367 | background-color: #0074d9 368 | } 369 | 370 | nav li:hover > a { 371 | color: white 372 | } 373 | 374 | nav a { 375 | padding:0 5px; 376 | text-decoration:none; 377 | text-align:left; 378 | color: black; 379 | } 380 | 381 | nav.black ul ul a { 382 | color: black 383 | } 384 | 385 | nav.black a, nav.black > label { 386 | color: white 387 | } 388 | 389 | nav li { 390 | position:relative; 391 | margin:0; 392 | padding:0; 393 | display: inline-block 394 | } 395 | 396 | nav ul ul { 397 | border:1px solid #e1e1e1; 398 | visibility:hidden; 399 | opacity:0; 400 | position:absolute; 401 | top:90%; 402 | left:-20px; 403 | padding:0; 404 | z-index:1000; 405 | transition:all 0.2s ease-out; 406 | list-style-type: none; 407 | box-shadow:5px 5px 10px #666; 408 | background-color: white 409 | } 410 | 411 | nav ul ul li { 412 | width: 100%; 413 | } 414 | 415 | nav ul ul a { 416 | padding:10px 15px; 417 | color:#333; 418 | font-weight:700; 419 | font-size:12px; 420 | line-height:16px; 421 | display: block; 422 | color: #111; 423 | } 424 | 425 | nav ul ul ul { 426 | top:0; 427 | left:80%; 428 | z-index:1100 429 | } 430 | 431 | nav li:hover > ul { 432 | visibility:visible; 433 | opacity:1 434 | } 435 | 436 | nav>li>ul>li:first-child:before{ 437 | content:''; 438 | position:absolute; 439 | width:1px; 440 | height:1px; 441 | border:10px solid transparent; 442 | left:50px; 443 | top:-20px; 444 | margin-left:-10px; 445 | border-bottom-color:white 446 | } 447 | 448 | /**************************************************** 449 | modal 450 | ****************************************************/ 451 | 452 | [role="dialog"] > div { 453 | position:fixed; 454 | z-index:9999; 455 | top:0; 456 | bottom:0; 457 | left:0; 458 | right:0; 459 | background-color:rgba(0,0,0,0.8); 460 | padding-top:20vh; 461 | transition:opacity 500ms; 462 | visibility:hidden; 463 | opacity:0; 464 | } 465 | [role="dialog"] > input[type=checkbox] { display: none !important; } 466 | input[type=checkbox]:checked ~ div {visibility:visible; opacity:1} 467 | [role="dialog"] > div > *:not(.close) {width: 66%; margin-left:auto; margin-right:auto; border-radius: 5px;} 468 | [role="dialog"] > div > .close, [role="alert"] > .close { 469 | background: url('data:image/svg+xml;utf8,') center right no-repeat; 470 | width:24px; height:24px; cursor: pointer; position:absolute; top:15px; right:15px; 471 | cursor: pointer; 472 | } 473 | 474 | /**************************************************** 475 | accordion 476 | ****************************************************/ 477 | 478 | .accordion>label{cursor:pointer} 479 | .accordion>input ~ label:before {content:"\25b2"; color:#ddd} 480 | .accordion>input:checked ~ label:before {content:"\25bc"; color:#ddd} 481 | .accordion>input {display:none} 482 | .accordion>input:checked ~ *:not(label) { 483 | max-height: 1000px !important; 484 | overflow:hidden !important; 485 | -webkit-transition: max-height .3s ease-in; 486 | transition: max-height .3s ease-in; 487 | } 488 | 489 | .accordion>*:not(label) { 490 | max-height: 0; 491 | overflow: hidden; 492 | margin: 0; 493 | padding: 0; 494 | -webkit-transition: max-height .3s ease-out; 495 | transition: max-height .3s ease-out; 496 | } 497 | 498 | /**************************************************** 499 | convenience 500 | ****************************************************/ 501 | 502 | [role="alert"] { 503 | margin: 1.5em; 504 | padding: 1.5em; 505 | position: relative; 506 | border-radius: 5px; 507 | color: black; 508 | } 509 | 510 | [role="alert"] > .close { 511 | position: absolute; 512 | top: 10px; 513 | right: 10px; 514 | } 515 | 516 | .padded { 517 | padding: 1.5em; 518 | } 519 | 520 | .fill { 521 | width: 100%; 522 | } 523 | 524 | ul.tags-list { 525 | padding-left: 0; 526 | } 527 | 528 | ul.tags-list li { 529 | display: inline-block; 530 | border-radius: 100px; 531 | background-color: #111111; 532 | color: white; 533 | padding: 0.3em 0.8em 0.2em 0.8em; 534 | line-height: 1.2em; 535 | margin: 2px; 536 | cursor: pointer; 537 | opacity: 0.2; 538 | text-transform: capitalize; 539 | } 540 | 541 | ul.tags-list li[data-selected=true] { 542 | opacity: 1.0; 543 | } -------------------------------------------------------------------------------- /static/js/utils.js: -------------------------------------------------------------------------------- 1 | "user strict"; 2 | 3 | // Allows "bla {a} bla {b}".format({'a': 'hello', 'b': 'world'}) 4 | if (!String.prototype.format) { 5 | String.prototype.format = function (args) { 6 | return this.replace(/\{([^}]+)\}/g, function (match, k) { return args[k]; }); 7 | }; 8 | } 9 | 10 | // Similar to jQuery $ but lighter 11 | window.Q = function(sel, el) { return (el||document).querySelectorAll(sel); }; 12 | 13 | // Clone any object 14 | Q.clone = function (data) { return JSON.parse(JSON.stringify(data)); }; 15 | 16 | Q.eval = function(text) { return eval('('+text+')'); }; 17 | 18 | // Given a url retuns an object with parsed query string 19 | Q.get_query = function (source) { 20 | source = source || window.location.search.substring(1); 21 | var vars = {}, items = source.split('&'); 22 | items.map(function (item) { 23 | var pair = item.split('='); 24 | vars[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 25 | }); 26 | return vars; 27 | }; 28 | 29 | // a wrapper for fetch return a promise 30 | Q.ajax = function(method, url, data, headers) { 31 | var options = { 32 | method: method, 33 | referrerPolicy: 'no-referrer', 34 | } 35 | if (data){ 36 | if ( !(data instanceof FormData)){ 37 | options.headers = {'Content-type': 'application/json'}; 38 | data = JSON.stringify(data); 39 | } 40 | options.body = data; 41 | } 42 | if (headers) for(var name in headers) options.headers[name] = headers[name]; 43 | return new Promise(function(resolve, reject) { 44 | fetch(url, options).then(function(res){ 45 | res.text().then(function(body){ 46 | res.data = body; 47 | res.json = function(){return JSON.parse(body);}; 48 | resolve(res); 49 | }, reject);}).catch(reject); 50 | }); 51 | } 52 | // Gets a cookie value 53 | Q.get_cookie = function (name) { 54 | var cookie = RegExp("" + name + "[^;]+").exec(document.cookie); 55 | if (!cookie) return null; 56 | return decodeURIComponent(!!cookie ? cookie.toString().replace(/^[^=]+./, "") : ""); 57 | }; 58 | 59 | // Gets a session token (py4web specific) 60 | Q.get_session_token = function () { 61 | var app_name = Q.get_cookie('app_name'); 62 | return Q.get_cookie(app_name + '_session'); 63 | }; 64 | 65 | // Load data from localstorage 66 | Q.retrieve = function (key) { 67 | try { 68 | return JSON.parse(window.localStorage.getItem(key)); 69 | } catch (e) { 70 | return null; 71 | } 72 | }; 73 | 74 | // Save data to localstorage 75 | Q.store = function (key, value) { 76 | window.localStorage.setItem(key, JSON.stringify(value)); 77 | }; 78 | 79 | // Load components lazily: https://vuejs.org/v2/guide/components.html#Async-Components 80 | Q.register_vue_component = function (name, src, onload) { 81 | Vue.component(name, function (resolve, reject) { 82 | Q.ajax('GET', src).then(function(res){resolve(onload(res));}); 83 | }); 84 | }; 85 | 86 | // Passes binary data to callback on drop of file in elem_id 87 | Q.upload_helper = function (elem_id, callback) { 88 | // function from http://jsfiddle.net/eliseosoto/JHQnk/ 89 | var elem = document.getelemById(elem_id); 90 | if (elem) { 91 | var files = elem.files; 92 | var reader = new FileReader(); 93 | if (files && files[0]) { 94 | reader.onload = function (event) { 95 | var b64 = btoa(event.target.result); 96 | callback(files[0].name, b64); 97 | }; 98 | reader.readAsBinaryString(files[0]); 99 | } else { 100 | callback(); 101 | } 102 | } 103 | }; 104 | 105 | // Internationalization helper 106 | // Usage: 107 | // T.translations = {'dog': {0: 'no cane', 1: 'un case', 2: '{n} cani', 10: 'tanti cani'}}; 108 | // T('dog').format({n: 5}) -> "5 cani" 109 | var T = function (text) { 110 | var obj = { 111 | toString: function () { return T.format(text); }, 112 | format: function (args) { return T.format(text, args); } 113 | }; 114 | return obj; 115 | }; 116 | 117 | // Adds a convenience format method to the client-side translator object 118 | T.format = function (text, args) { 119 | args = args || {}; 120 | translations = (T.translations || {})[text]; 121 | var n = ('n' in args) ? args.n : 1; 122 | if (translations) { 123 | var k = 0; 124 | for (var key in translations) { 125 | var i = parseInt(key); 126 | if (i <= n) k = i; else break; 127 | } 128 | text = translations[k]; 129 | } 130 | return text; 131 | }; 132 | 133 | // Originally inspired by David Walsh (https://davidwalsh.name/javascript-debounce-function) 134 | Q.debounce = (func, wait) => { 135 | let timeout; 136 | return function executedFunction(...args) { 137 | const later = () => { 138 | clearTimeout(timeout); 139 | func(...args); 140 | }; 141 | clearTimeout(timeout); 142 | timeout = setTimeout(later, wait); 143 | }; 144 | }; 145 | 146 | // https://levelup.gitconnected.com/throttle-in-javascript-improve-your-applications-performance-984a4e020a3f 147 | Q.throttle = (callback, delay) => { 148 | let throttleTimeout = null; 149 | let storedEvent = null; 150 | const throttledEventHandler = event => { 151 | storedEvent = event; 152 | const shouldHandleEvent = !throttleTimeout; 153 | if (shouldHandleEvent) { 154 | callback(storedEvent); 155 | storedEvent = null; 156 | throttleTimeout = setTimeout(() => { 157 | throttleTimeout = null; 158 | if (storedEvent) { 159 | throttledEventHandler(storedEvent); 160 | } 161 | }, delay); 162 | } 163 | }; 164 | return throttledEventHandler; 165 | }; 166 | 167 | // A Vue app prototype 168 | Q.app = function (elem_id) { 169 | self = {}; 170 | self.elem_id = elem_id || 'vue'; 171 | self.data = { loading: 0, page: null, state: null }; 172 | self.methods = {}; 173 | self.filters = {}; 174 | self.watch = {}; 175 | self.pages = {}; 176 | // translations 177 | self.methods.T = T; 178 | // toggles a variable 179 | self.methods.toggle = function (obj, key) { obj[key] = !obj[key] }; 180 | // sets a variable 181 | self.methods.set = function (obj, key, value) { obj[key] = value; }; 182 | // goto a given page and state (state should be 1 level deep dict 183 | self.methods.go = function (page, state, push) { 184 | self.v.loading++; 185 | var pagecall = self.pages[page]; 186 | if (pagecall) pagecall(state, function () { 187 | if (push) { 188 | var path = self.base + '/' + page; 189 | if (state) for (var key in state) path += '/' + key + '/' + state[key]; 190 | window.history.pushState(self.v, page, path); 191 | } 192 | self.v.loading--; 193 | self.v.page = page; 194 | self.v.state = state; 195 | }); 196 | }; 197 | // restores state when navigating history 198 | self.onpopstate = function (event) { 199 | for (var key in event.state) self.v[key] = event.state[key]; 200 | }; 201 | self.start = function (base) { 202 | self.base = base = base || window.location.href;; 203 | self.v = new Vue({ 204 | el: '#' + self.elem_id, 205 | data: self.data, 206 | methods: self.methods, 207 | watch: self.watch, 208 | filters: self.filters 209 | }); 210 | var parts = window.location.href.substr(base.length); 211 | var page = parts[0]; 212 | var state = {}; 213 | for (var i = 1; i < parts.length; i += 2) state[parts[i]] = parts[i + 1]; 214 | self.v.go(page, state, false); 215 | window.onpopstate = self.onpopstate; 216 | }; 217 | return self; 218 | }; 219 | 220 | // Renders a JSON field with tags_input 221 | Q.tags_input = function(elem, options) { 222 | if (typeof elem === typeof '') elem = Q(elem)[0]; 223 | if (!options) 224 | options = Q.eval(elem.dataset.options||'{}'); 225 | // preferred set of tags 226 | if (options.tags === undefined) options.tags = []; 227 | // set to false to only allow selecting one of the specified tags 228 | if (options.freetext === undefined) options.freetext = true; 229 | // how to transform typed tags to convert to actual tags 230 | if (options.transform === undefined) options.transform = function(x) {return x.toLowerCase();} 231 | // how to display tags 232 | if (options.labels === undefined) options.labels = {}; 233 | // placeholder for the freetext field 234 | if (options.placeholder === undefined) options.placeholder = ""; 235 | // autocomplete list attribute https://www.w3schools.com/tags/tag_datalist.asp 236 | if (options.autocomplete_list === undefined) options.autocomplete_list = null; 237 | var tags = options.tags; 238 | if(!elem) { console.log('Q.tags_input: elem '+selector+' not found'); return; } 239 | elem.type = "hidden"; 240 | var repl = document.createElement('ul'); 241 | repl.classList.add('tags-list') 242 | elem.parentNode.insertBefore(repl, elem); 243 | var keys = Q.eval(elem.value||'[]'); 244 | keys.map(function(x) { if(tags.indexOf(x)<0) tags.push(x); }); 245 | var fill = function(elem, repl) { 246 | repl.innerHTML = ''; 247 | tags.forEach(function(x){ 248 | console.log(x); 249 | var item = document.createElement('li'); 250 | item.innerHTML = options.labels[x] || x; 251 | item.dataset.value = x; 252 | item.dataset.selected = keys.indexOf(x)>=0; 253 | repl.appendChild(item); 254 | item.onclick = function(evt){ 255 | if(item.dataset.selected=='false') keys.push(x); else keys = keys.filter(function(y){ return x!=y; }); 256 | item.dataset.selected = keys.indexOf(x)>=0; 257 | elem.value = JSON.stringify(keys); 258 | }; 259 | }); 260 | }; 261 | if (options.freetext) { 262 | var inp = document.createElement('input'); 263 | elem.parentNode.insertBefore(inp, elem); 264 | inp.classList = elem.classList; 265 | inp.placeholder = options.placeholder; 266 | inp.setAttribute('list', options.autocomplete_list); 267 | inp.onchange = function(evt) { 268 | inp.value.split(',').map(function(x){ 269 | x = options.transform(x.trim()); 270 | if (options.regex && !x.match(options.regex)) return; 271 | if (x && tags.indexOf(x)<0) tags.push(x); 272 | if (x && keys.indexOf(x)<0) keys.push(x); 273 | }); 274 | inp.value = ''; 275 | elem.value = JSON.stringify(keys); 276 | fill(elem, repl); 277 | }; 278 | } 279 | fill(elem, repl); 280 | }; 281 | 282 | // Password strenght calculator 283 | Q.score_password = function(text) { 284 | var score = -10, counters = {}; 285 | text.split('').map(function(c){counters[c]=(counters[c]||0)+1; score += 5/counters[c];}); 286 | [/\d/, /[a-z]/, /[A-Z]/, /\W/].map(function(re){ score += re.test(text)?10:0; }); 287 | return Math.round(Math.max(0, score)); 288 | }; 289 | 290 | // Apply the strength calculator to some input field 291 | Q.score_input = function(elem, reference) { 292 | if (typeof elem === typeof '') elem = Q(elem)[0]; 293 | reference = reference || 100; 294 | elem.style.backgroundPosition = 'center right'; 295 | elem.style.backgroundRepeat = 'no-repeat'; 296 | elem.onkeyup = elem.onchange = function(evt) { 297 | var score = Q.score_password(elem.value.trim()); 298 | var r = Math.round(255*Math.max(0,Math.min(2-2*score/reference,1))); 299 | var g = Math.round(255*Math.max(0,Math.min(2*score/reference,1))); 300 | elem.style.backgroundImage = (score==0)?"":("url('"+'data:image/svg+xml;utf8,'+"')"); 301 | }; 302 | }; 303 | 304 | // Traps a form submission 305 | Q.trap_form = function (action, elem_id) { 306 | Q('#' + elem_id + ' form:not(.no-form-trap)').forEach(function (form) { 307 | var target = form.dataset['component_target'] || elem_id; 308 | form.dataset['component_target'] = target; 309 | var url = form.action; 310 | if (url === '' || url === '#' || url === void 0) url = action; 311 | var clickable = 'input[type=submit], input[type=image], button[type=submit], button:not([type])'; 312 | form.querySelectorAll(clickable).forEach(function (elem) { 313 | elem.onclick = function(event) { 314 | event.preventDefault(); 315 | form.querySelectorAll(clickable).forEach(function(elem) { 316 | elem.disabled = true; 317 | }); 318 | var form_data = new FormData(form); // Allows file uploads. 319 | Q.load_and_trap('POST', url, form_data, target); }; 320 | }); 321 | }); 322 | }; 323 | 324 | // loads a component via ajax and traps its forms 325 | Q.load_and_trap = function (method, url, form_data, target) { 326 | method = (method || 'GET').toLowerCase(); 327 | /* if target is not there, fill it with something that there isn't in the page*/ 328 | if (target === void 0 || target === '') target = 'none'; 329 | var onsuccess = function(res) { 330 | if (res.redirected) window.location = res.url; 331 | Q('#'+target)[0].innerHTML = res.data; 332 | Q.trap_form(url, target); 333 | console.log(res.headers); 334 | var flash = res.headers.get('component-flash'); 335 | if (flash) Q.flash(JSON.parse(flash)); 336 | }; 337 | var onerror = function(res) { 338 | alert('ajax error'); 339 | }; 340 | Q.ajax(method, url, form_data).then(onsuccess).catch(onerror); 341 | }; 342 | 343 | // Loads all ajax components 344 | Q.handle_components = function() { 345 | Q('ajax-component').forEach(function(elem) { 346 | Q.load_and_trap('GET', elem.attributes.url.value, null, elem.attributes.id.value); 347 | }); 348 | }; 349 | 350 | // Displays flash messages 351 | Q.handle_flash = function() { 352 | var elem = Q('flash-alerts')[0]; 353 | var make_delete_handler = function(node) { 354 | return function(event) { 355 | node.parentNode.removeChild(node); 356 | }; 357 | }; 358 | var make_handler = function(elem) { 359 | return function (event) { 360 | var node = document.createElement("div"); 361 | node.innerHTML = '
{0}
'.format([event.detail.message]); 362 | node = Q('[role="alert"]', node)[0]; 363 | node.classList.add(event.detail.class||'info'); 364 | elem.appendChild(node); 365 | Q('[role="alert"] .close',node)[0].onclick = make_delete_handler(node); 366 | }; 367 | }; 368 | if (elem) { 369 | elem.addEventListener('flash', make_handler(elem), false); 370 | Q.flash = function(detail) {elem.dispatchEvent(new CustomEvent('flash', {detail: detail}));}; 371 | console.log(elem.dataset.alert); 372 | if (elem.dataset.alert) Q.flash(Q.eval(elem.dataset.alert)); 373 | } 374 | }; 375 | 376 | Q.handle_components(); 377 | Q.handle_flash(); 378 | Q('input[type=text].type-list-string').forEach(function(elem){Q.tags_input(elem);}); 379 | Q('input[type=text].type-list-integer').forEach(function(elem){Q.tags_input(elem, {regex:/[-+]?[\d]+/});}); 380 | Q('input[name=password],input[name=new_password]').forEach(Q.score_input); 381 | -------------------------------------------------------------------------------- /stats.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, Field, session, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from py4web.utils.form import Form, FormStyleBulma 5 | from .models import MT_FILTER_TYPES 6 | from pydal.validators import * 7 | from . common import jasmin 8 | from .utils import cols_split 9 | 10 | 11 | def index(): 12 | return dict() 13 | 14 | @action('stats', method=['GET', 'POST']) 15 | @action.uses('stats.html') 16 | def stats(): 17 | return dict() 18 | 19 | @action('users_stats', method=['GET', 'POST']) 20 | @action('users_stats/', method=['GET', 'POST']) 21 | @action.uses('users_stats.html') 22 | def users_stats(usr): 23 | usr=usr 24 | tt = jasmin.stats(['user',usr]) 25 | users = [] 26 | for t in tt[2:-1]: 27 | user = [] 28 | r = str.split(t) 29 | l = len(r) 30 | if l ==4: 31 | ite = r[0][1:] 32 | tpe = r[1]+r[2] 33 | val = r[3] 34 | else: 35 | ite = r[0][1:] 36 | tpe = r[1]+r[2] 37 | if r[0][1:4] == 'bou': 38 | val = r[3]+" "+r[4]+" "+r[5]+" "+r[6]+" "+r[7]+" "+r[8] 39 | else: 40 | val = r[3]+" "+r[4] 41 | user.append(ite) 42 | user.append(tpe) 43 | user.append(val) 44 | users.append(user) 45 | return dict(usr=usr,items=users) 46 | 47 | @action('smppc_stats', method=['GET', 'POST']) 48 | @action('smppc_stats/', method=['GET', 'POST']) 49 | @action.uses('smppc_stats.html') 50 | def smppc_stats(con): 51 | con = con 52 | tt = jasmin.stats(['smppc', con]) 53 | connectors = [] 54 | for t in tt[2:-1]: 55 | connector = [] 56 | r = str.split(t) 57 | l = len(r) 58 | if l ==2: 59 | fld = r[0][1:] 60 | val = r[1] 61 | connector.append(fld) 62 | connector.append(val) 63 | connectors.append(connector) 64 | elif l == 1: #weird shit here 65 | fld = r[0][1:] 66 | val = "What is this" 67 | 68 | else: 69 | fld = r[0][1:] 70 | val = r[1]+" "+r[2] 71 | connector.append(fld) 72 | connector.append(val) 73 | connectors.append(connector) 74 | 75 | 76 | return dict(con=con,items=connectors) 77 | 78 | @action('httpapi_stats', method=['GET', 'POST']) 79 | @action.uses('httpapi_stats.html') 80 | def httpapi_stats(): 81 | items = [] 82 | tt=(jasmin.stats(['httpapi'])) 83 | lines=tt 84 | if not lines: 85 | return items 86 | 87 | for t in tt[2:-1]: 88 | item = [] 89 | r = str.split(t) 90 | l = len(r) 91 | if l ==2: 92 | fld = r[0][1:] 93 | val = r[1] 94 | item.append(fld) 95 | item.append(val) 96 | items.append(item) 97 | else: 98 | fld = r[0][1:] 99 | val = r[1]+" "+r[2] 100 | 101 | item.append(fld) 102 | item.append(val) 103 | items.append(item) 104 | 105 | return dict(items=items) 106 | 107 | @action('smppsapi_stats', method=['GET', 'POST']) 108 | @action.uses('smppsapi_stats.html') 109 | def smppsapi_stats(): 110 | items = [] 111 | tt=(jasmin.stats(['smppsapi'])) 112 | lines=tt 113 | if not lines: 114 | return items 115 | 116 | for t in tt[2:-1]: 117 | item = [] 118 | r = str.split(t) 119 | l = len(r) 120 | if l ==2: 121 | fld = r[0][1:] 122 | val = r[1] 123 | item.append(fld) 124 | item.append(val) 125 | items.append(item) 126 | else: 127 | fld = r[0][1:] 128 | val = r[1]+" "+r[2] 129 | 130 | item.append(fld) 131 | item.append(val) 132 | items.append(item) 133 | 134 | return dict(items=items) 135 | 136 | @action('smppcs_stats', method=['GET', 'POST']) 137 | @action.uses('smppcs_stats.html') 138 | def smppcs_stats(): 139 | connectors = [] 140 | rows=(jasmin.stats(['smppcs'])) 141 | lines=rows 142 | if not lines: 143 | return connectors 144 | tt = cols_split(lines[2:-2]) 145 | for row in tt: 146 | connector={} 147 | n = len(row) 148 | if n == 11: 149 | connector.update( 150 | cid=row[0][1:], 151 | ca=row[1]+ " "+row[2], 152 | ba=row[3]+ " "+row[4], 153 | da= row[5]+" "+row[6], 154 | sm=row[7], 155 | dl=row[8], 156 | qos=row[9], 157 | other=row[10] 158 | ) 159 | elif n == 10: 160 | connector.update( 161 | cid=row[0][1:], 162 | ca=row[1]+ " "+row[2], 163 | ba=row[3], 164 | da= row[4]+" "+row[5], 165 | sm=row[6], 166 | dl=row[7], 167 | qos=row[8], 168 | other=row[9] 169 | ) 170 | elif n == 8: 171 | connector.update( 172 | cid=row[0][1:], 173 | ca=row[1], 174 | ba=row[2], 175 | da= row[3], 176 | sm=row[4], 177 | dl=row[5], 178 | qos=row[6], 179 | other=row[7] 180 | ) 181 | connectors.append(connector) 182 | return dict(cons = connectors) 183 | 184 | @action('user_stats', method=['GET', 'POST']) 185 | @action.uses('user_stats.html') 186 | def user_stats(): 187 | users = [] 188 | rows=(jasmin.stats(['users'])) 189 | lines=rows 190 | if not lines: 191 | return users 192 | tt = cols_split(lines[2:-2]) 193 | for row in tt: 194 | n = len(row) 195 | user={} 196 | if n == 6: 197 | if row[1] == '0': #must be http binds 198 | user.update( 199 | uid=row[0][1:], 200 | smpp_bc=row[1], 201 | smpp_la=row[2], 202 | http_rc=row[3], 203 | http_la=row[4]+" "+row[5], 204 | ) 205 | else: 206 | user.update( 207 | uid=row[0][1:], 208 | smpp_bc=row[1], 209 | smpp_la=row[2]+" "+row[3], 210 | http_rc=row[4], 211 | http_la=row[5] 212 | ) 213 | elif n == 7: # both http and smpp activity 214 | user.update( 215 | uid=row[0][1:], 216 | smpp_bc=row[1], 217 | smpp_la=row[2] + " "+row[3], 218 | http_rc=row[4], 219 | http_la=row[5] + " "+row[6] 220 | ) 221 | else: 222 | user.update( 223 | uid=row[0][1:], 224 | smpp_bc=row[1], 225 | smpp_la=row[2], 226 | http_rc=row[3], 227 | http_la=row[4] 228 | ) 229 | users.append(user) 230 | return dict(users=users) 231 | -------------------------------------------------------------------------------- /super_admin.py: -------------------------------------------------------------------------------- 1 | from py4web import action, request, abort, redirect, URL 2 | from yatl.helpers import A 3 | from .common import db, session, T, cache, auth, logger, authenticated, unauthenticated, flash 4 | from .user_manager import list_users, list_groups 5 | from .common import jasmin 6 | 7 | def get_groups(): 8 | return list_groups() 9 | 10 | def get_gid(group): 11 | query = db.j_group.name == group 12 | rec = db(query).select().first() 13 | return rec 14 | 15 | def get_users(): 16 | users = list_users() 17 | for user in users: 18 | user_creds = {} 19 | query = (db.j_user.username == user['username']) 20 | cc=db(query).select().first() 21 | if not cc: 22 | if user['gid'][0] == '!': 23 | aa = user['gid'][1:] 24 | group = db.get_gid(aa) 25 | else: 26 | group = get_gid(user['gid']) 27 | 28 | print('user', user['uid'], 'Group', group.id, group.name) 29 | creds = jasmin.users(['get_creds', user['uid'] ]) 30 | for cred in creds: 31 | if 'mt_messaging_cred' in cred: 32 | splits = cred.split() 33 | user_creds[splits[1] + '_'+splits[2]] = splits[3] 34 | elif 'smpps_cred' in cred: 35 | splits = cred.split() 36 | user_creds[splits[1] + '_'+splits[2]] = splits[3] 37 | else: 38 | splits = cred.split() 39 | user_creds[splits[0]] = splits[1] 40 | u_id = db.j_user.insert(j_uid = user_creds['uid'], j_group=group, username = user_creds['username']) 41 | credentials = db.j_user_cred.insert(juser = user_creds['uid'], 42 | default_src_addr=user_creds['defaultvalue_src_addr'], 43 | quota_http_throughput=user_creds['quota_http_throughput'], 44 | quota_balance=user_creds['quota_balance'], 45 | quota_smpps_throughput=user_creds['quota_smpps_throughput'], 46 | quota_sms_count=user_creds['quota_sms_count'], 47 | quota_early_percent=user_creds['quota_early_percent'], 48 | value_priority=user_creds['valuefilter_priority'], 49 | value_content=user_creds['valuefilter_content'], 50 | value_src_addr=user_creds['valuefilter_src_addr'], 51 | value_dst_addr=user_creds['valuefilter_dst_addr'], 52 | value_validity_period=user_creds['valuefilter_validity_period'], 53 | author_http_send=user_creds['authorization_http_send'], 54 | author_http_dlr_method=user_creds['authorization_http_dlr_method'], 55 | author_http_balance=user_creds['authorization_http_balance'], 56 | author_smpps_send=user_creds['authorization_smpps_send'], 57 | author_priority=user_creds['authorization_priority'], 58 | author_http_long_content=user_creds['authorization_http_long_content'], 59 | author_src_addr=user_creds['authorization_src_addr'], 60 | author_dlr_level=user_creds['authorization_dlr_level'], 61 | author_http_rate=user_creds['authorization_http_rate'], 62 | author_validity_period=user_creds['authorization_validity_period'], 63 | author_http_bulk=user_creds['authorization_http_bulk'] 64 | ) 65 | print('User_creds', user_creds['uid'], u_id) 66 | return users 67 | 68 | def get_filters(): 69 | from .filter_manager import list_filters 70 | filters = list_filters() 71 | for filter in filters: 72 | ret = db.mt_filter.update_or_insert(db.mt_filter.fid == filter['filter_id'], 73 | fid = filter['filter_id'], 74 | filter_type = filter['filter_type'], 75 | filter_route = filter['route'], 76 | f_value = filter['description']) 77 | rows = db(db.mt_filter.id > 0).select() 78 | return dict(rows=rows) 79 | 80 | def get_smppcons(): 81 | from .connector_manager import list_smpp_connectors 82 | connectors=list_smpp_connectors() 83 | for connector in connectors: 84 | c_det = {} 85 | con = jasmin.connector(['show', connector['cid']]) 86 | for c in con: 87 | c_split = c.split() 88 | if len(c_split) > 1: 89 | if c_split[1] == 'Not': 90 | c_det[c_split[0]] = 'Not defined' 91 | else: 92 | c_det[c_split[0]] = c_split[1] 93 | else: 94 | c_det[c_split[0]] = '' 95 | ret = db.connector.update_or_insert(db.connector.name == c_det['cid'], 96 | name=c_det['cid'], 97 | c_logfile=c_det['logfile'], 98 | c_logrotate=c_det['logrotate'], 99 | c_loglevel=c_det['loglevel'], 100 | c_host=c_det['host'], 101 | c_port=c_det['port'], 102 | c_ssl=c_det['ssl'], 103 | c_username=c_det['username'], 104 | c_password=c_det['password'], 105 | c_bind=c_det['bind'], 106 | c_bind_to=c_det['bind_to'], 107 | c_trx_to=c_det['trx_to'], 108 | c_res_to=c_det['res_to'], 109 | c_pdu_red_to=c_det['pdu_red_to'], 110 | c_con_loss_retry=c_det['con_loss_retry'], 111 | c_con_loss_delay=c_det['con_loss_delay'], 112 | c_con_fail_retry=c_det['con_fail_retry'], 113 | c_con_fail_delay=c_det['con_fail_delay'], 114 | c_src_addr=c_det['src_addr'], 115 | c_src_ton=c_det['src_ton'], 116 | c_src_npi=c_det['src_npi'], 117 | c_dst_ton=c_det['dst_ton'], 118 | c_dst_npi=c_det['dst_npi'], 119 | c_bind_ton=c_det['bind_ton'], 120 | c_bind_npi=c_det['bind_npi'], 121 | c_validity=c_det['validity'], 122 | c_priority=c_det['priority'], 123 | c_requeue_delay=c_det['requeue_delay'], 124 | c_addr_range=c_det['addr_range'], 125 | c_systype=c_det['systype'], 126 | c_dlr_expiry=c_det['dlr_expiry'], 127 | c_submit_throughput=c_det['submit_throughput'], 128 | c_proto_id=c_det['proto_id'], 129 | c_coding=c_det['coding'], 130 | c_elink_interval=c_det['elink_interval'], 131 | c_def_msg_id=c_det['def_msg_id'], 132 | c_ripf=c_det['ripf'], 133 | c_dlr_msgid=c_det['dlr_msgid']) 134 | 135 | rows = db(db.connector.id > 0).select() 136 | return dict(rows=rows) 137 | 138 | def get_httpcons(): 139 | from .connector_manager import http_cons 140 | connectors=http_cons() 141 | for con in connectors: 142 | ret = db.http_cons.update_or_insert(db.http_cons.hcon_cid == con['cid'], 143 | hcon_cid = con['cid'], 144 | hcon_method = con['method'], 145 | hcon_url = con['baseurl']) 146 | 147 | rows = db(db.http_cons).select() 148 | return dict(rows=rows) 149 | 150 | def get_fid(f_type, f_val=None): 151 | query = (db.mt_filter.filter_type == f_type)&(db.mt_filter.f_value == f_val) 152 | filter = db(query).select().first() 153 | return filter.id 154 | 155 | def get_mtroutes(): 156 | import re 157 | con_regex = '.*\((.*?)\).*' 158 | fil_regex = '.*\<(.*?)\>.*' 159 | from .route_manager import mt_routes 160 | routes = mt_routes() 161 | for route in routes: 162 | c_split = route['r_connectors'].split() 163 | cids = [] 164 | fids = [] 165 | for con in c_split: 166 | matches = re.search(con_regex, con) 167 | connector = matches.group(1) 168 | if 'smpp' in con: 169 | c = db(db.connector.name == connector).select().first() 170 | cids.append(c.id) 171 | else: 172 | print('MT ROUTES RE HTTP CONNECTORS', con, connector ) 173 | f_split = route['r_filters'].split(', ') 174 | for f in f_split: 175 | f_type = '' 176 | f_val = '' 177 | if 'DA' in f: 178 | f_type = 'DestinationAddrFilter' 179 | matches = re.search(con_regex, f) 180 | if matches: 181 | line = matches.group(1) 182 | f_val= line.split('=')[1] 183 | elif 'U' in f: 184 | f_type = 'UserFilter' 185 | matches = re.search(con_regex, f) 186 | if matches: 187 | line = matches.group(1) 188 | f_val= line.split('=')[1] 189 | elif 'SA' in f: 190 | f_type = 'SourceAddrFilter' 191 | matches = re.search(con_regex, f) 192 | if matches: 193 | line = matches.group(1) 194 | f_val= line.split('=')[1] 195 | elif 'SM' in f: 196 | f_type = 'ShortMessageFilter' 197 | matches = re.search(con_regex, f) 198 | if matches: 199 | line = matches.group(1) 200 | f_val= line.split('=')[1] 201 | elif 'DI' in f: 202 | f_type = 'DateIntervalFilter' 203 | matches = re.search(con_regex, f) 204 | if matches: 205 | line = matches.group(1) 206 | f_val= line 207 | elif 'TI' in f: 208 | f_type = 'TimeIntervalFilter' 209 | matches = re.search(con_regex, f) 210 | if matches: 211 | line = matches.group(1) 212 | f_val= line 213 | elif 'TG' in f: 214 | f_type = 'TagFilter' 215 | matches = re.search(con_regex, f) 216 | if matches: 217 | line = matches.group(1) 218 | f_val= line.split('=')[1] 219 | 220 | elif 'G' in f: 221 | f_type = 'GroupFilter' 222 | matches = re.search(con_regex, f) 223 | if matches: 224 | line = matches.group(1) 225 | f_val= line.split('=')[1] 226 | 227 | elif '' in f: 228 | f_type = 'TransparentFilter' 229 | else: 230 | continue 231 | fid = get_fid(f_type, f_val) 232 | fids.append(fid) 233 | # now after all this update the records 234 | ret = db.mtroute.update_or_insert(db.mtroute.mt_order == route['r_order'], 235 | mt_order = route['r_order'], 236 | mt_type = route['r_type'], 237 | mt_connectors = cids, 238 | mt_filters = fids, 239 | mt_rate = route['r_rate'] 240 | ) 241 | 242 | return 'Inside get_imos' 243 | 244 | def get_moroutes(): 245 | import re 246 | con_regex = '.*\((.*?)\).*' 247 | from .route_manager import mo_routes 248 | routes = mo_routes() 249 | for route in routes: 250 | c_split = route['r_connectors'].split() 251 | cids = [] 252 | fids = [] 253 | cidh = [] 254 | for con in c_split: 255 | matches = re.search(con_regex, con) 256 | connector = matches.group(1) 257 | if 'smpp' in con: 258 | c = db(db.connector.name == connector).select().first() 259 | cids.append(c.id) 260 | else: 261 | c = db(db.http_cons.hcon_cid == connector).select().first() 262 | cidh.append(c.id) 263 | f_split = route['r_filters'].split('>,') 264 | for f in f_split: 265 | f_type = '' 266 | f_val = '' 267 | if 'DA' in f: 268 | f_type = 'DestinationAddrFilter' 269 | matches = re.search(con_regex, f) 270 | if matches: 271 | line = matches.group(1) 272 | f_val= line.split('=')[1] 273 | elif 'U' in f: 274 | f_type = 'UserFilter' 275 | matches = re.search(con_regex, f) 276 | if matches: 277 | line = matches.group(1) 278 | f_val= line.split('=')[1] 279 | elif 'SA' in f: 280 | f_type = 'SourceAddrFilter' 281 | matches = re.search(con_regex, f) 282 | if matches: 283 | line = matches.group(1) 284 | f_val= line.split('=')[1] 285 | elif 'SM' in f: 286 | f_type = 'ShortMessageFilter' 287 | matches = re.search(con_regex, f) 288 | if matches: 289 | line = matches.group(1) 290 | f_val= line.split('=')[1] 291 | elif 'DI' in f: 292 | f_type = 'DateIntervalFilter' 293 | matches = re.search(con_regex, f) 294 | if matches: 295 | line = matches.group(1) 296 | f_val= line 297 | elif 'TI' in f: 298 | f_type = 'TimeIntervalFilter' 299 | matches = re.search(con_regex, f) 300 | if matches: 301 | line = matches.group(1) 302 | f_val= line 303 | elif 'TG' in f: 304 | f_type = 'TagFilter' 305 | matches = re.search(con_regex, f) 306 | if matches: 307 | line = matches.group(1) 308 | f_val= line.split('=')[1] 309 | 310 | elif 'G' in f: 311 | f_type = 'GroupFilter' 312 | matches = re.search(con_regex, f) 313 | if matches: 314 | line = matches.group(1) 315 | f_val= line.split('=')[1] 316 | 317 | elif ' 0).select() 338 | return dict(rows=rows) 339 | 340 | def get_imos(): 341 | return 'Inside get_imos' 342 | 343 | def get_imts(): 344 | return 'Isnside get_imts' 345 | 346 | @action("populate_database", method=['GET', 'POST']) 347 | @action.uses(db, session, auth, flash, "generic.html") 348 | def popualate_database(): 349 | groups = get_groups() 350 | users = get_users() 351 | filters= get_filters() 352 | smpp_cons = get_smppcons() 353 | http_cons = get_httpcons() 354 | mt_routes = get_mtroutes() 355 | mo_routes = get_moroutes() 356 | mo_interceptors = get_imos() 357 | mt_interceptors = get_imts() 358 | flash.set('Database populated with existing Jasmin data') 359 | redirect(URL('index')) 360 | 361 | 362 | @action("super_admin", method=['GET', 'POST']) 363 | @action.uses(db, session, auth, flash, "superadmin_index.html") 364 | def super_admin(): 365 | tot_imos = 0 366 | tot_imts = 0 367 | tot_users = 0 368 | tot_mtroutes = 0 369 | tot_moroutes = 0 370 | tot_imos = db(db.j_imo).count() 371 | tot_imts = db(db.j_imt).count() 372 | tot_users = db(db.j_user).count() 373 | tot_mtroutes = db(db.mtroute).count() 374 | tot_moroutes = db(db.moroute).count() 375 | return dict(tot_imos = tot_imos, 376 | tot_imts = tot_imts, 377 | tot_users = tot_users, 378 | tot_mtroutes = tot_mtroutes, 379 | tot_moroutes = tot_moroutes, 380 | ) 381 | @action('user_unbind', method=['GET', 'POST']) 382 | @action.uses(db, session, auth, flash) 383 | def user_unbind(user): 384 | j_user = user 385 | ret = jasmin.users(['unbind', j_user] ) 386 | return ret 387 | 388 | @action('user_ban/', method=['GET', 'POST']) 389 | @action.uses(db, session, auth, flash) 390 | def user_ban(user): 391 | j_user = user 392 | ret = jasmin.users(['ban', j_user] ) 393 | return ret 394 | 395 | @action('flush_moroutes', method=['GET', 'POST']) 396 | @action.uses(db, session, auth, flash) 397 | def flush_moroutes(): 398 | route= '' 399 | t = jasmin.morouter(['flush', route]) 400 | if not t: 401 | db(db.moroute.id > 0).delete() 402 | flash.set("Flushed all MO routes. This is unrecoverable") 403 | else: 404 | flash.set('Unable to fulsh MO routes. Please check and try again') 405 | redirect(URL('super_admin')) 406 | 407 | @action('flush_mtroutes', method=['GET', 'POST']) 408 | @action.uses(db, session, auth, flash) 409 | def flush_mtroutes(): 410 | route = '' 411 | t = jasmin.mtrouter(['flush',route]) 412 | if not t: 413 | db(db.mtroute.id > 0).delete() 414 | flash.set("Flushed all MT routes. This is unrecoverable") 415 | else: 416 | flash.set("Unable to flush MT routes. Please try again later") 417 | redirect(URL('super_admin')) 418 | 419 | @action('flush_imos', method=['GET', 'POST']) 420 | @action.uses(db, session, auth, flash) 421 | def flush_imos(): 422 | order = script = filters = '' 423 | resp= jasmin.interceptor(['mo','flush', order, script,filters]) 424 | if not resp: 425 | db(db.j_imo.id > 0).delete() 426 | flash.set("Flushed all MO Interceptors. This is unrecoverable") 427 | else: 428 | flash.set("Unable to flush all MO Interceptors. Please check connection and try again") 429 | redirect(URL('super_admin')) 430 | 431 | @action('flush_imts', method=['GET', 'POST']) 432 | @action.uses(db, session, auth, flash) 433 | def flush_imts(): 434 | order = script = filters = '' 435 | resp= jasmin.interceptor(['mt','flush', order, script,filters]) 436 | if not resp: 437 | db(db.j_imt.id > 0).delete() 438 | flash.set("Flushed all MT interceptors. This is unrecoverable") 439 | else: 440 | flash.set("Unable to flush all MT interceptors. Please check connection and try again") 441 | redirect(URL('super_admin')) 442 | 443 | @action('s_users', method=['GET', 'POST']) 444 | @action('s_users//', method=['GET', 'POST']) 445 | @action.uses(db, session, auth, flash, 's_users.html') 446 | def s_users(user=None, action=None): 447 | j_gid = '' 448 | if action == 'unbind': 449 | ret = user_unbind(user) 450 | if ret: 451 | flash.set(ret) 452 | else: 453 | flash.set('Successfully unbound user %s ' % user) 454 | elif action == 'ban': 455 | ret = user_ban(user) 456 | if ret: 457 | flash.set(ret) 458 | else: 459 | flash.set('Successfully unbound and banned user %s' % user) 460 | users=list_users() 461 | return dict(users=users) 462 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | """ 2 | To use celery tasks: 3 | 1) pip install -U "celery[redis]" 4 | 2) In settings.py: 5 | USE_CELERY = True 6 | CELERY_BROKER = "redis://localhost:6379/0" 7 | 3) Start "redis-server" 8 | 4) Start "celery -A apps.{appname}.tasks beat" 9 | 5) Start "celery -A apps.{appname}.tasks worker --loglevel=info" for each worker 10 | 11 | """ 12 | from .common import settings, scheduler, db, Field 13 | 14 | # example of task that needs db access 15 | @scheduler.task 16 | def my_task(): 17 | try: 18 | # this task will be executed in its own thread, connect to db 19 | db._adapter.reconnect() 20 | # do something here 21 | db.commit() 22 | except: 23 | # rollback on failure 24 | db.rollback() 25 | 26 | 27 | # run my_task very 10 seconds 28 | scheduler.conf.beat_schedule = { 29 | "my_first_task": { 30 | "task": "apps.%s.tasks.my_task" % settings.APP_NAME, 31 | "schedule": 10.0, 32 | "args": (), 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /templates/README copy.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/auth.html: -------------------------------------------------------------------------------- 1 | [[extend "layout_auth.html"]] 2 | 13 | [[block page_head]] 14 | 15 | [[end]] 16 | 17 |
18 | [[=form]] 19 |
20 | -------------------------------------------------------------------------------- /templates/generic.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 | 8 | 9 |
10 |
11 | [[=BEAUTIFY(__vars__)]] 12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/groups_list.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 | 9 |
10 |
11 |
12 |
Add a new Group
13 | [[=form]] 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | [[for con in groups:]] 26 | 27 | [[grp = con['gid'][0] ]] 28 | [[if grp[0] == '!':]] 29 | [[group_name = con['gid'][1:] ]] 30 | 31 | 39 | [[else:]] 40 | [[group_name = con['gid'] ]] 41 | 42 | 50 | [[pass]] 51 | 52 | 66 | 67 | [[pass]] 68 | 69 |
GroupStatusActions
[[=group_name]] 32 | 33 | 34 | 35 | 36 | Disabled 37 | 38 | [[=group_name]] 43 | 44 | 45 | 46 | 47 | Enabled 48 | 49 | 53 | 54 | 56 | 57 | 58 | 60 | 61 | 62 | 64 | 65 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /templates/http_connector_list.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | [[=form]] 12 |
13 |
14 |
15 |
16 |
17 |
18 |
HTTP Connector List
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | [[for con in cons:]] 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | [[pass]] 40 | 41 |
CIDTypeMethodBase URLOptions
[[=con['cid'] ]][[=con['c_type'] ]][[=con['method'] ]][[=con['baseurl'] ]] 35 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /templates/httpapi_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
HTTP Server Stats
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | [[for item in items:]] 27 | 28 | 29 | 30 | 31 | [[pass]] 32 | 33 |
ItemValue
[[=item[0] ]][[=item[1] ]]
34 |
35 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 | 129 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | [[block page_head]][[end]] 17 | 18 | 19 | 52 | [[if globals().get('flash'):]] 53 |
54 | 55 | 56 |
57 | [[pass]] 58 |
59 | [[include]] 60 | 64 | 65 |
66 |
67 |
68 |

69 | 70 | Powered By Py4web 71 |

72 |
73 |
74 | 75 | 76 | 77 | 78 | [[block page_scripts]][[end]] 79 | 80 | -------------------------------------------------------------------------------- /templates/layout_auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | [[block page_head]][[end]] 13 | 14 | 15 |
16 | 17 | 18 | 52 |
53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | [[include]] 62 |
63 |
64 | 65 |
66 |

67 | Made with py4web 68 |

69 |
70 | 71 | 72 | 73 | [[block page_scripts]][[end]] 74 | 75 | -------------------------------------------------------------------------------- /templates/layout_sav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | [[block page_head]][[end]] 32 | 33 | 34 | 68 | [[if globals().get('flash'):]] 69 |
70 | 71 | 72 |
73 | 81 | [[pass]] 82 |
83 | 84 | [[include]] 85 | 89 | 90 |
91 | 100 | 101 | 102 | 103 | 104 | [[block page_scripts]][[end]] 105 | 106 | -------------------------------------------------------------------------------- /templates/list_filters.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 |
4 | 5 | 6 | 9 | 10 |
11 |
12 |
13 | [[=form]] 14 |
15 |
16 |
17 |
18 |
19 |
20 |
List Filters
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | [[for con in filters:]] 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | [[pass]] 43 | 44 |
Filter IdTypeRouteValueOptions
[[=con['filter_id'] ]][[=con['filter_type'] ]][[=con['route'] ]][[=con['description'] ]] 37 | 39 | 40 |
45 |
46 |
47 | 59 | -------------------------------------------------------------------------------- /templates/list_interceptors.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 16 | 17 | 18 |
19 | 20 | 23 | 24 |
25 |
26 |
27 | [[=form]] 28 |
29 |
30 |
31 |
32 |
33 |
34 |
[[=title]]
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | [[for imt in imts:]] 45 | 46 | 47 | 48 | 49 | 50 | [[if type == 'mt':]] 51 | 52 | [[else:]] 53 | 54 | [[pass]] 55 | 56 | [[pass]] 57 | 58 |
OrderTypeScriptFiltersAvailabe Actions
[[=imt['i_order'] ]][[=imt['i_type'] ]][[=imt['i_script'] ]][[=imt['i_filter'] ]]
59 |
60 |
61 | -------------------------------------------------------------------------------- /templates/list_moroutes.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | [[=form]] 12 |
13 |
14 |
15 |
16 |
17 |
18 |
List MO Routes
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | [[for rt in routes:]] 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | [[pass]] 42 | 43 |
Route OrderTypeConnector ID(s)Filter(s)MO Router Actions
[[=rt['r_order'] ]][[=rt['r_type'] ]][[=rt['r_connectors'] ]][[=rt['r_filters'] ]] 35 | 36 | 38 | 39 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /templates/mt_routes_list.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | [[=form]] 12 |
13 |
14 |
15 |
16 |
17 |
18 |
List MT Routes
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | [[for rt in routes:]] 29 | 30 | 31 | 32 | [[if rt['r_rate'][2]== '!':]] 33 | 41 | 42 | [[else:]] 43 | 46 | [[pass]] 47 | 48 | 49 | 55 | 56 | [[pass]] 57 | 58 |
Route OrderTypeRateConnector ID(s)Filter(s)MT Router Actions
[[=rt['r_order'] ]][[=rt['r_type'] ]] 34 | 35 | 36 | 37 | 38 | 0.00 39 | 40 | 44 | [[=rt['r_rate'] ]] 45 | [[=rt['r_connectors'] ]][[=rt['r_filters'] ]] 50 | 51 | 53 | 54 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /templates/record_content.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 |
4 |
5 |
6 |
7 | 14 |
15 |
[[=title]]
16 |
17 |
18 |
19 | [[=content]] 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /templates/s_users.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | [[for con in users:]] 10 | 11 | [[if con['uid'][0] == '!':]] 12 | 20 | [[else:]] 21 | 29 | [[pass]] 30 | [[if con['gid'][0] == '!':]] 31 | 39 | [[else:]] 40 | 48 | [[pass]] 49 | 50 | [[if con['balanceh'][0] == 'N':]] 51 | 59 | [[else:]] 60 | 63 | [[pass]] 64 | [[if con['balances'][0] == 'N':]] 65 | 73 | [[else:]] 74 | 77 | [[pass]] 78 | [[if con['mt_http'][0] == 'N':]] 79 | 87 | [[else:]] 88 | 91 | [[pass]] 92 | [[if con['mt_smpp'][0] == 'N':]] 93 | 101 | [[else:]] 102 | 105 | [[pass]] 106 | 116 | 117 | [[pass]] 118 | 119 |
User IDGroup IDUser NameHTTP Bal.SMPP Bal.HTTP ThruputSMPP ThruputActions
13 | 14 | 15 | 16 | 17 | [[=con['uid'] ]] 18 | 19 | 22 | 23 | 24 | 25 | 26 | [[=con['uid'] ]] 27 | 28 | 32 | 33 | 34 | 35 | 36 | [[=con['gid'] ]] 37 | 38 | 41 | 42 | 43 | 44 | 45 | [[=con['gid'] ]] 46 | 47 | [[=con['username'] ]] 52 | 53 | 54 | 55 | 56 | ND 57 | 58 | 61 | [[=con['balanceh'] ]] 62 | 66 | 67 | 68 | 69 | 70 | ND 71 | 72 | 75 | [[=con['balances'] ]] 76 | 80 | 81 | 82 | 83 | 84 | ND 85 | 86 | 89 | [[=con['mt_http'] ]] 90 | 94 | 95 | 96 | 97 | 98 | ND 99 | 100 | 103 | [[=con['mt_smpp'] ]] 104 | 107 | 108 | 110 | 111 | 112 | 114 | 115 |
120 |
121 |
122 | -------------------------------------------------------------------------------- /templates/show_record.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 |
4 |
5 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | [[for row in rows:]] 21 | 22 | 25 | 29 |
FieldValue
23 | [[=row[0] ]] 24 | 26 | [[=row[1] ]] 27 | [[pass]] 28 |
30 |
31 |
32 | 35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /templates/smpp_connector_list.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 8 |
9 | 10 | 13 | 14 |
15 |
16 |
17 | [[=form]] 18 |
19 |
20 |
21 |
22 |
23 |
24 |

SMPP Connector List

25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | [[for con in cons:]] 35 | 36 | 37 | [[if con['status'] == 'started':]] 38 | 46 | [[else:]] 47 | 55 | [[pass]] 56 | [[conn = db(db.connector.name ==con['cid']).select().first()]] 57 | [[if conn:]] 58 | 59 | 60 | 61 | 62 | 63 | [[else:]] 64 | 65 | 66 | 67 | 68 | [[pass]] 69 | [[if con['session'][0]== 'B':]] 70 | 78 | [[else:]] 79 | 87 | [[pass]] 88 | 89 | 90 | 91 | 113 | 114 | [[pass]] 115 | 116 |
CIDStatusHostPortUsernamePaswordSessionStartsStopsOptions
[[=con['cid'] ]] 39 | 40 | 41 | 42 | 43 | [[=con['status'] ]] 44 | 45 | 48 | 49 | 50 | 51 | 52 | [[=con['status'] ]] 53 | 54 | [[=conn.c_host]][[=conn.c_port]][[=conn.c_username]][[=conn.c_password]]"ND""ND""ND""ND" 71 | 72 | 73 | 74 | 75 | [[=con['session'] ]] 76 | 77 | 80 | 81 | 82 | 83 | 84 | [[=con['session'] ]] 85 | 86 | [[=con['starts'] ]][[=con['stops'] ]] 92 | 94 | 95 | 96 | 98 | 99 | 100 | 102 | 103 | 104 | 106 | 107 | 108 | 110 | 111 | 112 |
117 |
118 |
119 |
120 | -------------------------------------------------------------------------------- /templates/smppc_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
Stats for Connector : [[=con]]
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | [[for item in items:]] 24 | 25 | 26 | 27 | 28 | [[pass]] 29 | 30 |
ItemValue
[[=item[0] ]][[=item[1] ]]
31 |
32 | -------------------------------------------------------------------------------- /templates/smppcs_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
SMPP Connectors Stats
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | [[for con in cons:]] 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | [[pass]] 40 | 41 |
Connector IDConnected atBound atDisconnected atSubmitsDeliversQOS ErrorsOther ErrorsOptions
[[=con['cid'] ]][[=con['ca'] ]][[=con['ba'] ]][[=con['da'] ]][[=con['sm'] ]][[=con['dl'] ]][[=con['qos'] ]][[=con['other'] ]] 37 |
42 |
43 | -------------------------------------------------------------------------------- /templates/smppsapi_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
SMPP Server Stats
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | [[for item in items:]] 25 | 26 | 27 | 28 | 29 | [[pass]] 30 | 31 |
ItemValue
[[=item[0] ]][[=item[1] ]]
32 |
33 | -------------------------------------------------------------------------------- /templates/stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 |
4 |
Statistics
5 |
6 |
7 |
8 | 19 | 30 | 40 | 51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /templates/superadmin_index.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 |
4 | 39 | 71 | -------------------------------------------------------------------------------- /templates/user_creds.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 |
3 |
4 |
5 |
Credentials for user [[=usr]]
6 | [[=form]] 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /templates/user_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
User Stats
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | [[for usr in users:]] 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | [[pass]] 37 | 38 |
UserIDSMPP BindsSMPP L.AHTTP RequestsHTTP L.AOptions
[[=usr['uid'] ]][[=usr['smpp_bc'] ]][[=usr['smpp_la'] ]][[=usr['http_rc'] ]][[=usr['http_la'] ]] 34 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /templates/users_list.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 3 |
4 | 5 | 8 | 9 |
10 |
11 |
12 | [[=form]] 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | [[for con in users:]] 25 | 26 | [[if con['uid'][0] == '!':]] 27 | 35 | [[else:]] 36 | 44 | [[pass]] 45 | [[if con['gid'][0] == '!':]] 46 | 54 | [[else:]] 55 | 63 | [[pass]] 64 | 65 | [[if con['balanceh'][0] == 'N':]] 66 | 74 | [[else:]] 75 | 78 | [[pass]] 79 | [[if con['balances'][0] == 'N':]] 80 | 88 | [[else:]] 89 | 92 | [[pass]] 93 | [[if con['mt_http'][0] == 'N':]] 94 | 102 | [[else:]] 103 | 106 | [[pass]] 107 | [[if con['mt_smpp'][0] == 'N':]] 108 | 116 | [[else:]] 117 | 120 | [[pass]] 121 | 140 | 141 | [[pass]] 142 | 143 |
User IDGroup IDUser NameHTTP Bal.SMPP Bal.HTTP ThruputSMPP ThruputActions
28 | 29 | 30 | 31 | 32 | [[=con['uid'] ]] 33 | 34 | 37 | 38 | 39 | 40 | 41 | [[=con['uid'] ]] 42 | 43 | 47 | 48 | 49 | 50 | 51 | [[=con['gid'] ]] 52 | 53 | 56 | 57 | 58 | 59 | 60 | [[=con['gid'] ]] 61 | 62 | [[=con['username'] ]] 67 | 68 | 69 | 70 | 71 | ND 72 | 73 | 76 | [[=con['balanceh'] ]] 77 | 81 | 82 | 83 | 84 | 85 | ND 86 | 87 | 90 | [[=con['balances'] ]] 91 | 95 | 96 | 97 | 98 | 99 | ND 100 | 101 | 104 | [[=con['mt_http'] ]] 105 | 109 | 110 | 111 | 112 | 113 | ND 114 | 115 | 118 | [[=con['mt_smpp'] ]] 119 | 122 | 123 | 125 | 126 | 127 | 129 | 130 | 131 | 133 | 134 | 135 | 137 | 138 | 139 |
144 |
145 |
146 | -------------------------------------------------------------------------------- /templates/users_stats.html: -------------------------------------------------------------------------------- 1 | [[extend 'layout.html']] 2 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
Stats for User : [[=usr]]
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | [[for item in items:]] 24 | 25 | 26 | 27 | 28 | 29 | [[pass]] 30 | 31 |
ItemTypeValue
[[=item[0] ]][[=item[1] ]][[=item[2] ]]
32 |
33 | 34 | -------------------------------------------------------------------------------- /translations/it.json: -------------------------------------------------------------------------------- 1 | {"Hello World from {name}": {"0": "Salve Mondo da {name}"}, "thing": {"0": "cosa", "1": "cose"}} 2 | -------------------------------------------------------------------------------- /user_manager.py: -------------------------------------------------------------------------------- 1 | 2 | from py4web import action, request, abort, redirect, URL 3 | from yatl.helpers import A 4 | from .common import db, session, Field, T, cache, auth, logger, authenticated, unauthenticated, flash 5 | from py4web.utils.form import Form, FormStyleBulma 6 | from pydal.validators import * 7 | from .utils import cols_split 8 | 9 | from . common import jasmin 10 | 11 | @action('user_manager_index') 12 | @action.uses(db, session, 'generic.html') 13 | def index(): 14 | return dict(msg="Hello form User Manager Index") 15 | 16 | def list_groups(): 17 | groups = [] 18 | rows=(jasmin.list_it('groups')) 19 | if not rows: 20 | return groups 21 | tt = cols_split(rows[2:-2]) 22 | group=dict(gid='') 23 | for t in tt: 24 | group=dict(gid = '') 25 | 26 | group.update( 27 | gid=t[0][1:], 28 | ) 29 | groups.append(group) 30 | query = (db.j_group.name == group['gid']) 31 | cc = db(query).select().first() 32 | if not cc: 33 | if not group['gid'][0] == '!': # this is the result of disabling or enabling a group 34 | db.j_group.insert(name=group['gid']) 35 | 36 | return groups 37 | 38 | def remove_group(group): 39 | gr = group 40 | t = jasmin.users(['remove_group',gr]) 41 | flash.set("Removed Group %s" % gr) 42 | query = db.j_group.name == gr 43 | db(query).delete() 44 | redirect(URL('manage_groups')) 45 | 46 | 47 | def disable_group(group): 48 | if not group: 49 | return ('You need to select a group') 50 | if group[0] == '!': 51 | return('Group %s is already disabled' % group) 52 | else: 53 | t = jasmin.users(['disable_group',group]) 54 | return t 55 | 56 | def enable_group(group): 57 | if not group: 58 | return ('You need to select a group') 59 | if group[0] != '!': 60 | return('Group %s is already enabled' % group) 61 | else: 62 | t=jasmin.users(['enable_group',group[1:]]) 63 | return t 64 | 65 | @action('manage_groups', method=['GET', 'POST']) 66 | @action('manage_groups//') 67 | @action.uses(db, session, auth, flash, 'groups_list.html') 68 | def manage_groups(group=None, action=None): 69 | groups=[] 70 | if action == 'enable': 71 | ret = enable_group(group) 72 | if ret: 73 | flash.set(ret) 74 | else: 75 | flash.set('Successfully enabled group %s' % group) 76 | elif action == 'disable': 77 | ret = disable_group(group) 78 | if ret: 79 | flash.set(ret) 80 | else: 81 | flash.set('Successfully disabled group %s' % group) 82 | elif action == 'remove': 83 | ret=remove_group(group) 84 | if ret: 85 | flash.set(ret) 86 | else: 87 | flash.set('Removed group %s' % group) 88 | 89 | form=Form(db.j_group, dbio=False, formstyle=FormStyleBulma) 90 | if form.accepted: 91 | group=jasmin.users(['create_group', form.vars['name']]) 92 | if group: 93 | db.j_group.insert(**form.vars) 94 | groups=list_groups() 95 | return dict(form=form, groups=groups) 96 | 97 | def disable_user(user): 98 | if not user: 99 | return ('You need to select a user') 100 | if user[0] == '!': 101 | return('User %s is already disabled' % user) 102 | else: 103 | t = jasmin.users(['disable_user',user]) 104 | return t 105 | 106 | def enable_user(user): 107 | if not user: 108 | return ('You need to select a user') 109 | if user[0] != '!': 110 | return('User %s is already enabled' % user) 111 | else: 112 | t=jasmin.users(['enable_user',user[1:]]) 113 | return t 114 | 115 | 116 | def remove_user(user): 117 | t = jasmin.users(['remove_user', user] ) 118 | flash.set('User %s removed' % user) 119 | query = db.j_user.j_uid == user 120 | db(query).delete() 121 | query = db.j_user_cred.juser == user 122 | db(query).delete() 123 | redirect(URL('manage_users')) 124 | 125 | @action('user_credentials/', method=['GET', 'POST']) 126 | @action.uses(db, session, auth, flash, 'record_content.html') 127 | def user_cred(user=None): 128 | usr = user 129 | title= 'Credentials for user %s ' % user 130 | query = db.j_user_cred.juser == usr 131 | juser = db(query).select().first() 132 | if juser: 133 | db.j_user_cred.juser.readable = db.j_user_cred.juser.writable = False 134 | db.j_user_cred.juser.default = usr 135 | db.j_user_cred.id.readable = db.j_user_cred.id.writable = False 136 | 137 | form = Form(db.j_user_cred, juser.id, deletable=False, formstyle=FormStyleBulma) 138 | flash.set('Please refer to the Jamsin user manual when updating values') 139 | if form.accepted: 140 | # need to update Jasmin with new creds 141 | juser=usr 142 | default_src_addr=form.vars.default_src_addr 143 | quota_http_throughput=form.vars.quota_http_throughput 144 | quota_balance=form.vars.quota_balance 145 | quota_smpps_throughput=form.vars.quota_smpps_throughput 146 | quota_sms_count=form.vars.quota_sms_count 147 | quota_early_percent=form.vars.quota_early_percent 148 | value_priority=form.vars.value_priority 149 | value_content=form.vars.value_content 150 | value_src_addr=form.vars.value_src_addr 151 | value_dst_addr=form.vars.value_dst_addr 152 | value_validity_period=form.vars.value_validity_period 153 | author_http_send=form.vars.author_http_send 154 | author_http_dlr_method=form.vars.author_http_dlr_method 155 | author_http_balance=form.vars.author_http_balance 156 | author_smpps_send=form.vars.author_smpps_send 157 | author_priority=form.vars.author_priority 158 | author_http_long_content=form.vars.author_http_long_content 159 | author_src_addr=form.vars.author_src_addr 160 | author_dlr_level=form.vars.author_dlr_level 161 | author_http_rate=form.vars.author_http_rate 162 | author_validity_period=form.vars.author_validity_period 163 | author_http_bulk=form.vars.author_http_bulk 164 | users=jasmin.users(['update', juser, \ 165 | default_src_addr,quota_http_throughput,quota_balance,quota_smpps_throughput,\ 166 | quota_sms_count,quota_early_percent,value_priority,value_content,value_src_addr,\ 167 | value_dst_addr,value_validity_period,author_http_send,author_http_dlr_method,\ 168 | author_http_balance,author_smpps_send,author_priority,author_http_long_content,author_src_addr,\ 169 | author_dlr_level,author_http_rate,author_validity_period,author_http_bulk]) 170 | if not users: 171 | flash.set('User credentials updated') 172 | else: 173 | flash.set('Unable to update credentials') 174 | redirect(URL('manage_users')) 175 | else: 176 | flash.set("User '%s' could not be found" % (usr)) 177 | return dict(usr=usr, content='', caller='../manage_users') 178 | return dict(content=form, title=title, caller='../manage_users') 179 | 180 | 181 | return dict(usr=usr, form=form) 182 | 183 | 184 | 185 | @action('manage_users', method=['GET', 'POST']) 186 | @action('manage_users//', method=['GET', 'POST']) 187 | @action.uses(db, session, auth, flash, 'users_list.html') 188 | def manage_users(user=None, action=None): 189 | j_gid = '' 190 | if action == 'enable': 191 | ret = enable_user(user) 192 | if ret: 193 | flash.set(ret) 194 | else: 195 | flash.set('Successfully enabled user %s' % user) 196 | elif action == 'disable': 197 | ret = disable_user(user) 198 | if ret: 199 | flash.set(ret) 200 | else: 201 | flash.set('Successfully disabled user %s' % user) 202 | elif action == 'remove': 203 | ret=remove_user(user) 204 | if ret: 205 | flash.set(ret) 206 | else: 207 | flash.set('Removed user %s ' % user) 208 | form = Form(db.j_user,dbio=False, formstyle=FormStyleBulma) 209 | '''[ 210 | Field('username', 'string', length=10, comment="Jasmin User Name for HTTP and SMPP connecting. Must not include any spaces and can not be longer than 10 characters"), 211 | Field('password', 'string', length=10, comment='Jasmin Password for HTTP and SMPP connecting. Must not include any spaces and can not be longer than 10 characters'), 212 | Field('j_uid','string',label='Jasmin UID',length=12, comment='Jasmin UID cannot be longer than 12 characters and reccoment all in UPPER case. No spaces allowed. Suggest USER_1 etc.'), 213 | Field('j_group','reference j_group',label = 'Jasim GID', comment='Select a Group', requires=IS_IN_DB(db,'j_group.id','j_group.name'))], 214 | ]''' 215 | if form.accepted: 216 | j_gid=db.j_group[form.vars['j_group']].name 217 | ret=jasmin.users(['create_user',form.vars['j_uid'],form.vars['username'], form.vars['password'], j_gid]) 218 | if not ret: 219 | flash.set('Added user %s' % form.vars['username']) 220 | ret = db.j_user.update_or_insert(db.j_user.j_uid == form.vars['j_uid'], 221 | username = form.vars['username'], 222 | password = form.vars['password'], 223 | j_uid = form.vars['j_uid'], 224 | j_group = form.vars['j_group']) 225 | if ret: #means we have inserted new one 226 | cred = db.j_user_cred.insert(juser = form.vars['j_uid']) 227 | else: 228 | flash.set(ret) 229 | users=list_users() 230 | return dict(form=form, users=users) 231 | 232 | def list_users(): 233 | users = [] 234 | rows=(jasmin.list_it('users')) 235 | if not rows: 236 | return users 237 | tt = cols_split(rows[2:-2]) 238 | user={} 239 | w1='' 240 | w2='' 241 | for t in tt: 242 | user={} 243 | if len(t) == 6: 244 | l = len(t[5]) 245 | i=0 246 | while i < l: 247 | if t[5][i] == '/': 248 | w1=t[5][0:i] 249 | i = i + 1 250 | else: 251 | i = i + 1 252 | j=len(w1) 253 | j = j+1 254 | w2=t[5][j:] 255 | user.update( 256 | uid=t[0][1:], 257 | gid=t[1], 258 | username=t[2], 259 | balanceh=t[3], 260 | balances=t[4], 261 | mt_http=w1, 262 | mt_smpp=w2, 263 | ) 264 | else: 265 | l = len(t[7]) 266 | i=0 267 | while i < l: 268 | if t[7][i] == '/': 269 | w1=t[7][0:i] 270 | i = i + 1 271 | else: 272 | i = i + 1 273 | j=len(w1) 274 | j = j+1 275 | w2=t[7][j:] 276 | user.update( 277 | uid=t[0][1:], 278 | gid=t[1], 279 | username=t[2], 280 | balanceh=t[3]+" "+t[4], 281 | balances=t[5]+" "+t[6], 282 | mt_http=w1, 283 | mt_smpp=w2, 284 | 285 | ) 286 | users.append(user) 287 | query = (db.j_user.username == user['username']) 288 | cc=db(query).select().first() 289 | #if not cc: 290 | # if user['gid'][0] == '!': 291 | # aa = user['gid'][1:] 292 | # group = get_gid(user['gid'][1:]) 293 | # else: 294 | # group = get_gid(user['gid']) 295 | # u_id = db.j_user.insert(gw = session.g_id, j_uid = user['uid'], j_group=group, username = user['username']) 296 | # cred = db.j_user_cred.insert(gw = session.g_id, juser = user['uid'], quota_http_throughput = user['mt_http'], quota_balance=user['balanceh'],quota_smpps_throughput=user['mt_smpp'], quota_sms_count = user['balances']) 297 | return users 298 | 299 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | def cols_split(lines): 2 | #"split columns into lists, skipping blank and non-data lines" 3 | parsed = [] 4 | for line in lines: 5 | raw_split = line.split() 6 | fields = [s for s in raw_split if (s and raw_split[0][0] == '#')] 7 | parsed.append(fields) 8 | return parsed 9 | --------------------------------------------------------------------------------