From 815eb272a970ffeeb878a5b9a47510b7a5f6125c Mon Sep 17 00:00:00 2001 From: Vitor Nunes Date: Fri, 29 Jan 2016 17:39:39 -0300 Subject: [PATCH 01/22] update on README.md update on README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 8068dce..d6dbfbc 100644 --- a/README.md +++ b/README.md @@ -167,3 +167,8 @@ print p.addresses.count() # 1 print p.addresses[0] #
print p.addresses.filter_by(email='foo@bar.com').count() # 1 + +## Upstart + +To have Celcombiler as service you have to modify the file celcombiller.conf and put it in /etc/init + From 21adefa50cc8e38646012fb213195309995738c2 Mon Sep 17 00:00:00 2001 From: Vitor Nunes Date: Fri, 29 Jan 2016 17:41:44 -0300 Subject: [PATCH 02/22] create celcombiller.conf --- celcombiller.conf | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 celcombiller.conf diff --git a/celcombiller.conf b/celcombiller.conf new file mode 100644 index 0000000..4c2407f --- /dev/null +++ b/celcombiller.conf @@ -0,0 +1,21 @@ +# CELCOMBiller +# +# This service runs celcombiller app.py from the point the system is +# started until it is shut down again. +# on ubuntu you have to put this file in /etc/init +# +start on startup + +respawn + +script + /path_to_venv/venv/bin/python /path_to_celcombiller/celcombiller/app.py +end script + +pre-start script + + touch /path_to_db/alph.db + + chmod 666 /path_to_db/alph.db + +end script From bcda9050b73d6ab3d6b4c7c011aae0aebe06eef0 Mon Sep 17 00:00:00 2001 From: vitor Date: Fri, 5 Feb 2016 16:15:52 -0300 Subject: [PATCH 03/22] adjustments on celcombiller.conf file --- celcombiller.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/celcombiller.conf b/celcombiller.conf index 4c2407f..493d34c 100644 --- a/celcombiller.conf +++ b/celcombiller.conf @@ -4,7 +4,7 @@ # started until it is shut down again. # on ubuntu you have to put this file in /etc/init # -start on startup +start on runlevel [2345] respawn From 44ecd0b383c10fc076098f3d22bb379ab8a0fc8f Mon Sep 17 00:00:00 2001 From: vitor Date: Mon, 22 Feb 2016 10:10:59 -0300 Subject: [PATCH 04/22] Small changes in the README --- README.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/README.md b/README.md index d6dbfbc..d4b0258 100644 --- a/README.md +++ b/README.md @@ -145,30 +145,7 @@ celcombiller_reducer ``` - - -You dont need to write a constructor, you can either treat the addresses property on a Person instance as a list: - -a = Address(email='foo@bar.com') -p = Person(name='foo') -p.addresses.append(a) - -Or you can pass a list of addresses to the Person constructor - -a = Address(email='foo@bar.com') -p = Person(name='foo', addresses=[a]) - -In either case you can then access the addresses on your Person instance like so: - -db.session.add(p) -db.session.add(a) -db.session.commit() -print p.addresses.count() # 1 -print p.addresses[0] #
-print p.addresses.filter_by(email='foo@bar.com').count() # 1 - - ## Upstart -To have Celcombiler as service you have to modify the file celcombiller.conf and put it in /etc/init +To have Celcombiler running as a service you have to modify the file celcombiller.conf and put it in your /etc/init directory. From a4727685f75e12730d1ea74a124ffae87f4d645f Mon Sep 17 00:00:00 2001 From: laps11 Date: Mon, 29 Feb 2016 10:05:47 -0300 Subject: [PATCH 05/22] The admin user credentials were moved from celcom_reduncer to config --- celcombiller_reducer.py | 5 +---- config.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/celcombiller_reducer.py b/celcombiller_reducer.py index 48638eb..46fea4e 100644 --- a/celcombiller_reducer.py +++ b/celcombiller_reducer.py @@ -10,12 +10,10 @@ import asterisk.agi #from asterisk.agi import AGIAppError from models import User - +from config import adm_user,adm_pssw import requests import json -adm_user = 'admin' -adm_pssw = 'adm123' agi = asterisk.agi.AGI() @@ -50,7 +48,6 @@ if r.ok == False: pass - print from_user.id_ payload = '{"signal":"-", "type_":"decrease", "value": "'+ str(billsec) +\ '", "userId":'+ str(from_user.id_) +'}' diff --git a/config.py b/config.py index 86ddca0..ece8cc5 100644 --- a/config.py +++ b/config.py @@ -3,10 +3,15 @@ import flask.ext.restless from flask.ext.login import LoginManager +#adm login and password +adm_user = 'admin' +adm_pssw = 'adm123' + + # Create the Flask application and the Flask-SQLAlchemy object. app = flask.Flask(__name__) app.config['DEBUG'] = True -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/alph.db' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/alph.db' db = flask.ext.sqlalchemy.SQLAlchemy(app) From a8ec73d3afe7b9df72c75919b797f29a5347a4fa Mon Sep 17 00:00:00 2001 From: laps11 Date: Wed, 16 Mar 2016 12:41:46 -0300 Subject: [PATCH 06/22] Now we can pass the imsi as id to change the ballace. It has a bug where the answer of the request has id as null but the database shows the right id --- app.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/app.py b/app.py index 2308a3b..7b56e36 100644 --- a/app.py +++ b/app.py @@ -135,11 +135,20 @@ def add_user_balance(*args, **kargs): global buffer_usersId data = request.data request_body = json.loads(data) - request_body['userId'] - x = Ballance.query.order_by(Ballance.id_.desc()).first() - x.usersId = request_body['userId'] - db.session.add(x) - db.session.commit() + # check if we are passing the user id or the imsi in the userId field + if request_body['userId'] < 1e13: + x = Ballance.query.order_by(Ballance.id_.desc()).first() + x.usersId = request_body['userId'] + print x,"aqui" + db.session.add(x) + db.session.commit() + else: + x = Ballance.query.order_by(Ballance.id_.desc()).first() + x.usersId = User.query.filter_by( imsi=request_body['userId'] ).first().id_ + print x,"2" + db.session.add(x) + db.session.commit() + @app.route('/logout') def logout(): @@ -165,9 +174,9 @@ def auth(*args, **kargs): """ Required API request to be authenticated """ - if not current_user.is_authenticated(): - raise ProcessingException(description='Not authenticated', code=401) - + #if not current_user.is_authenticated(): + # raise ProcessingException(description='Not authenticated', code=401) + pass def preprocessor_check_adm(*args, **kargs): if not current_user.is_admin(): @@ -204,8 +213,8 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): User, preprocessors={ 'POST': [ - auth, - preprocessor_check_adm + auth + # preprocessor_check_adm ], 'GET_MANY': [ auth, @@ -293,7 +302,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): Ballance, preprocessors={ 'POST': [ - preprocessor_check_adm, + # preprocessor_check_adm, date_now ], 'GET_MANY': [ From 9cdb7fde1d8429cca4368df355754478247bdcad Mon Sep 17 00:00:00 2001 From: laps11 Date: Wed, 16 Mar 2016 12:41:46 -0300 Subject: [PATCH 07/22] Now we can pass the imsi as id to change the ballace. It has a bug where the answer of the request has id as null but the database shows the right id --- app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app.py b/app.py index 7b56e36..69af7c9 100644 --- a/app.py +++ b/app.py @@ -139,13 +139,11 @@ def add_user_balance(*args, **kargs): if request_body['userId'] < 1e13: x = Ballance.query.order_by(Ballance.id_.desc()).first() x.usersId = request_body['userId'] - print x,"aqui" db.session.add(x) db.session.commit() else: x = Ballance.query.order_by(Ballance.id_.desc()).first() x.usersId = User.query.filter_by( imsi=request_body['userId'] ).first().id_ - print x,"2" db.session.add(x) db.session.commit() From d86d80db89dfc1769ff14dd35c7278324039d8c7 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Tue, 10 May 2016 11:03:09 -0300 Subject: [PATCH 08/22] feat(routes): The route check_balance was added The route check_balance was added. It is intended to be used by the OpenBTS to check if the user has credit. --- app.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 69af7c9..6523633 100644 --- a/app.py +++ b/app.py @@ -21,7 +21,6 @@ def index(): """ return render_template('index.html') - @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': @@ -38,6 +37,17 @@ def login(): json_with_names = check_time() return "Hello, cross-origin-world!" +@app.route('/check_balance', methods=['GET','POST']) +def check_balance(*args, **kargs): + data = request.data + request_body = json.loads(data) + ## Is it a better way to handle the exception when the user is not found ? + try: + x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser + return str(x) + except AttributeError: + return "none" + def check_time(): if not current_user.is_admin(): return False @@ -139,13 +149,11 @@ def add_user_balance(*args, **kargs): if request_body['userId'] < 1e13: x = Ballance.query.order_by(Ballance.id_.desc()).first() x.usersId = request_body['userId'] - db.session.add(x) - db.session.commit() else: x = Ballance.query.order_by(Ballance.id_.desc()).first() x.usersId = User.query.filter_by( imsi=request_body['userId'] ).first().id_ - db.session.add(x) - db.session.commit() + db.session.add(x) + db.session.commit() @app.route('/logout') From fd869d56292ad801c5f9ff2e48088b8e9fd84405 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Thu, 12 May 2016 14:26:13 -0300 Subject: [PATCH 09/22] feat(User,Balance) more user's informations and data balance. Now the user has to insert his adress, real name and cpf. The user's balance is now splited in voice and data balance. These informations are in the User table now. --- README.md | 16 ++++++---- adduser.py | 7 +++-- app.py | 17 +++++++++-- models.py | 86 +++++++++++++++++++++++++++++++++++++----------------- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d4b0258..c0db2b0 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,14 @@ Each SIP user must be inserted in the database of users with a balance. The pyth from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from config import db -from models import User +from models import User, Ballance, Groups -admin = User('admin', 'adm123', '999999999', '9999', True) -guest = User('guest', '123123', '999999999', '0000', False) +admin = User(True,'administrator', 'nowhere', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0') +guest = User(True,'guest', 'nowhere', '1','guest', '123123', '999999998','999999999999998', '0' ,'0') db.session.add(admin) db.session.add(guest) + db.session.commit() ``` @@ -50,7 +51,10 @@ curl -c cookiefile -d "username=admin&password=adm123" -X POST -s http://localho now to add user: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900" "admin":'false'}' -s http://localhost:5000/api/users +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"testphone","password":"yourpassword","clid":"87654321","imsi":"724059100470553", "admin":'false',"name":"testphone","adress":"lasse","cpf":"001","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users + + +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","adress":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users ``` the balance came by another table, so we want add balance to user we need run: @@ -58,7 +62,7 @@ the balance came by another table, so we want add balance to user we need run: add balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1,"balance":"voice"}' -s http://localhost:5000/api/balance #note that userId need some user id, in that case we use 1 ``` @@ -66,7 +70,7 @@ curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+" remove balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1, "balance":"voice"}' -s http://localhost:5000/api/balance ``` update user diff --git a/adduser.py b/adduser.py index f6341ac..ab3e081 100644 --- a/adduser.py +++ b/adduser.py @@ -3,9 +3,12 @@ from config import db from models import User, Ballance -admin = User('admin', 'adm123', '999999999', True,'999999999999999') -guest = User('guest', '123123', '999999998', False,'000000000000000') + + +admin = User(True,'administrator', 'nowhere', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0') +guest = User(True,'guest', 'nowhere', '1','guest', '123123', '999999998','999999999999998', '0' ,'0') db.session.add(admin) db.session.add(guest) + db.session.commit() diff --git a/app.py b/app.py index 6523633..bda42e9 100644 --- a/app.py +++ b/app.py @@ -43,7 +43,18 @@ def check_balance(*args, **kargs): request_body = json.loads(data) ## Is it a better way to handle the exception when the user is not found ? try: - x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser + x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser() + return str(x) + except AttributeError: + return "none" + +@app.route('/check_data_balance', methods=['GET','POST']) +def check_daata_balance(*args, **kargs): + data = request.data + request_body = json.loads(data) + ## Is it a better way to handle the exception when the user is not found ? + try: + x = User.query.filter_by(imsi =request_body['imsi']).first().DataBallanceUser() return str(x) except AttributeError: return "none" @@ -139,7 +150,7 @@ def date_now(*args, **kargs): kargs['data']['date'] = unicode(datetime.now()) global buffer_usersId buffer_usersId = kargs['data']['userId'] - del kargs['data']['userId'] + #del kargs['data']['userId'] def add_user_balance(*args, **kargs): global buffer_usersId @@ -324,7 +335,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): }, postprocessors={ 'POST': [add_user_balance] - }, + }, methods=['POST', 'GET', 'PATCH', 'DELETE'], results_per_page=100, ) diff --git a/models.py b/models.py index 0806b61..cf7df45 100644 --- a/models.py +++ b/models.py @@ -25,13 +25,21 @@ class User(db.Model): """ __tablename__ = 'users' - id_ = db.Column(db.Integer, primary_key=True) - username = db.Column(db.Unicode, unique=True) - password = db.Column(db.Integer) + + + id_ = db.Column(db.Integer, primary_key=True, nullable=False) + admin = db.Column(db.Boolean, nullable=False) + name = db.Column(db.Unicode, nullable=False) + adress = db.Column(db.Unicode) + cpf = db.Column(db.Integer, nullable=False) + username = db.Column(db.Unicode, nullable=False, unique=True) + password = db.Column(db.Unicode, nullable=False) clid = db.Column(db.String(9), nullable=False, unique=True) - admin = db.Column(db.Boolean) + imsi = db.Column(db.Integer, nullable=False, unique=True) + voice_balance = db.Column(db.Integer, nullable=False) + data_balance = db.Column(db.Integer, nullable=False) + tunel = db.relationship('Groups', secondary=tunel_table) - imsi = db.Column(db.Integer, unique=True) def is_admin(self): return self.admin @@ -48,35 +56,35 @@ def is_anonymous(self): def get_id(self): return unicode(self.id_) - @hybrid_property def BallanceUser(self): - # TODO: Maybe it should use object_session - userBalanceNeg = Ballance.query.filter_by(usersId=self.id_).filter_by(signal=unicode('-')) - userBalancePos = Ballance.query.filter_by(usersId=self.id_).filter_by(signal=unicode('+')) - countNeg = 0 - countPos = 0 - for x in userBalanceNeg: - countNeg = countNeg + x.value - for x in userBalancePos: - countPos = countPos + x.value - return countPos - countNeg + return self.voice_balance + + def DataBallanceUser(self): + return self.data_balance @hybrid_property def BallanceUserHistoric(self): # TODO: Maybe it should use object_session - balances = Ballance.query.order_by(Ballance.id_.desc()).filter_by(usersId=self.id_).limit(10) + balances = Ballance.query.order_by(Ballance.id_.desc()).filter_by(userId=self.id_).limit(10) historic_list = [] for y in balances: historic_list.append(row2dict(y)) return historic_list - def __init__(self , username ,password, clid,admin, imsi ): + def __init__(self , admin, name, adress, cpf, username, password, clid, imsi, voice_balance, data_balance ): + self.admin = admin + self.name = name + self.adress = adress + self.cpf = cpf self.username = username self.password = password - self.clid = clid - self.admin = admin - self.imsi = imsi + self.clid = clid + self.imsi = imsi + self.voice_balance = voice_balance + self.data_balance = data_balance + + def __repr__(self): return '' % (self.username) @@ -165,27 +173,53 @@ def __init__(self, date): def __repr__(self): return 'DATES %r' % (self.id_) + + class Ballance(db.Model): __tablename__ = 'balance' id_ = db.Column(db.Integer, primary_key=True) - usersId = db.Column(db.Integer, db.ForeignKey('users.id_')) + userId = db.Column(db.Integer, db.ForeignKey('users.id_')) date = db.Column(db.DateTime()) type_ = db.Column(ENUM('increase', 'decrease')) value = db.Column(db.Integer) signal = db.Column(db.String(1)) + balance = db.Column(ENUM('voice', 'data')) - def __init__(self, date, type_, value, signal, usersId=None): - self.date = date + + def __init__(self, type_, value,balance, signal, userId,date=None): + self.date = datetime.now() self.type_ = type_ self.value = value self.signal = signal - if usersId is not None: - self.usersId = usersId + self.balance = balance + self.userId = userId + + if balance == 'voice': + if type_ == 'increase': + user = db.session.query(User).filter_by(id_=userId).first() + user.voice_balance = user.data_balance + int(value) + elif type_ == 'decrease': + user = db.session.query(User).filter_by(id_=userId).first() + user.voice_balance = user.data_balance - int(value) + db.session.commit() + + elif balance == 'data': + if type_ == 'increase': + user = db.session.query(User).filter_by(id_=userId).first() + user.data_balance = user.data_balance + int(value) + elif type_ == 'decrease': + user = db.session.query(User).filter_by(id_=userId).first() + user.data_balance = user.data_balance - int(value) + db.session.commit() + def __repr__(self): return 'balance %r' % (self.id_) + + + # Create the database tables. db.create_all() From d11bd19c33f06ff4917f6786babb39a330b68bd6 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Thu, 12 May 2016 14:26:13 -0300 Subject: [PATCH 10/22] feat(User,Balance) more user's informations and data balance. Now the user has to insert his adress, real name and cpf. The user's balance is now splited in voice and data balance. These informations are in the User table now. --- README.md | 14 +++++---- adduser.py | 7 +++-- app.py | 17 +++++++++-- models.py | 86 +++++++++++++++++++++++++++++++++++++----------------- 4 files changed, 87 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d4b0258..f29df58 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,14 @@ Each SIP user must be inserted in the database of users with a balance. The pyth from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from config import db -from models import User +from models import User, Ballance -admin = User('admin', 'adm123', '999999999', '9999', True) -guest = User('guest', '123123', '999999999', '0000', False) +admin = User(True,'administrator', 'nowhere', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0') +guest = User(True,'guest', 'nowhere', '1','guest', '123123', '999999998','999999999999998', '0' ,'0') db.session.add(admin) db.session.add(guest) + db.session.commit() ``` @@ -50,7 +51,8 @@ curl -c cookiefile -d "username=admin&password=adm123" -X POST -s http://localho now to add user: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900" "admin":'false'}' -s http://localhost:5000/api/users + +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","adress":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users ``` the balance came by another table, so we want add balance to user we need run: @@ -58,7 +60,7 @@ the balance came by another table, so we want add balance to user we need run: add balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1,"balance":"voice"}' -s http://localhost:5000/api/balance #note that userId need some user id, in that case we use 1 ``` @@ -66,7 +68,7 @@ curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+" remove balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1, "balance":"voice"}' -s http://localhost:5000/api/balance ``` update user diff --git a/adduser.py b/adduser.py index f6341ac..ab3e081 100644 --- a/adduser.py +++ b/adduser.py @@ -3,9 +3,12 @@ from config import db from models import User, Ballance -admin = User('admin', 'adm123', '999999999', True,'999999999999999') -guest = User('guest', '123123', '999999998', False,'000000000000000') + + +admin = User(True,'administrator', 'nowhere', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0') +guest = User(True,'guest', 'nowhere', '1','guest', '123123', '999999998','999999999999998', '0' ,'0') db.session.add(admin) db.session.add(guest) + db.session.commit() diff --git a/app.py b/app.py index 6523633..bda42e9 100644 --- a/app.py +++ b/app.py @@ -43,7 +43,18 @@ def check_balance(*args, **kargs): request_body = json.loads(data) ## Is it a better way to handle the exception when the user is not found ? try: - x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser + x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser() + return str(x) + except AttributeError: + return "none" + +@app.route('/check_data_balance', methods=['GET','POST']) +def check_daata_balance(*args, **kargs): + data = request.data + request_body = json.loads(data) + ## Is it a better way to handle the exception when the user is not found ? + try: + x = User.query.filter_by(imsi =request_body['imsi']).first().DataBallanceUser() return str(x) except AttributeError: return "none" @@ -139,7 +150,7 @@ def date_now(*args, **kargs): kargs['data']['date'] = unicode(datetime.now()) global buffer_usersId buffer_usersId = kargs['data']['userId'] - del kargs['data']['userId'] + #del kargs['data']['userId'] def add_user_balance(*args, **kargs): global buffer_usersId @@ -324,7 +335,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): }, postprocessors={ 'POST': [add_user_balance] - }, + }, methods=['POST', 'GET', 'PATCH', 'DELETE'], results_per_page=100, ) diff --git a/models.py b/models.py index 0806b61..cf7df45 100644 --- a/models.py +++ b/models.py @@ -25,13 +25,21 @@ class User(db.Model): """ __tablename__ = 'users' - id_ = db.Column(db.Integer, primary_key=True) - username = db.Column(db.Unicode, unique=True) - password = db.Column(db.Integer) + + + id_ = db.Column(db.Integer, primary_key=True, nullable=False) + admin = db.Column(db.Boolean, nullable=False) + name = db.Column(db.Unicode, nullable=False) + adress = db.Column(db.Unicode) + cpf = db.Column(db.Integer, nullable=False) + username = db.Column(db.Unicode, nullable=False, unique=True) + password = db.Column(db.Unicode, nullable=False) clid = db.Column(db.String(9), nullable=False, unique=True) - admin = db.Column(db.Boolean) + imsi = db.Column(db.Integer, nullable=False, unique=True) + voice_balance = db.Column(db.Integer, nullable=False) + data_balance = db.Column(db.Integer, nullable=False) + tunel = db.relationship('Groups', secondary=tunel_table) - imsi = db.Column(db.Integer, unique=True) def is_admin(self): return self.admin @@ -48,35 +56,35 @@ def is_anonymous(self): def get_id(self): return unicode(self.id_) - @hybrid_property def BallanceUser(self): - # TODO: Maybe it should use object_session - userBalanceNeg = Ballance.query.filter_by(usersId=self.id_).filter_by(signal=unicode('-')) - userBalancePos = Ballance.query.filter_by(usersId=self.id_).filter_by(signal=unicode('+')) - countNeg = 0 - countPos = 0 - for x in userBalanceNeg: - countNeg = countNeg + x.value - for x in userBalancePos: - countPos = countPos + x.value - return countPos - countNeg + return self.voice_balance + + def DataBallanceUser(self): + return self.data_balance @hybrid_property def BallanceUserHistoric(self): # TODO: Maybe it should use object_session - balances = Ballance.query.order_by(Ballance.id_.desc()).filter_by(usersId=self.id_).limit(10) + balances = Ballance.query.order_by(Ballance.id_.desc()).filter_by(userId=self.id_).limit(10) historic_list = [] for y in balances: historic_list.append(row2dict(y)) return historic_list - def __init__(self , username ,password, clid,admin, imsi ): + def __init__(self , admin, name, adress, cpf, username, password, clid, imsi, voice_balance, data_balance ): + self.admin = admin + self.name = name + self.adress = adress + self.cpf = cpf self.username = username self.password = password - self.clid = clid - self.admin = admin - self.imsi = imsi + self.clid = clid + self.imsi = imsi + self.voice_balance = voice_balance + self.data_balance = data_balance + + def __repr__(self): return '' % (self.username) @@ -165,27 +173,53 @@ def __init__(self, date): def __repr__(self): return 'DATES %r' % (self.id_) + + class Ballance(db.Model): __tablename__ = 'balance' id_ = db.Column(db.Integer, primary_key=True) - usersId = db.Column(db.Integer, db.ForeignKey('users.id_')) + userId = db.Column(db.Integer, db.ForeignKey('users.id_')) date = db.Column(db.DateTime()) type_ = db.Column(ENUM('increase', 'decrease')) value = db.Column(db.Integer) signal = db.Column(db.String(1)) + balance = db.Column(ENUM('voice', 'data')) - def __init__(self, date, type_, value, signal, usersId=None): - self.date = date + + def __init__(self, type_, value,balance, signal, userId,date=None): + self.date = datetime.now() self.type_ = type_ self.value = value self.signal = signal - if usersId is not None: - self.usersId = usersId + self.balance = balance + self.userId = userId + + if balance == 'voice': + if type_ == 'increase': + user = db.session.query(User).filter_by(id_=userId).first() + user.voice_balance = user.data_balance + int(value) + elif type_ == 'decrease': + user = db.session.query(User).filter_by(id_=userId).first() + user.voice_balance = user.data_balance - int(value) + db.session.commit() + + elif balance == 'data': + if type_ == 'increase': + user = db.session.query(User).filter_by(id_=userId).first() + user.data_balance = user.data_balance + int(value) + elif type_ == 'decrease': + user = db.session.query(User).filter_by(id_=userId).first() + user.data_balance = user.data_balance - int(value) + db.session.commit() + def __repr__(self): return 'balance %r' % (self.id_) + + + # Create the database tables. db.create_all() From 572ab345baf020a44998198b0deec8ae6d225209 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Mon, 23 May 2016 13:42:52 -0300 Subject: [PATCH 11/22] adapting to the new database schema --- README.md | 19 ++- app.py | 284 +++++++++++++++++++++++++--------------- celcombiller_caller.py | 2 +- celcombiller_reducer.py | 4 +- models.py | 263 ++++++++++++++++++------------------- 5 files changed, 321 insertions(+), 251 deletions(-) mode change 100644 => 100755 app.py diff --git a/README.md b/README.md index 8efd68f..b214d44 100644 --- a/README.md +++ b/README.md @@ -56,20 +56,25 @@ curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":" the balance came by another table, so we want add balance to user we need run: -add balance: +add/remove data balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1,"balance":"voice"}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"value": "1000", "userId":1}' -s http://localhost:5000/api/data_balance #note that userId need some user id, in that case we use 1 +#to remove balance the value must be negative ``` -remove balance: +add/remove voice balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"signal":"+", "type_":"increase", "value": "1000", "userId":1, "balance":"voice"}' -s http://localhost:5000/api/balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"value": "1000", "userId":1}' -s http://localhost:5000/api/voice_balance + +#note that userId need some user id, in that case we use 1 +#to remove balance the value must be negative ``` + update user ```bash @@ -82,18 +87,18 @@ remove user curl -X DELETE -s http://localhost:5000/api/users/yourusername -b cookiefile ``` -###GROUPS +###Schedule add group ```bash -curl -X POST -H "Content-Type: application/json" -d '{"name":"group_name","day":1, "month":1, "year":3000, "count":10, "users":[id_]}' -s http://localhost:5000/api/groups +curl -X POST -H "Content-Type: application/json" -d '{"name":"group_name","day":1, "month":1, "year":3000, "count":10}' -s http://localhost:5000/api/schedules ``` update group ```bash -curl -X PATCH -H "Content-Type: application/json" -d '{}' -s http://localost:5000/api/groups/group_name +curl -X PATCH -H "Content-Type: application/json" -d '{}' -s http://localost:5000/api/schedules/schedule ``` diff --git a/app.py b/app.py old mode 100644 new mode 100755 index bda42e9..96b90de --- a/app.py +++ b/app.py @@ -2,7 +2,8 @@ from flask import Flask, session, request, flash, url_for, redirect, \ render_template, abort from config import db, app, login_manager -from models import CDR, User, Groups, Dates, Ballance +from models import User, VoiceBalance, DataBalance, Schedules, ScheduleInput, \ + ScheduleUser from time import strftime from datetime import * from dateutil.rrule import * @@ -14,6 +15,7 @@ import json from openbts import to_openbts + @app.route('/') def index(): """ @@ -21,6 +23,8 @@ def index(): """ return render_template('index.html') + +# Login, if the user does not exist it returs a error page @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': @@ -33,90 +37,105 @@ def login(): if registered_user is None: flash('Username or Password is invalid', 'error') return render_template('ERROR.html') - login_user(registered_user) - json_with_names = check_time() - return "Hello, cross-origin-world!" + if login_user(registered_user): + return "Hello, cross-origin-world!" + else: + flash('Flask Login error', 'error') + return render_template('ERROR.html') + #json_with_names = check_time() + -@app.route('/check_balance', methods=['GET','POST']) -def check_balance(*args, **kargs): +# Returns the user data balance +@app.route('/check_data_balance', methods=['GET', 'POST']) +def check_data_balance(*args, **kargs): data = request.data request_body = json.loads(data) - ## Is it a better way to handle the exception when the user is not found ? + # Is it a better way to handle the exception when the user is not found ? try: - x = User.query.filter_by(imsi =request_body['imsi']).first().BallanceUser() + x = User.query.filter_by( + imsi=request_body['imsi']).first().DataBalance() return str(x) except AttributeError: return "none" -@app.route('/check_data_balance', methods=['GET','POST']) -def check_daata_balance(*args, **kargs): +# Returns the user voice balance + + +@app.route('/check_voice_balance', methods=['GET', 'POST']) +def check_voice_balance(*args, **kargs): data = request.data request_body = json.loads(data) - ## Is it a better way to handle the exception when the user is not found ? + # Is it a better way to handle the exception when the user is not found ? try: - x = User.query.filter_by(imsi =request_body['imsi']).first().DataBallanceUser() + x = User.query.filter_by( + imsi=request_body['imsi']).first().VoiceBalance() return str(x) except AttributeError: return "none" -def check_time(): - if not current_user.is_admin(): - return False - else: - json_need_update = [] - groups = Groups.query.all() - for group in groups: - boolean_time = (group.dates_to_update[0].date - datetime.now())\ - .total_seconds() - boolean_time = 1 if boolean_time < 0 else 0 - if boolean_time == 1: - json_need_update.append(group.name) - if json_need_update == []: - return None - else: - return json_need_update - -def already_has_group(data=None, **kargs): +# I dont know what this function does so I commented it. +# def check_time(): +# if not current_user.is_admin(): +# return False +# else: +# json_need_update = [] +# schedules = Schedules.query.all() +# for schedule in schedules: +# boolean_time = (schedule.dates_to_update[0].date - datetime.now())\ +# .total_seconds() +# boolean_time = 1 if boolean_time < 0 else 0 +# if boolean_time == 1: +# json_need_update.append(schedule.name) +# if json_need_update == []: +# return None +# else: +# return json_need_update + +# Check if the user has a schedule +def schedule_exists(data=None, **kargs): data = request.data request_body = json.loads(data) - group = Groups.query.\ + schedule = Schedules.query.\ filter_by(name=request_body['name']).first() - if group is not None: - raise ProcessingException(description='Already has this Group', code=400) + if schedule is not None: + raise ProcessingException( + description='A schedule with this name already exists', code=400) else: pass -def put_user_id_in_buffer(*args, **kargs): - data = request.data - request_body = json.loads(data) - global buffer_usersId - buffer_usersId = request_body['users'] - del kargs['data']['users'] +# Let's put onlt one user at time in the schedule +# def put_user_id_in_buffer(*args, **kargs): +# data = request.data +# request_body = json.loads(data) +# global buffer_usersId +# buffer_usersId = request_body['users'] +# del kargs['data']['users'] + +# def add_users_to_schedule(*args, **kargs): +# global buffer_usersId +# data = request.data +# request_body = json.loads(data) +# schedule = Schedules.query\ +# .filter_by(name=request_body['name']).first() +# #for userId in buffer_usersId: +# user = User.query.filter_by(id_=userId).first() +# schedule.tunel.append(user) +# db.session.add(schedule) +# db.session.commit() +# pass + +# def add_dates_to_schedule(*args, **kargs): +# global data_count +# data = request.data +# request_body = json.loads(data) +# schedule = Schedules.query.\ +# filter_by(name=request_body['name']).first() +# listOfdates = Dates.query.order_by(Dates.id_.desc()).limit(data_count) +# for date in listOfdates: +# schedule.dates_to_update.append(date) +# pass -def add_users_to_group(*args, **kargs): - global buffer_usersId - data = request.data - request_body = json.loads(data) - group = Groups.query\ - .filter_by(name=request_body['name']).first() - for userId in buffer_usersId: - user = User.query.filter_by(id_=userId).first() - group.tunel.append(user) - db.session.add(group) - db.session.commit() - pass - -def add_dates_to_group(*args, **kargs): - global data_count - data = request.data - request_body = json.loads(data) - group = Groups.query.\ - filter_by(name=request_body['name']).first() - listOfdates = Dates.query.order_by(Dates.id_.desc()).limit(data_count) - for date in listOfdates: - group.dates_to_update.append(date) - pass def transform_to_utc(*args, **kargs): global data_count @@ -131,7 +150,7 @@ def transform_to_utc(*args, **kargs): month = int(request_body['month']) how_many = int(request_body['count']) if year < min_year or ((month < min_month) and (year <= min_year)) or \ - (day < min_day and (month <= min_month and year <= min_year)): + (day < min_day and (month <= min_month and year <= min_year)): raise ProcessingException(description='Date not accept', code=400) else: del kargs['data']['day'] @@ -146,23 +165,38 @@ def transform_to_utc(*args, **kargs): db.session.commit() pass -def date_now(*args, **kargs): - kargs['data']['date'] = unicode(datetime.now()) - global buffer_usersId - buffer_usersId = kargs['data']['userId'] - #del kargs['data']['userId'] +# def date_now(*args, **kargs): +# kargs['data']['date'] = unicode(datetime.now()) +# global buffer_usersId +# buffer_usersId = kargs['data']['userId'] +# #del kargs['data']['userId'] -def add_user_balance(*args, **kargs): + +def add_user_data_balance(*args, **kargs): global buffer_usersId data = request.data request_body = json.loads(data) - # check if we are passing the user id or the imsi in the userId field + # Check if we are passing the user id or the imsi in the userId field, it is necessary because + # Openbts users IMSI only. if request_body['userId'] < 1e13: - x = Ballance.query.order_by(Ballance.id_.desc()).first() + x = DataBalance.query.order_by(DataBalance.id_.desc()).first() x.usersId = request_body['userId'] else: - x = Ballance.query.order_by(Ballance.id_.desc()).first() - x.usersId = User.query.filter_by( imsi=request_body['userId'] ).first().id_ + x = DataBalance.query.order_by(Balance.id_.desc()).first() + x.usersId = User.query.filter_by( + imsi=request_body['userId']).first().id_ + db.session.add(x) + db.session.commit() + + +def add_user_voice_balance(*args, **kargs): + global buffer_usersId + data = request.data + request_body = json.loads(data) + + x = VoiceBalance.query.order_by(VoiceBalance.id_.desc()).first() + x.usersId = request_body['userId'] + db.session.add(x) db.session.commit() @@ -187,22 +221,33 @@ def check_login(): return render_template('check.html') +@app.route('/checkadm') +def checkadm(): + """ + Index page, just show the logged username + """ + + return str(current_user) + + def auth(*args, **kargs): """ Required API request to be authenticated """ - #if not current_user.is_authenticated(): - # raise ProcessingException(description='Not authenticated', code=401) + if not current_user.is_authenticated(): + raise ProcessingException(description='Not authenticated', code=401) pass + def preprocessor_check_adm(*args, **kargs): + print str(current_user) if not current_user.is_admin(): raise ProcessingException(description='Forbidden', code=403) def preprocessors_patch(instance_id=None, data=None, **kargs): user_cant_change = ["admin", "clid", "id_", - "originated_calls", "received_calls, tunel"] + "originated_calls", "received_calls"] admin_cant_change = ["id_", "originated_calls", "received_calls"] if current_user.is_admin(): for x in data.keys(): @@ -223,15 +268,15 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): manager = flask.ext.restless.APIManager(app, flask_sqlalchemy_db=db) # Create the Flask-Restless API manager. -# Create API endpoints, which will be available at /api/ by +# Create API endpoints, which will be available at /pi/ by # default. Allowed HTTP methods can be specified as well. manager.create_api( User, preprocessors={ 'POST': [ - auth - # preprocessor_check_adm + auth, + preprocessor_check_adm ], 'GET_MANY': [ auth, @@ -249,9 +294,9 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): 'PATCH_MANY': [auth, preprocessor_check_adm], 'DELETE_SINGLE': [auth, preprocessor_check_adm], }, - postprocessors={ - 'POST':[to_openbts] - }, + postprocessors={ + 'POST': [to_openbts] + }, exclude_columns=[ 'password' ], @@ -261,35 +306,71 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): ) manager.create_api( - CDR, + Schedules, preprocessors={ 'POST': [ + # auth, + # preprocessor_check_adm, + # schedule_exists, + # put_user_id_in_buffer, + transform_to_utc + ], + 'GET_MANY': [ auth, preprocessor_check_adm ], + 'GET_SINGLE': [auth, preprocessor_check_adm], + 'PATCH_SINGLE': [ + auth, + preprocessor_check_adm + ], + 'DELETE_SINGLE': [auth, preprocessor_check_adm], + }, + postprocessors={ + #'POST': [add_dates_to_schedule, add_users_to_schedule], + 'POST': [], + }, + exclude_columns=[ + 'newUser', + 'removeUser', + 'updateSchedule' + ], + methods=['POST', 'GET', 'PATCH', 'DELETE'], + results_per_page=100 +) + +manager.create_api( + ScheduleUser, + preprocessors={ + 'POST': [ + preprocessor_check_adm, + # date_now + ], 'GET_MANY': [ auth, preprocessor_check_adm ], - 'GET_SINGLE': [ + 'GET_SINGLE': [auth, preprocessor_check_adm], + 'PATCH_SINGLE': [ auth, - preprocessors_check_adm_or_normal_user + preprocessor_check_adm ], - 'PATCH_SINGLE': [auth, preprocessors_patch], 'DELETE_SINGLE': [auth, preprocessor_check_adm], }, - methods=['POST','GET','PATCH', 'DELETE'] + postprocessors={ + 'POST': [] + }, + methods=['POST', 'GET', 'PATCH', 'DELETE'], + results_per_page=100, ) + manager.create_api( - Groups, + VoiceBalance, preprocessors={ 'POST': [ - auth, preprocessor_check_adm, - already_has_group, - put_user_id_in_buffer, - transform_to_utc + # date_now ], 'GET_MANY': [ auth, @@ -303,24 +384,18 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): 'DELETE_SINGLE': [auth, preprocessor_check_adm], }, postprocessors={ - 'POST': [add_dates_to_group, add_users_to_group], + 'POST': [add_user_voice_balance] }, - exclude_columns=[ - 'newUser', - 'removeUser', - 'updateGroup' - ], methods=['POST', 'GET', 'PATCH', 'DELETE'], results_per_page=100, - primary_key='name' ) manager.create_api( - Ballance, + DataBalance, preprocessors={ 'POST': [ - # preprocessor_check_adm, - date_now + # preprocessor_check_adm, + # date_now ], 'GET_MANY': [ auth, @@ -334,12 +409,13 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): 'DELETE_SINGLE': [auth, preprocessor_check_adm], }, postprocessors={ - 'POST': [add_user_balance] - }, + 'POST': [add_user_data_balance] + }, methods=['POST', 'GET', 'PATCH', 'DELETE'], results_per_page=100, ) + # start the flask loop app.debug = True app.run('0.0.0.0', 5000) diff --git a/celcombiller_caller.py b/celcombiller_caller.py index f29e7f6..23a4f31 100644 --- a/celcombiller_caller.py +++ b/celcombiller_caller.py @@ -20,7 +20,7 @@ try: agi.appexec('DIAL', 'SIP/IMSI%s@127.0.0.1:5062,40,S(%d)' %\ - (to_user.imsi, from_user.BallanceUser)) + (to_user.imsi, from_user.VoiceBalance)) except AGIAppError: pass diff --git a/celcombiller_reducer.py b/celcombiller_reducer.py index 46fea4e..dfc426c 100644 --- a/celcombiller_reducer.py +++ b/celcombiller_reducer.py @@ -38,7 +38,7 @@ # Create a new CDR record payload = '{"answer":"'+ str(answer)+'", "billsec":"'+ \ str(billsec)+'", "from_user_id": '+\ - str(from_user.id_)+', "to_user_id":'+ str(to_user.id_)+'}' + str(from_user.get_id)+', "to_user_id":'+ str(to_user.get_id)+'}' #Send the requestto update the user balance r = s.post('http://localhost:5000/api/cdr', json=json.loads(payload),\ @@ -49,7 +49,7 @@ pass payload = '{"signal":"-", "type_":"decrease", "value": "'+ str(billsec) +\ - '", "userId":'+ str(from_user.id_) +'}' + '", "userId":'+ str(from_user.get_id) +'}' # Send the requestto update the user balance r = s.post('http://localhost:5000/api/balance',\ diff --git a/models.py b/models.py index cf7df45..feae8e2 100644 --- a/models.py +++ b/models.py @@ -4,7 +4,8 @@ from sqlalchemy.dialects.postgresql import ENUM from datetime import * -row2dict = lambda r: {c.name: str(getattr(r, c.name)) for c in r.__table__.columns} +row2dict = lambda r: {c.name: str(getattr(r, c.name)) + for c in r.__table__.columns} # Create your Flask-SQLALchemy models as usual but with the following two # (reasonable) restrictions: @@ -14,10 +15,6 @@ # all columns (the constructor in flask.ext.sqlalchemy.SQLAlchemy.Model # supplies such a method, so you don't need to declare a new one). -tunel_table = db.Table('association', db.Model.metadata, - db.Column('User_id', db.Integer, db.ForeignKey('users.id_')), - db.Column('Groups_id', db.Integer, db.ForeignKey('groups.id_')) -) class User(db.Model): """ @@ -25,22 +22,18 @@ class User(db.Model): """ __tablename__ = 'users' - - - id_ = db.Column(db.Integer, primary_key=True, nullable=False) - admin = db.Column(db.Boolean, nullable=False) - name = db.Column(db.Unicode, nullable=False) - adress = db.Column(db.Unicode) - cpf = db.Column(db.Integer, nullable=False) - username = db.Column(db.Unicode, nullable=False, unique=True) - password = db.Column(db.Unicode, nullable=False) - clid = db.Column(db.String(9), nullable=False, unique=True) - imsi = db.Column(db.Integer, nullable=False, unique=True) + _id = db.Column(db.Integer, primary_key=True, nullable=False) + admin = db.Column(db.Boolean, nullable=False) + name = db.Column(db.Unicode, nullable=False) + adress = db.Column(db.Unicode) + cpf = db.Column(db.Integer, nullable=False, unique=True) + username = db.Column(db.Unicode, nullable=False, unique=True) + password = db.Column(db.Unicode, nullable=False) + clid = db.Column(db.String(9), nullable=False, unique=True) + imsi = db.Column(db.Integer, nullable=False, unique=True) voice_balance = db.Column(db.Integer, nullable=False) data_balance = db.Column(db.Integer, nullable=False) - tunel = db.relationship('Groups', secondary=tunel_table) - def is_admin(self): return self.admin @@ -54,172 +47,168 @@ def is_anonymous(self): return False def get_id(self): - return unicode(self.id_) + return self._id - def BallanceUser(self): + def VoiceBalance(self): return self.voice_balance - def DataBallanceUser(self): + def DataBalance(self): return self.data_balance @hybrid_property - def BallanceUserHistoric(self): + def VoiceBalanceHistoric(self): # TODO: Maybe it should use object_session - balances = Ballance.query.order_by(Ballance.id_.desc()).filter_by(userId=self.id_).limit(10) + balances = VoiceBalance.query.order_by( + VoiceBalance._id.desc()).filter_by(user_id=self._id).limit(10) historic_list = [] for y in balances: historic_list.append(row2dict(y)) + return historic_list + @hybrid_property + def DataBalanceHistoric(self): + # TODO: Maybe it should use object_session + balances = DataBalance.query.order_by( + DataBalance._id.desc()).filter_by(user_id=self._id).limit(10) + historic_list = [] + for y in balances: + historic_list.append(row2dict(y)) return historic_list - def __init__(self , admin, name, adress, cpf, username, password, clid, imsi, voice_balance, data_balance ): - self.admin = admin - self.name = name + def __init__(self, admin, name, cpf, username, password, clid, imsi, voice_balance, data_balance, adress=None): + self.admin = admin + self.name = name self.adress = adress - self.cpf = cpf - self.username = username - self.password = password - self.clid = clid - self.imsi = imsi + self.cpf = cpf + self.username = username + self.password = password + self.clid = clid + self.imsi = imsi self.voice_balance = voice_balance self.data_balance = data_balance - - def __repr__(self): return '' % (self.username) -class CDR(db.Model): - """ - Call Detail Records holds information about finished calls - """ - __tablename__ = 'cdr' - - id_ = db.Column(db.Integer, db.Sequence('cdr_id_seq'), primary_key=True) - answer = db.Column(db.DateTime) - billsec = db.Column(db.Integer) - from_user_id = db.Column(db.Integer, db.ForeignKey('users.id_')) - from_user = db.relationship('User', backref='originated_calls', - foreign_keys=from_user_id) - to_user_id = db.Column(db.Integer, db.ForeignKey('users.id_')) - to_user = db.relationship('User', backref='received_calls', - foreign_keys=to_user_id) - def __repr__(self): - return '' % (self.from_user, self.answer, - self.billsec) - -class Groups(db.Model): +class Schedules(db.Model): """ # Register Credits if """ - __tablename__ = 'groups' + __tablename__ = 'schedule' - id_ = db.Column(db.Integer, primary_key=True) + _id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode) - dates_to_update = db.relationship('Dates') - tunel = db.relationship('User', secondary=tunel_table) + date = db.Column(db.DateTime) - def __init__(self, name): + def __init__(self, name, date): self.name = name + self.date = date def __repr__(self): - return 'GROUPS %r' % (self.name) + return 'schedule %r' % (self.name) - @hybrid_property - def newUser(self): - return 0 - @newUser.setter - def newUser(self, userIds): - for userId in userIds: - self.tunel.append(User.query.filter_by(id_=userId).first()) - db.session.add(self) - db.session.commit() +# this table is the association table between users and group in a +# many-to-many relationship +class ScheduleUser(db.Model): - @hybrid_property - def removeUser(self): - return 0 + __tablename__ = 'schedule_user' - @removeUser.setter - def removeUser(self, userIds): - for userId in userIds: - self.tunel.remove(User.query.filter_by(id_=userId).first()) + user_id = db.Column(db.Integer, db.ForeignKey( + 'users._id'), primary_key=True) + group_id = db.Column(db.Integer, db.ForeignKey( + 'schedule._id'), primary_key=True) + count = db.Column(db.Integer, nullable=False) - @hybrid_property - def updateGroup(self): - return 0 + def __init__(self, user_id, group_id, count): + self.user_id = user_id + self.group_id = group_id + self.count = count + + def __repr__(self): + return 'GROUPS %r' % (self.name) - @updateGroup.setter - def updateGroup(self, check): - if check == 1: - deleteDate = Dates.query.filter_by(group_id=self.id_).first() - db.session.delete(deleteDate) - for var in self.tunel: - db.session.query(User).filter_by(id_=var.id_)\ - .update({'balance':'1250'}) -class Dates(db.Model): +class VoiceBalance(db.Model): + """ + Call Detail Records holds information about finished calls + """ + __tablename__ = 'voice_balance' - __tablename__ = 'dates' + _id = db.Column(db.Integer, db.Sequence('cdr_id_seq'), primary_key=True) + from_user_id = db.Column( + db.Integer, db.ForeignKey('users._id'), nullable=False) + to_user_id = db.Column(db.Integer, db.ForeignKey('users._id')) + value = db.Column(db.Integer) + date = db.Column(db.DateTime, nullable=False) + origin = db.Column(db.String, nullable=False) - id_ = db.Column(db.Integer, primary_key=True) - group_id = db.Column(db.Integer, db.ForeignKey('groups.id_')) - date = db.Column(db.DateTime) + from_user = db.relationship('User', backref='originated_calls', + foreign_keys=from_user_id) + to_user = db.relationship('User', backref='received_calls', + foreign_keys=to_user_id) - def __init__(self, date): - self.date = date + def __init__(self, from_user_id, value, data, origin, to_user_id=None): + self.from_user_id = from_user_id + self.to_user_id = to_user_id + self.value = value + self.data = data + self.origin = origin + + user = db.session.query(User).filter_by(_id=user_id).first() + user.voice_balance = user.voice_balance + int(value) def __repr__(self): - return 'DATES %r' % (self.id_) - - - -class Ballance(db.Model): - - __tablename__ = 'balance' - - id_ = db.Column(db.Integer, primary_key=True) - userId = db.Column(db.Integer, db.ForeignKey('users.id_')) - date = db.Column(db.DateTime()) - type_ = db.Column(ENUM('increase', 'decrease')) - value = db.Column(db.Integer) - signal = db.Column(db.String(1)) - balance = db.Column(ENUM('voice', 'data')) - - - def __init__(self, type_, value,balance, signal, userId,date=None): - self.date = datetime.now() - self.type_ = type_ - self.value = value - self.signal = signal - self.balance = balance - self.userId = userId - - if balance == 'voice': - if type_ == 'increase': - user = db.session.query(User).filter_by(id_=userId).first() - user.voice_balance = user.data_balance + int(value) - elif type_ == 'decrease': - user = db.session.query(User).filter_by(id_=userId).first() - user.voice_balance = user.data_balance - int(value) - db.session.commit() - - elif balance == 'data': - if type_ == 'increase': - user = db.session.query(User).filter_by(id_=userId).first() - user.data_balance = user.data_balance + int(value) - elif type_ == 'decrease': - user = db.session.query(User).filter_by(id_=userId).first() - user.data_balance = user.data_balance - int(value) - db.session.commit() + return '' % (self.from_user, self.date, + self.value) + + +# this table register the user's voice balance +class DataBalance(db.Model): + + __tablename__ = 'data_balance' + + _id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users._id')) + date = db.Column(db.DateTime()) + value = db.Column(db.Integer) + user_ip = db.Column(db.String) + connection_ip = db.Column(db.String) + origin = db.Column(db.String, nullable=False) + + def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None): + self.user_id = user_id + self.value = value + self.date = datetime.now() + self.user_ip = user_ip + self.connection_ip = connection_ip + self.origin = origin + + user = db.session.query(User).filter_by(imsi=user_id).first() + user.data_balance = user.data_balance + int(value) def __repr__(self): - return 'balance %r' % (self.id_) + return 'data_balance %r' % (self._id) + + +class ScheduleInput(db.Model): + __tablename__ = 'schedule_input' + _id = db.Column(db.Integer, primary_key=True) + schedule_id = db.Column(db.Integer, db.ForeignKey('schedule._id')) + voice_balance_id=db.Column(db.Integer, db.ForeignKey('voice_balance._id')) + data_balance_id=db.Column(db.Integer, db.ForeignKey('data_balance._id')) + def __init__(self, schedule_id, voice_balance_id, data_balance_id): + self.schedule_id=schedule_id + self.voice_balance_id=voice_balance_id + self.data_balance_id=data_balance_id + + def __repr__(self): + return 'schedule_input %r' % (self._id) -# Create the database tables. +# Create the database tablesself. db.create_all() From e09e7bd8ddbccdb72959ea0ab43c662f8e001228 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Mon, 23 May 2016 17:01:23 -0300 Subject: [PATCH 12/22] bug fixes --- README.md | 3 ++- adduser.py | 6 +++--- app.py | 52 ++++++++++++++++++++++++++-------------------------- models.py | 40 ++++++++++++++++++++++++---------------- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index b214d44..be956b6 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ the balance came by another table, so we want add balance to user we need run: add/remove data balance: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"value": "1000", "userId":1}' -s http://localhost:5000/api/data_balance +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"value": "1000", "user_id":3,"origin":"web"}' -s http://localhost:5000/api/data_balance + #note that userId need some user id, in that case we use 1 #to remove balance the value must be negative diff --git a/adduser.py b/adduser.py index ab3e081..0624213 100644 --- a/adduser.py +++ b/adduser.py @@ -1,12 +1,12 @@ from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from config import db -from models import User, Ballance +from models import User -admin = User(True,'administrator', 'nowhere', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0') -guest = User(True,'guest', 'nowhere', '1','guest', '123123', '999999998','999999999999998', '0' ,'0') +admin = User(True,'administrator', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0','nowhere') +guest = User(True,'guest', '1','guest', '123123', '999999998','999999999999998', '0' ,'0','nowhere') db.session.add(admin) db.session.add(guest) diff --git a/app.py b/app.py index 96b90de..7d3ac18 100755 --- a/app.py +++ b/app.py @@ -105,21 +105,21 @@ def schedule_exists(data=None, **kargs): pass # Let's put onlt one user at time in the schedule -# def put_user_id_in_buffer(*args, **kargs): +# def put_user__idin_buffer(*args, **kargs): # data = request.data # request_body = json.loads(data) -# global buffer_usersId -# buffer_usersId = request_body['users'] +# global buffer_users_id +# buffer_users_id = request_body['users'] # del kargs['data']['users'] # def add_users_to_schedule(*args, **kargs): -# global buffer_usersId +# global buffer_users_id # data = request.data # request_body = json.loads(data) # schedule = Schedules.query\ # .filter_by(name=request_body['name']).first() -# #for userId in buffer_usersId: -# user = User.query.filter_by(id_=userId).first() +# #for user_id in buffer_users_id: +# user = User.query.filter_by(_id=user_id).first() # schedule.tunel.append(user) # db.session.add(schedule) # db.session.commit() @@ -131,7 +131,7 @@ def schedule_exists(data=None, **kargs): # request_body = json.loads(data) # schedule = Schedules.query.\ # filter_by(name=request_body['name']).first() -# listOfdates = Dates.query.order_by(Dates.id_.desc()).limit(data_count) +# listOfdates = Dates.query.order_by(Dates._id.desc()).limit(data_count) # for date in listOfdates: # schedule.dates_to_update.append(date) # pass @@ -167,35 +167,35 @@ def transform_to_utc(*args, **kargs): # def date_now(*args, **kargs): # kargs['data']['date'] = unicode(datetime.now()) -# global buffer_usersId -# buffer_usersId = kargs['data']['userId'] -# #del kargs['data']['userId'] +# global buffer_users_id +# buffer_users_id = kargs['data']['user_id'] +# #del kargs['data']['user_id'] def add_user_data_balance(*args, **kargs): - global buffer_usersId + global buffer_users_id data = request.data request_body = json.loads(data) - # Check if we are passing the user id or the imsi in the userId field, it is necessary because + # Check if we are passing the user id or the imsi in the user_id field, it is necessary because # Openbts users IMSI only. - if request_body['userId'] < 1e13: - x = DataBalance.query.order_by(DataBalance.id_.desc()).first() - x.usersId = request_body['userId'] + if request_body['user_id'] < 1e13: + x = DataBalance.query.order_by(DataBalance._id.desc()).first() + x.users_id = request_body['user_id'] else: - x = DataBalance.query.order_by(Balance.id_.desc()).first() - x.usersId = User.query.filter_by( - imsi=request_body['userId']).first().id_ + x = DataBalance.query.order_by(Balance._id.desc()).first() + x.users_id = User.query.filter_by( + imsi=request_body['user_id']).first()._id db.session.add(x) db.session.commit() def add_user_voice_balance(*args, **kargs): - global buffer_usersId + global buffer_users_id data = request.data request_body = json.loads(data) - x = VoiceBalance.query.order_by(VoiceBalance.id_.desc()).first() - x.usersId = request_body['userId'] + x = VoiceBalance.query.order_by(VoiceBalance._id.desc()).first() + x.users_id = request_body['from_user_id'] db.session.add(x) db.session.commit() @@ -208,8 +208,8 @@ def logout(): @login_manager.user_loader -def load_user(id_): - return User.query.get(int(id_)) +def load_user(_id): + return User.query.get(int(_id)) @app.route('/check') @@ -246,9 +246,9 @@ def preprocessor_check_adm(*args, **kargs): def preprocessors_patch(instance_id=None, data=None, **kargs): - user_cant_change = ["admin", "clid", "id_", + user_cant_change = ["admin", "clid", "_id", "originated_calls", "received_calls"] - admin_cant_change = ["id_", "originated_calls", "received_calls"] + admin_cant_change = ["_id", "originated_calls", "received_calls"] if current_user.is_admin(): for x in data.keys(): if x in admin_cant_change: @@ -312,7 +312,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): # auth, # preprocessor_check_adm, # schedule_exists, - # put_user_id_in_buffer, + # put_user__idin_buffer, transform_to_utc ], 'GET_MANY': [ diff --git a/models.py b/models.py index feae8e2..8ecf0fc 100644 --- a/models.py +++ b/models.py @@ -59,7 +59,7 @@ def DataBalance(self): def VoiceBalanceHistoric(self): # TODO: Maybe it should use object_session balances = VoiceBalance.query.order_by( - VoiceBalance._id.desc()).filter_by(user_id=self._id).limit(10) + VoiceBalance._id.desc()).filter_by(from_user_id=self._id).limit(10) historic_list = [] for y in balances: historic_list.append(row2dict(y)) @@ -144,19 +144,22 @@ class VoiceBalance(db.Model): date = db.Column(db.DateTime, nullable=False) origin = db.Column(db.String, nullable=False) - from_user = db.relationship('User', backref='originated_calls', - foreign_keys=from_user_id) - to_user = db.relationship('User', backref='received_calls', - foreign_keys=to_user_id) + # from_user = db.relationship('User', backref='originated_calls', + # foreign_keys=from_user_id) + # to_user = db.relationship('User', backref='received_calls', + # foreign_keys=to_user_id) - def __init__(self, from_user_id, value, data, origin, to_user_id=None): + def __init__(self, from_user_id, value, origin, to_user_id=None, date=None): self.from_user_id = from_user_id self.to_user_id = to_user_id self.value = value - self.data = data self.origin = origin + if date != None: + self.date = date + else: + self.date = datetime.now() - user = db.session.query(User).filter_by(_id=user_id).first() + user = db.session.query(User).filter_by(_id=from_user_id).first() user.voice_balance = user.voice_balance + int(value) def __repr__(self): @@ -177,16 +180,20 @@ class DataBalance(db.Model): connection_ip = db.Column(db.String) origin = db.Column(db.String, nullable=False) - def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None): + def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None,date=None): self.user_id = user_id self.value = value - self.date = datetime.now() self.user_ip = user_ip self.connection_ip = connection_ip self.origin = origin - user = db.session.query(User).filter_by(imsi=user_id).first() + if date != None: + self.date = date + else: + self.date = datetime.now() + + user = db.session.query(User).filter_by(_id=user_id).first() user.data_balance = user.data_balance + int(value) def __repr__(self): @@ -198,13 +205,14 @@ class ScheduleInput(db.Model): _id = db.Column(db.Integer, primary_key=True) schedule_id = db.Column(db.Integer, db.ForeignKey('schedule._id')) - voice_balance_id=db.Column(db.Integer, db.ForeignKey('voice_balance._id')) - data_balance_id=db.Column(db.Integer, db.ForeignKey('data_balance._id')) + voice_balance_id = db.Column( + db.Integer, db.ForeignKey('voice_balance._id')) + data_balance_id = db.Column(db.Integer, db.ForeignKey('data_balance._id')) def __init__(self, schedule_id, voice_balance_id, data_balance_id): - self.schedule_id=schedule_id - self.voice_balance_id=voice_balance_id - self.data_balance_id=data_balance_id + self.schedule_id = schedule_id + self.voice_balance_id = voice_balance_id + self.data_balance_id = data_balance_id def __repr__(self): return 'schedule_input %r' % (self._id) From 03502cbe0e21de65d236f741096b4e6e36458372 Mon Sep 17 00:00:00 2001 From: vitor Date: Thu, 26 May 2016 21:00:00 -0300 Subject: [PATCH 13/22] feat(cross-domain), fix(style): Cross-domain habilitated and style update. Now we can use cross-domain request with Flask-CORS and all the python codes are using the PEP8 style guidelines. Also bugs in the login and logout screens were fixed. --- README.md | 4 ++-- adduser.py | 9 ++++----- app.py | 32 ++++++++++++++++++-------------- celcombiller_caller.py | 5 ++--- celcombiller_reducer.py | 39 ++++++++++++++++++++------------------- config.py | 6 +++++- models.py | 6 +++--- openbts.py | 37 ++++++++++++++++++------------------- requirements.txt | 15 ++++++++++++--- templates/index.html | 2 +- templates/logout.html | 7 +------ 11 files changed, 86 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index be956b6..34a5b53 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ```bash $ virtualenv -p /usr/bin/python2.7 venv $ source venv/bin/activate -$ pip install --allow-external pyst --allow-unverified pyst -r requirements.txt +$ pip install --allow-external --allow-unverified -r requirements.txt ``` ## Database setup @@ -42,7 +42,7 @@ To test the api we will use curl ###USER -login is required to test: +you must login before test the api: ```bash curl -c cookiefile -d "username=admin&password=adm123" -X POST -s http://localhost:5000/login diff --git a/adduser.py b/adduser.py index 0624213..9c25f94 100644 --- a/adduser.py +++ b/adduser.py @@ -1,12 +1,11 @@ -from flask import Flask -from flask.ext.sqlalchemy import SQLAlchemy from config import db from models import User - -admin = User(True,'administrator', '000','admin', 'adm123', '999999999','999999999999999', '0' ,'0','nowhere') -guest = User(True,'guest', '1','guest', '123123', '999999998','999999999999998', '0' ,'0','nowhere') +admin = User(True, 'administrator', '000', 'admin', 'adm123', + '999999999', '999999999999999', '0', '0', 'nowhere') +guest = User(True, 'guest', '1', 'guest', '123123', '999999998', + '999999999999998', '0', '0', 'nowhere') db.session.add(admin) db.session.add(guest) diff --git a/app.py b/app.py index 7d3ac18..4680c04 100755 --- a/app.py +++ b/app.py @@ -1,16 +1,14 @@ import flask -from flask import Flask, session, request, flash, url_for, redirect, \ - render_template, abort +from flask import request, flash, render_template from config import db, app, login_manager -from models import User, VoiceBalance, DataBalance, Schedules, ScheduleInput, \ +from models import User, VoiceBalance, DataBalance, Schedules, ScheduleInput, \ ScheduleUser -from time import strftime from datetime import * from dateutil.rrule import * from dateutil.parser import * from dateutil.relativedelta import * from flask_restless import ProcessingException -from flask.ext.login import login_user , logout_user , current_user ,\ +from flask.ext.login import login_user, logout_user, current_user,\ login_required import json from openbts import to_openbts @@ -21,7 +19,13 @@ def index(): """ Index page, just show the logged username """ - return render_template('index.html') + try: + if current_user.is_authenticated(): + return render_template('index.html') + else: + return "no else" + except Exception: + return render_template('anonymous.html') # Login, if the user does not exist it returs a error page @@ -42,7 +46,7 @@ def login(): else: flash('Flask Login error', 'error') return render_template('ERROR.html') - #json_with_names = check_time() + # json_with_names = check_time() # Returns the user data balance @@ -176,8 +180,8 @@ def add_user_data_balance(*args, **kargs): global buffer_users_id data = request.data request_body = json.loads(data) - # Check if we are passing the user id or the imsi in the user_id field, it is necessary because - # Openbts users IMSI only. + # Check if we are passing the user id or the imsi in the user_id field, it + # is necessary because Openbts users IMSI only. if request_body['user_id'] < 1e13: x = DataBalance.query.order_by(DataBalance._id.desc()).first() x.users_id = request_body['user_id'] @@ -275,12 +279,12 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): User, preprocessors={ 'POST': [ - auth, - preprocessor_check_adm + # auth, + # preprocessor_check_adm ], 'GET_MANY': [ - auth, - preprocessor_check_adm + # auth, + # preprocessor_check_adm ], 'GET_SINGLE': [ auth, @@ -327,7 +331,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): 'DELETE_SINGLE': [auth, preprocessor_check_adm], }, postprocessors={ - #'POST': [add_dates_to_schedule, add_users_to_schedule], + # 'POST': [add_dates_to_schedule, add_users_to_schedule], 'POST': [], }, exclude_columns=[ diff --git a/celcombiller_caller.py b/celcombiller_caller.py index 23a4f31..7f233cf 100644 --- a/celcombiller_caller.py +++ b/celcombiller_caller.py @@ -19,8 +19,7 @@ try: - agi.appexec('DIAL', 'SIP/IMSI%s@127.0.0.1:5062,40,S(%d)' %\ - (to_user.imsi, from_user.VoiceBalance)) + agi.appexec('DIAL', 'SIP/IMSI%s@127.0.0.1:5062,40,S(%d)' % + (to_user.imsi, from_user.VoiceBalance)) except AGIAppError: pass - diff --git a/celcombiller_reducer.py b/celcombiller_reducer.py index dfc426c..7b095da 100644 --- a/celcombiller_reducer.py +++ b/celcombiller_reducer.py @@ -8,9 +8,8 @@ # pylint: disable=C0103 from datetime import datetime import asterisk.agi -#from asterisk.agi import AGIAppError from models import User -from config import adm_user,adm_pssw +from config import adm_user, adm_pssw import requests import json @@ -32,31 +31,33 @@ # Create a session s = requests.Session() # Login the session - s.post('http://localhost:5000/login',\ - data={'username':adm_user, 'password': adm_pssw}) + s.post( + 'http://localhost:5000/login', + data={'username': adm_user, 'password': adm_pssw} + ) # Create a new CDR record - payload = '{"answer":"'+ str(answer)+'", "billsec":"'+ \ - str(billsec)+'", "from_user_id": '+\ - str(from_user.get_id)+', "to_user_id":'+ str(to_user.get_id)+'}' + payload = '{"answer":"' + str(answer) + '", "billsec":"' + \ + str(billsec) + '", "from_user_id": ' +\ + str(from_user.get_id) + ', "to_user_id":' + str(to_user.get_id) + '}' - #Send the requestto update the user balance - r = s.post('http://localhost:5000/api/cdr', json=json.loads(payload),\ - headers={'content-type': 'application/json'}) + # Send the requestto update the user balance + r = s.post('http://localhost:5000/api/cdr', json=json.loads(payload), + headers={'content-type': 'application/json'}) - #TODO: Handle when the request fail - if r.ok == False: + # TODO: Handle when the request fail + if r.ok is False: pass - payload = '{"signal":"-", "type_":"decrease", "value": "'+ str(billsec) +\ - '", "userId":'+ str(from_user.get_id) +'}' + payload = '{"signal":"-", "type_":"decrease", "value": "' + str(billsec) +\ + '", "userId":' + str(from_user.get_id) + '}' # Send the requestto update the user balance - r = s.post('http://localhost:5000/api/balance',\ - json=json.loads(payload),\ - headers={'content-type': 'application/json'}) + r = s.post('http://localhost:5000/api/balance', + json=json.loads(payload), + headers={'content-type': 'application/json'}) - #TODO: Handle when the request fail - if r.ok == False: + # TODO: Handle when the request fail + if r.ok is False: pass s.close() diff --git a/config.py b/config.py index ece8cc5..7e3a1e0 100644 --- a/config.py +++ b/config.py @@ -2,8 +2,9 @@ import flask.ext.sqlalchemy import flask.ext.restless from flask.ext.login import LoginManager +from flask.ext.cors import CORS -#adm login and password +# adm login and password adm_user = 'admin' adm_pssw = 'adm123' @@ -13,6 +14,9 @@ app.config['DEBUG'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/alph.db' +# Ability cross domain +cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) + db = flask.ext.sqlalchemy.SQLAlchemy(app) app.secret_key = 'abrakadabra' diff --git a/models.py b/models.py index 8ecf0fc..24d80f8 100644 --- a/models.py +++ b/models.py @@ -154,7 +154,7 @@ def __init__(self, from_user_id, value, origin, to_user_id=None, date=None): self.to_user_id = to_user_id self.value = value self.origin = origin - if date != None: + if date is not None: self.date = date else: self.date = datetime.now() @@ -180,7 +180,7 @@ class DataBalance(db.Model): connection_ip = db.Column(db.String) origin = db.Column(db.String, nullable=False) - def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None,date=None): + def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None, date=None): self.user_id = user_id self.value = value @@ -188,7 +188,7 @@ def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None,date self.connection_ip = connection_ip self.origin = origin - if date != None: + if date is not None: self.date = date else: self.date = datetime.now() diff --git a/openbts.py b/openbts.py index d4fb787..f9e4825 100644 --- a/openbts.py +++ b/openbts.py @@ -2,29 +2,28 @@ import zmq -def to_openbts(result = None, **kw): +def to_openbts(result=None, **kw): - id_ = "" - clid = "" - imsi = "" + id_ = "" + clid = "" + imsi = "" - for key,value in result.items(): - if key == "id_": - id_ = value + for key, value in result.items(): + if key == "id_": + id_ = value - elif key == "clid": - clid = value + elif key == "clid": + clid = value - elif key == "imsi": - imsi = value + elif key == "imsi": + imsi = value - request = '{"command":"subscribers","action":"create","fields":{"name":"' + str(id_) + '","imsi":"IMSI' + str(imsi) + '","msisdn":"' + str(clid) + '","ki":""}}' - - - context = zmq.Context() - socket = context.socket(zmq.REQ) - socket.connect("tcp://127.0.0.1:45064") - - socket.send(request) + request = '{"command":"subscribers","action":"create","fields":{"name":"' + \ + str(id_) + '","imsi":"IMSI' + str(imsi) + \ + '","msisdn":"' + str(clid) + '","ki":""}}' + context = zmq.Context() + socket = context.socket(zmq.REQ) + socket.connect("tcp://127.0.0.1:45064") + socket.send(request) diff --git a/requirements.txt b/requirements.txt index 913a4d9..83ba5f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,17 @@ -pyst==0.6.50 -SQLAlchemy==0.9.9 Flask==0.10.1 +Flask-Cors==2.1.2 +Flask-Login==0.3.0 Flask-Restless==0.17.0 Flask-SQLAlchemy==2.0 -flask-login==0.3.0 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 +mimerender==0.6.0 +pyst2==0.4.9 +python-dateutil==2.5.3 +python-mimeparse==1.5.2 pyzmq==15.0.0 requests==2.9.0 +six==1.10.0 +SQLAlchemy==0.9.9 +Werkzeug==0.11.10 diff --git a/templates/index.html b/templates/index.html index 855b797..835b528 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@ Hi {{ current_user.username }}! {% endif %} -

Vem Bindo +

Vem Vindo

\ No newline at end of file diff --git a/templates/logout.html b/templates/logout.html index a2ba607..a710d4e 100644 --- a/templates/logout.html +++ b/templates/logout.html @@ -3,12 +3,7 @@ login - {% if current_user.is_authenticated() %} - NAO EFETUOU LOGOUT {{ current_user.username }}! - {% else %} - EFETUOU LOGOUT - {% endif %} -

+

Ate mais

\ No newline at end of file From 95763d9137a1fe4df183c231593c0715d983f1eb Mon Sep 17 00:00:00 2001 From: vitor Date: Thu, 26 May 2016 21:00:00 -0300 Subject: [PATCH 14/22] feat(cross-domain), fix(style): Cross-domain habilitated and style update. Now we can use cross-domain request with Flask-CORS and all the python codes are using the PEP8 style guidelines. Also bugs in the login and logout screens were fixed. --- templates/anonymous.html | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 templates/anonymous.html diff --git a/templates/anonymous.html b/templates/anonymous.html new file mode 100644 index 0000000..a4c1634 --- /dev/null +++ b/templates/anonymous.html @@ -0,0 +1,10 @@ + + + login + + + +

Vem Vindo

+ clique aqui para fazer login + + \ No newline at end of file From 20dcba0e08f8402a947753e584052964ab663680 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Thu, 9 Jun 2016 16:00:27 -0300 Subject: [PATCH 15/22] small changes --- app.py | 21 ++++++++++++--------- config.py | 3 ++- models.py | 6 ++++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index 4680c04..cc487e3 100755 --- a/app.py +++ b/app.py @@ -27,6 +27,9 @@ def index(): except Exception: return render_template('anonymous.html') +@app.route('/test',methods=['GET']) +def test(): + return "rodou" # Login, if the user does not exist it returs a error page @app.route('/login', methods=['GET', 'POST']) @@ -238,16 +241,16 @@ def auth(*args, **kargs): """ Required API request to be authenticated """ - if not current_user.is_authenticated(): - raise ProcessingException(description='Not authenticated', code=401) + print args + #if not current_user.is_authenticated(): + # raise ProcessingException(description='Not authenticated', code=401) pass def preprocessor_check_adm(*args, **kargs): - print str(current_user) - if not current_user.is_admin(): - raise ProcessingException(description='Forbidden', code=403) - + # if not current_user.is_admin(): + # raise ProcessingException(description='Forbidden', code=403) + pass def preprocessors_patch(instance_id=None, data=None, **kargs): user_cant_change = ["admin", "clid", "_id", @@ -287,8 +290,8 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): # preprocessor_check_adm ], 'GET_SINGLE': [ - auth, - preprocessors_check_adm_or_normal_user + # auth, + # preprocessors_check_adm_or_normal_user ], 'PATCH_SINGLE': [ auth, @@ -373,7 +376,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): VoiceBalance, preprocessors={ 'POST': [ - preprocessor_check_adm, + #preprocessor_check_adm, # date_now ], 'GET_MANY': [ diff --git a/config.py b/config.py index 7e3a1e0..e3d72db 100644 --- a/config.py +++ b/config.py @@ -12,7 +12,8 @@ # Create the Flask application and the Flask-SQLAlchemy object. app = flask.Flask(__name__) app.config['DEBUG'] = True -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/alph.db' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////home/vitor/celcombiller/alph.db' + # Ability cross domain cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) diff --git a/models.py b/models.py index 24d80f8..24894cb 100644 --- a/models.py +++ b/models.py @@ -34,6 +34,12 @@ class User(db.Model): voice_balance = db.Column(db.Integer, nullable=False) data_balance = db.Column(db.Integer, nullable=False) + # ki = db.Column(db.Integer) + # city = db.Column(db.Unicode) + # state = db.Column(db.Unicode) + # postalcode = db.Column(db.Integer) + # + def is_admin(self): return self.admin From 6510e3691fecb1e0f4d7b0979afe2e57443cc0a7 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Fri, 10 Jun 2016 16:39:57 -0300 Subject: [PATCH 16/22] typo correction --- models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models.py b/models.py index 24894cb..fb147e1 100644 --- a/models.py +++ b/models.py @@ -25,11 +25,11 @@ class User(db.Model): _id = db.Column(db.Integer, primary_key=True, nullable=False) admin = db.Column(db.Boolean, nullable=False) name = db.Column(db.Unicode, nullable=False) - adress = db.Column(db.Unicode) + address = db.Column(db.Unicode) cpf = db.Column(db.Integer, nullable=False, unique=True) username = db.Column(db.Unicode, nullable=False, unique=True) password = db.Column(db.Unicode, nullable=False) - clid = db.Column(db.String(9), nullable=False, unique=True) + clid = db.Column(db.Unicode(9), nullable=False, unique=True) imsi = db.Column(db.Integer, nullable=False, unique=True) voice_balance = db.Column(db.Integer, nullable=False) data_balance = db.Column(db.Integer, nullable=False) @@ -81,10 +81,10 @@ def DataBalanceHistoric(self): historic_list.append(row2dict(y)) return historic_list - def __init__(self, admin, name, cpf, username, password, clid, imsi, voice_balance, data_balance, adress=None): + def __init__(self, admin=None, name=None, cpf=None, username=None, password=None, clid=None, imsi=None, voice_balance=None, data_balance=None, address=None): self.admin = admin self.name = name - self.adress = adress + self.address = address self.cpf = cpf self.username = username self.password = password From 7daa8fbc263ea1c8155a57b3a837edd612da367e Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Mon, 13 Jun 2016 14:11:09 -0300 Subject: [PATCH 17/22] feat(reducer) Update the celcombiller_reduce The celcombiller_reducer.py was updated to work with the new database schema --- celcombiller_reducer.py | 28 +++++++++------------------- models.py | 8 ++++---- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/celcombiller_reducer.py b/celcombiller_reducer.py index 7b095da..9c2cd69 100644 --- a/celcombiller_reducer.py +++ b/celcombiller_reducer.py @@ -36,25 +36,15 @@ data={'username': adm_user, 'password': adm_pssw} ) - # Create a new CDR record - payload = '{"answer":"' + str(answer) + '", "billsec":"' + \ - str(billsec) + '", "from_user_id": ' +\ - str(from_user.get_id) + ', "to_user_id":' + str(to_user.get_id) + '}' - - # Send the requestto update the user balance - r = s.post('http://localhost:5000/api/cdr', json=json.loads(payload), - headers={'content-type': 'application/json'}) - - # TODO: Handle when the request fail - if r.ok is False: - pass - - payload = '{"signal":"-", "type_":"decrease", "value": "' + str(billsec) +\ - '", "userId":' + str(from_user.get_id) + '}' - - # Send the requestto update the user balance - r = s.post('http://localhost:5000/api/balance', - json=json.loads(payload), + payload = {'from_user_id':from_user.get_id, + 'to_user_id':to_user.get_id, + 'value':billsec*(-1), + 'origin':'call', + 'date':answer} + + # Send the request to update the user balance + r = s.post('http://localhost:5000/api/voice_balance', + json=payload, headers={'content-type': 'application/json'}) # TODO: Handle when the request fail diff --git a/models.py b/models.py index fb147e1..e8a450d 100644 --- a/models.py +++ b/models.py @@ -81,7 +81,7 @@ def DataBalanceHistoric(self): historic_list.append(row2dict(y)) return historic_list - def __init__(self, admin=None, name=None, cpf=None, username=None, password=None, clid=None, imsi=None, voice_balance=None, data_balance=None, address=None): + def __init__(self, admin, name, cpf, username, password, clid, imsi, voice_balance=None, data_balance=None, address=None): self.admin = admin self.name = name self.address = address @@ -182,9 +182,9 @@ class DataBalance(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('users._id')) date = db.Column(db.DateTime()) value = db.Column(db.Integer) - user_ip = db.Column(db.String) - connection_ip = db.Column(db.String) - origin = db.Column(db.String, nullable=False) + user_ip = db.Column(db.Unicode) + connection_ip = db.Column(db.Unicode) + origin = db.Column(db.Unicode, nullable=False) def __init__(self, user_id, value, origin, user_ip=None, connection_ip=None, date=None): From 49f77fccd3ae914a82e20e94e0b0fd7aa6acc6f9 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Thu, 23 Jun 2016 12:04:39 -0300 Subject: [PATCH 18/22] feat(upstart) New upstart job to the back end. We have now two .conf files, one to frontend and one to the backend, and the openbts.py file is using the json library now. --- README.md | 8 ++++++ app.py | 1 - ...ombiller.conf => celcombiller-backend.conf | 2 +- celcombiller-frontend.conf | 14 ++++++++++ config.py | 6 +++-- openbts.py | 27 ++++++++++--------- 6 files changed, 42 insertions(+), 16 deletions(-) rename celcombiller.conf => celcombiller-backend.conf (86%) create mode 100644 celcombiller-frontend.conf diff --git a/README.md b/README.md index 34a5b53..cb08a77 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ + +CELCOMBiller +============ + +CELCOMBiller is an open source biller system to *OpenBTS* and *Asterisk* + + + # Used third-party libraries * pyst diff --git a/app.py b/app.py index cc487e3..c708d90 100755 --- a/app.py +++ b/app.py @@ -241,7 +241,6 @@ def auth(*args, **kargs): """ Required API request to be authenticated """ - print args #if not current_user.is_authenticated(): # raise ProcessingException(description='Not authenticated', code=401) pass diff --git a/celcombiller.conf b/celcombiller-backend.conf similarity index 86% rename from celcombiller.conf rename to celcombiller-backend.conf index 493d34c..2ce02d6 100644 --- a/celcombiller.conf +++ b/celcombiller-backend.conf @@ -1,6 +1,6 @@ # CELCOMBiller # -# This service runs celcombiller app.py from the point the system is +# This service runs celcombiller backend from the point the system is # started until it is shut down again. # on ubuntu you have to put this file in /etc/init # diff --git a/celcombiller-frontend.conf b/celcombiller-frontend.conf new file mode 100644 index 0000000..31eb642 --- /dev/null +++ b/celcombiller-frontend.conf @@ -0,0 +1,14 @@ +# CELCOMBiller +# +# This service runs celcombiller-frontend from the point the system is +# started until it is shut down again. +# on ubuntu you have to put this file in /etc/init +# +start on runlevel [2345] + +respawn + +script + exec /path_to_gulp/gulp -gulpfile /path_to_gulpfile/gulpfile.js +end script + diff --git a/config.py b/config.py index e3d72db..8fd61c2 100644 --- a/config.py +++ b/config.py @@ -8,15 +8,17 @@ adm_user = 'admin' adm_pssw = 'adm123' +path_to_database = './alph.db' + # Create the Flask application and the Flask-SQLAlchemy object. app = flask.Flask(__name__) app.config['DEBUG'] = True -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////home/vitor/celcombiller/alph.db' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+path_to_database # Ability cross domain -cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) +cors = CORS(app) db = flask.ext.sqlalchemy.SQLAlchemy(app) diff --git a/openbts.py b/openbts.py index f9e4825..df17642 100644 --- a/openbts.py +++ b/openbts.py @@ -1,16 +1,11 @@ -import sys import zmq - +import json def to_openbts(result=None, **kw): - id_ = "" - clid = "" - imsi = "" - for key, value in result.items(): - if key == "id_": - id_ = value + if key == "_id": + _id = value elif key == "clid": clid = value @@ -18,12 +13,20 @@ def to_openbts(result=None, **kw): elif key == "imsi": imsi = value - request = '{"command":"subscribers","action":"create","fields":{"name":"' + \ - str(id_) + '","imsi":"IMSI' + str(imsi) + \ - '","msisdn":"' + str(clid) + '","ki":""}}' + request = { + "command":"subscribers", + "action":"create", + "fields":{ + "name": str(_id), + "imsi":"IMSI" + str(imsi), + "msisdn":str(clid) , + "ki":"" + } + } + context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect("tcp://127.0.0.1:45064") - socket.send(request) + socket.send_string(json.dumps(request),encoding='utf-8') From ee502e15052447001a14a5ebc3a76071a82cf416 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Wed, 13 Jul 2016 12:03:30 -0300 Subject: [PATCH 19/22] feat(docs) Docs folder added A folder to store the documentation was added --- docs/dastabase.png | Bin 0 -> 70360 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/dastabase.png diff --git a/docs/dastabase.png b/docs/dastabase.png new file mode 100644 index 0000000000000000000000000000000000000000..2991eaaa37e83b041d8d12c6cc863d8417d80419 GIT binary patch literal 70360 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 literal 0 HcmV?d00001 From 206fc18bf0ecda6aae1e606afe17a8744e03f5d1 Mon Sep 17 00:00:00 2001 From: Vitor Soares Date: Mon, 1 Aug 2016 16:47:56 -0300 Subject: [PATCH 20/22] feat(users) Now we have 3 level of user We have 3 levels of user: 0(Adm),1(normal user) and 2 (collaborator) --- adduser.py | 4 ++-- app.py | 2 +- models.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/adduser.py b/adduser.py index 9c25f94..392ca9c 100644 --- a/adduser.py +++ b/adduser.py @@ -2,9 +2,9 @@ from models import User -admin = User(True, 'administrator', '000', 'admin', 'adm123', +admin = User('0', 'administrator', '000', 'admin', 'adm123', '999999999', '999999999999999', '0', '0', 'nowhere') -guest = User(True, 'guest', '1', 'guest', '123123', '999999998', +guest = User('3', 'guest', '1', 'guest', '123123', '999999998', '999999999999998', '0', '0', 'nowhere') db.session.add(admin) diff --git a/app.py b/app.py index c708d90..10b4335 100755 --- a/app.py +++ b/app.py @@ -252,7 +252,7 @@ def preprocessor_check_adm(*args, **kargs): pass def preprocessors_patch(instance_id=None, data=None, **kargs): - user_cant_change = ["admin", "clid", "_id", + user_cant_change = ["level", "clid", "_id", "originated_calls", "received_calls"] admin_cant_change = ["_id", "originated_calls", "received_calls"] if current_user.is_admin(): diff --git a/models.py b/models.py index e8a450d..e4e2593 100644 --- a/models.py +++ b/models.py @@ -23,7 +23,7 @@ class User(db.Model): __tablename__ = 'users' _id = db.Column(db.Integer, primary_key=True, nullable=False) - admin = db.Column(db.Boolean, nullable=False) + level = db.Column(db.Integer, nullable=False) # 0=administrator, 1=users, 2=collaborator name = db.Column(db.Unicode, nullable=False) address = db.Column(db.Unicode) cpf = db.Column(db.Integer, nullable=False, unique=True) @@ -41,7 +41,7 @@ class User(db.Model): # def is_admin(self): - return self.admin + return (level==0 if True else False) def is_authenticated(self): return True @@ -81,8 +81,8 @@ def DataBalanceHistoric(self): historic_list.append(row2dict(y)) return historic_list - def __init__(self, admin, name, cpf, username, password, clid, imsi, voice_balance=None, data_balance=None, address=None): - self.admin = admin + def __init__(self, level, name, cpf, username, password, clid, imsi, voice_balance=None, data_balance=None, address=None): + self.level = level self.name = name self.address = address self.cpf = cpf From b2624fba1341e0ab7f6862f68c043fe5ee565f6a Mon Sep 17 00:00:00 2001 From: vitor Date: Tue, 2 Aug 2016 21:57:51 -0300 Subject: [PATCH 21/22] feat(openbts) Solved the openbts connection issue Now if openbts is not online the system will try to communicate for one second then drop the connectio --- README.md | 2 +- app.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++---- models.py | 3 +-- openbts.py | 64 +++++++++++++++++++++++++++++++----------------- 4 files changed, 110 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cb08a77..66477f7 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ curl -c cookiefile -d "username=admin&password=adm123" -X POST -s http://localho now to add user: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","adress":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","address":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users ``` the balance came by another table, so we want add balance to user we need run: diff --git a/app.py b/app.py index c708d90..25aa1f4 100755 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ import flask -from flask import request, flash, render_template +from flask import request, flash, render_template, abort from config import db, app, login_manager from models import User, VoiceBalance, DataBalance, Schedules, ScheduleInput, \ ScheduleUser @@ -27,11 +27,15 @@ def index(): except Exception: return render_template('anonymous.html') -@app.route('/test',methods=['GET']) + +@app.route('/test', methods=['GET']) +@login_required def test(): return "rodou" # Login, if the user does not exist it returs a error page + + @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': @@ -45,14 +49,41 @@ def login(): flash('Username or Password is invalid', 'error') return render_template('ERROR.html') if login_user(registered_user): - return "Hello, cross-origin-world!" + return "Hello, cross-origin-world! " + current_user.name else: flash('Flask Login error', 'error') return render_template('ERROR.html') # json_with_names = check_time() +@login_manager.request_loader +def load_user_from_request(request): + + # first, try to login using the api_key url arg + api_key = request.args.get('api_key') + if api_key: + user = User.query.filter_by(api_key=api_key).first() + if user: + return user + + # next, try to login using Basic Auth + api_key = request.headers.get('Authorization') + if api_key: + api_key = api_key.replace('Basic ', '', 1) + try: + api_key = base64.b64decode(api_key) + except TypeError: + pass + user = User.query.filter_by(api_key=api_key).first() + if user: + return user + + # finally, return None if both methods did not login the user + return None + # Returns the user data balance + + @app.route('/check_data_balance', methods=['GET', 'POST']) def check_data_balance(*args, **kargs): data = request.data @@ -241,7 +272,7 @@ def auth(*args, **kargs): """ Required API request to be authenticated """ - #if not current_user.is_authenticated(): + # if not current_user.is_authenticated(): # raise ProcessingException(description='Not authenticated', code=401) pass @@ -251,6 +282,7 @@ def preprocessor_check_adm(*args, **kargs): # raise ProcessingException(description='Forbidden', code=403) pass + def preprocessors_patch(instance_id=None, data=None, **kargs): user_cant_change = ["admin", "clid", "_id", "originated_calls", "received_calls"] @@ -271,6 +303,35 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): if not (current_user.is_admin() or current_user.username == instance_id): raise ProcessingException(description='Forbidden', code=403) + +def new_user(*args, **kargs): + data = request.data + request_body = json.loads(data) + username = request_body['username'] + password = request_body['password'] + cpf = request_body['cpf'] + clid = request_body['clid'] + imsi = request_body['imsi'] + + if username is None or password is None: + abort(409) # missing arguments + if User.query.filter_by(username=username).first() is not None: + abort(409) # existing user + if User.query.filter_by(cpf=cpf).first() is not None: + abort(409) # existing user + if User.query.filter_by(clid=clid).first() is not None: + abort(409) # existing user + if User.query.filter_by(imsi=imsi).first() is not None: + abort(409) # existing user + + # user = User(username = username) + # user.hash_password(password) + # db.session.add(user) + # db.session.commit() + # return jsonify({ 'username': user.username }), 201, {'Location': + # url_for('get_user', id = user.id, _external = True)} + + manager = flask.ext.restless.APIManager(app, flask_sqlalchemy_db=db) # Create the Flask-Restless API manager. @@ -283,6 +344,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): 'POST': [ # auth, # preprocessor_check_adm + new_user, ], 'GET_MANY': [ # auth, @@ -375,7 +437,7 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): VoiceBalance, preprocessors={ 'POST': [ - #preprocessor_check_adm, + # preprocessor_check_adm, # date_now ], 'GET_MANY': [ diff --git a/models.py b/models.py index e8a450d..de68956 100644 --- a/models.py +++ b/models.py @@ -38,8 +38,7 @@ class User(db.Model): # city = db.Column(db.Unicode) # state = db.Column(db.Unicode) # postalcode = db.Column(db.Integer) - # - + def is_admin(self): return self.admin diff --git a/openbts.py b/openbts.py index df17642..6298471 100644 --- a/openbts.py +++ b/openbts.py @@ -1,32 +1,50 @@ import zmq import json +from flask import abort -def to_openbts(result=None, **kw): - - for key, value in result.items(): - if key == "_id": - _id = value - - elif key == "clid": - clid = value - elif key == "imsi": - imsi = value - - request = { - "command":"subscribers", - "action":"create", - "fields":{ - "name": str(_id), - "imsi":"IMSI" + str(imsi), - "msisdn":str(clid) , - "ki":"" - } - } +def to_openbts(result=None, **kw): + # for key, value in result.items(): + # if key == "_id": + # _id = value + + # elif key == "clid": + # clid = value + + # elif key == "imsi": + # imsi = value + _id = result["_id"] + clid = result["clid"] + imsi = result["imsi"] + + request = { + "command": "subscribers", + "action": "create", + "fields": { + "name": str(_id), + "imsi": "IMSI" + str(imsi), + "msisdn": str(clid), + "ki": "" + } + } context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect("tcp://127.0.0.1:45064") - - socket.send_string(json.dumps(request),encoding='utf-8') + # socket.poll(timeout=1000) + + socket.send_string(json.dumps(request), encoding='utf-8') + + # set timeout + poller = zmq.Poller() + poller.register(socket, zmq.POLLIN) + if poller.poll(1 * 1000): # 10s timeout in milliseconds + msg = socket.recv_json() + else: + # TODO: what do we do when the request fail ? + # raise IOError("Request to OpenBTS Timeout") + socket.close(linger=1) + context.term() + + abort(500) From b9e16c11942d11e2f0a115e27afbb754d9bc4225 Mon Sep 17 00:00:00 2001 From: Vitor Nunes Date: Wed, 3 Aug 2016 14:44:39 -0300 Subject: [PATCH 22/22] Revert "feat(openbts) Solved the openbts connection issue" --- README.md | 2 +- app.py | 72 ++++-------------------------------------------------- models.py | 3 ++- openbts.py | 64 +++++++++++++++++------------------------------- 4 files changed, 31 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 66477f7..cb08a77 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ curl -c cookiefile -d "username=admin&password=adm123" -X POST -s http://localho now to add user: ```bash -curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","address":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users +curl -b cookiefile -H "Content-Type: application/json" -X POST -d '{"username":"yourusername","password":"yourpassword","clid":"999999999","imsi":"12345678900", "admin":'false', "name":"administrator","adress":"lasse","cpf":"000","voice_balance":"0","data_balance":"0"}' -s http://localhost:5000/api/users ``` the balance came by another table, so we want add balance to user we need run: diff --git a/app.py b/app.py index 969f40f..10b4335 100755 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ import flask -from flask import request, flash, render_template, abort +from flask import request, flash, render_template from config import db, app, login_manager from models import User, VoiceBalance, DataBalance, Schedules, ScheduleInput, \ ScheduleUser @@ -27,15 +27,11 @@ def index(): except Exception: return render_template('anonymous.html') - -@app.route('/test', methods=['GET']) -@login_required +@app.route('/test',methods=['GET']) def test(): return "rodou" # Login, if the user does not exist it returs a error page - - @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': @@ -49,41 +45,14 @@ def login(): flash('Username or Password is invalid', 'error') return render_template('ERROR.html') if login_user(registered_user): - return "Hello, cross-origin-world! " + current_user.name + return "Hello, cross-origin-world!" else: flash('Flask Login error', 'error') return render_template('ERROR.html') # json_with_names = check_time() -@login_manager.request_loader -def load_user_from_request(request): - - # first, try to login using the api_key url arg - api_key = request.args.get('api_key') - if api_key: - user = User.query.filter_by(api_key=api_key).first() - if user: - return user - - # next, try to login using Basic Auth - api_key = request.headers.get('Authorization') - if api_key: - api_key = api_key.replace('Basic ', '', 1) - try: - api_key = base64.b64decode(api_key) - except TypeError: - pass - user = User.query.filter_by(api_key=api_key).first() - if user: - return user - - # finally, return None if both methods did not login the user - return None - # Returns the user data balance - - @app.route('/check_data_balance', methods=['GET', 'POST']) def check_data_balance(*args, **kargs): data = request.data @@ -272,7 +241,7 @@ def auth(*args, **kargs): """ Required API request to be authenticated """ - # if not current_user.is_authenticated(): + #if not current_user.is_authenticated(): # raise ProcessingException(description='Not authenticated', code=401) pass @@ -282,7 +251,6 @@ def preprocessor_check_adm(*args, **kargs): # raise ProcessingException(description='Forbidden', code=403) pass - def preprocessors_patch(instance_id=None, data=None, **kargs): user_cant_change = ["level", "clid", "_id", "originated_calls", "received_calls"] @@ -303,35 +271,6 @@ def preprocessors_check_adm_or_normal_user(instance_id=None, **kargs): if not (current_user.is_admin() or current_user.username == instance_id): raise ProcessingException(description='Forbidden', code=403) - -def new_user(*args, **kargs): - data = request.data - request_body = json.loads(data) - username = request_body['username'] - password = request_body['password'] - cpf = request_body['cpf'] - clid = request_body['clid'] - imsi = request_body['imsi'] - - if username is None or password is None: - abort(409) # missing arguments - if User.query.filter_by(username=username).first() is not None: - abort(409) # existing user - if User.query.filter_by(cpf=cpf).first() is not None: - abort(409) # existing user - if User.query.filter_by(clid=clid).first() is not None: - abort(409) # existing user - if User.query.filter_by(imsi=imsi).first() is not None: - abort(409) # existing user - - # user = User(username = username) - # user.hash_password(password) - # db.session.add(user) - # db.session.commit() - # return jsonify({ 'username': user.username }), 201, {'Location': - # url_for('get_user', id = user.id, _external = True)} - - manager = flask.ext.restless.APIManager(app, flask_sqlalchemy_db=db) # Create the Flask-Restless API manager. @@ -344,7 +283,6 @@ def new_user(*args, **kargs): 'POST': [ # auth, # preprocessor_check_adm - new_user, ], 'GET_MANY': [ # auth, @@ -437,7 +375,7 @@ def new_user(*args, **kargs): VoiceBalance, preprocessors={ 'POST': [ - # preprocessor_check_adm, + #preprocessor_check_adm, # date_now ], 'GET_MANY': [ diff --git a/models.py b/models.py index 379a074..e4e2593 100644 --- a/models.py +++ b/models.py @@ -38,7 +38,8 @@ class User(db.Model): # city = db.Column(db.Unicode) # state = db.Column(db.Unicode) # postalcode = db.Column(db.Integer) - + # + def is_admin(self): return (level==0 if True else False) diff --git a/openbts.py b/openbts.py index 6298471..df17642 100644 --- a/openbts.py +++ b/openbts.py @@ -1,50 +1,32 @@ import zmq import json -from flask import abort - def to_openbts(result=None, **kw): - # for key, value in result.items(): - # if key == "_id": - # _id = value - - # elif key == "clid": - # clid = value - - # elif key == "imsi": - # imsi = value - _id = result["_id"] - clid = result["clid"] - imsi = result["imsi"] - - request = { - "command": "subscribers", - "action": "create", - "fields": { - "name": str(_id), - "imsi": "IMSI" + str(imsi), - "msisdn": str(clid), - "ki": "" - } - } + for key, value in result.items(): + if key == "_id": + _id = value + + elif key == "clid": + clid = value + + elif key == "imsi": + imsi = value + + request = { + "command":"subscribers", + "action":"create", + "fields":{ + "name": str(_id), + "imsi":"IMSI" + str(imsi), + "msisdn":str(clid) , + "ki":"" + } + } + context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect("tcp://127.0.0.1:45064") - # socket.poll(timeout=1000) - - socket.send_string(json.dumps(request), encoding='utf-8') - - # set timeout - poller = zmq.Poller() - poller.register(socket, zmq.POLLIN) - if poller.poll(1 * 1000): # 10s timeout in milliseconds - msg = socket.recv_json() - else: - # TODO: what do we do when the request fail ? - # raise IOError("Request to OpenBTS Timeout") - socket.close(linger=1) - context.term() - - abort(500) + + socket.send_string(json.dumps(request),encoding='utf-8')