From 4705393a08eead527eb97ce6e71117f8678287a2 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 1 Jan 2021 16:39:25 +0100 Subject: [PATCH] API - refactor api responses --- docs/api/auth.html | 3 +- docs/searchindex.js | 2 +- fittrackee/activities/activities.py | 274 +++++++----------- fittrackee/activities/records.py | 6 +- fittrackee/activities/sports.py | 55 ++-- fittrackee/activities/stats.py | 44 +-- fittrackee/application/app_config.py | 39 +-- fittrackee/responses.py | 117 ++++++++ .../activities/test_activities_api_0_get.py | 16 +- .../activities/test_activities_api_1_post.py | 5 +- fittrackee/tests/users/test_auth_api.py | 24 +- fittrackee/tests/users/test_users_api.py | 4 +- fittrackee/users/auth.py | 230 +++++---------- fittrackee/users/users.py | 151 ++++------ fittrackee/users/utils.py | 76 ++--- 15 files changed, 482 insertions(+), 564 deletions(-) create mode 100644 fittrackee/responses.py diff --git a/docs/api/auth.html b/docs/api/auth.html index af1e00c7..b2ab9d69 100644 --- a/docs/api/auth.html +++ b/docs/api/auth.html @@ -240,7 +240,8 @@
Status Codes
diff --git a/docs/searchindex.js b/docs/searchindex.js index ad0fe59e..29bcde31 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["api/activities","api/auth","api/configuration","api/index","api/records","api/sports","api/stats","api/users","changelog","features","index","installation","troubleshooting/administrator","troubleshooting/index","troubleshooting/user"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["api/activities.rst","api/auth.rst","api/configuration.rst","api/index.rst","api/records.rst","api/sports.rst","api/stats.rst","api/users.rst","changelog.md","features.rst","index.rst","installation.rst","troubleshooting/administrator.rst","troubleshooting/index.rst","troubleshooting/user.rst"],objects:{"":{"/api/activities":[0,1,1,"post--api-activities"],"/api/activities/(string:activity_short_id)":[0,3,1,"patch--api-activities-(string-activity_short_id)"],"/api/activities/(string:activity_short_id)/chart_data":[0,0,1,"get--api-activities-(string-activity_short_id)-chart_data"],"/api/activities/(string:activity_short_id)/chart_data/segment/(int:segment_id)":[0,0,1,"get--api-activities-(string-activity_short_id)-chart_data-segment-(int-segment_id)"],"/api/activities/(string:activity_short_id)/gpx":[0,0,1,"get--api-activities-(string-activity_short_id)-gpx"],"/api/activities/(string:activity_short_id)/gpx/segment/(int:segment_id)":[0,0,1,"get--api-activities-(string-activity_short_id)-gpx-segment-(int-segment_id)"],"/api/activities/map/(map_id)":[0,0,1,"get--api-activities-map-(map_id)"],"/api/activities/no_gpx":[0,1,1,"post--api-activities-no_gpx"],"/api/auth/login":[1,1,1,"post--api-auth-login"],"/api/auth/logout":[1,0,1,"get--api-auth-logout"],"/api/auth/password/reset-request":[1,1,1,"post--api-auth-password-reset-request"],"/api/auth/password/update":[1,1,1,"post--api-auth-password-update"],"/api/auth/picture":[1,1,1,"post--api-auth-picture"],"/api/auth/profile":[1,0,1,"get--api-auth-profile"],"/api/auth/profile/edit":[1,1,1,"post--api-auth-profile-edit"],"/api/auth/register":[1,1,1,"post--api-auth-register"],"/api/config":[2,3,1,"patch--api-config"],"/api/ping":[2,0,1,"get--api-ping"],"/api/records":[4,0,1,"get--api-records"],"/api/sports":[5,0,1,"get--api-sports"],"/api/sports/(int:sport_id)":[5,3,1,"patch--api-sports-(int-sport_id)"],"/api/stats/(user_name)/by_sport":[6,0,1,"get--api-stats-(user_name)-by_sport"],"/api/stats/(user_name)/by_time":[6,0,1,"get--api-stats-(user_name)-by_time"],"/api/stats/all":[6,0,1,"get--api-stats-all"],"/api/users":[7,0,1,"get--api-users"],"/api/users/(user_name)":[7,3,1,"patch--api-users-(user_name)"],"/api/users/(user_name)/picture":[7,0,1,"get--api-users-(user_name)-picture"],"APP_LOG \ud83c\udd95":[11,4,1,"envvar-APP_LOG"],"DATABASE_DISABLE_POOLING \ud83c\udd95":[11,4,1,"envvar-DATABASE_DISABLE_POOLING"],"MAP_ATTRIBUTION \ud83c\udd95":[11,4,1,"envvar-MAP_ATTRIBUTION"],"TILE_SERVER_URL \ud83c\udd95":[11,4,1,"envvar-TILE_SERVER_URL"],"UPLOAD_FOLDER \ud83c\udd95":[11,4,1,"envvar-UPLOAD_FOLDER"],APP_SECRET_KEY:[11,4,1,"-"],APP_SETTINGS:[11,4,1,"-"],APP_WORKERS:[11,4,1,"-"],DATABASE_URL:[11,4,1,"-"],EMAIL_URL:[11,4,1,"-"],FLASK_APP:[11,4,1,"-"],HOST:[11,4,1,"-"],PORT:[11,4,1,"-"],REACT_APP_ALLOW_REGISTRATION:[11,4,1,"-"],REACT_APP_API_URL:[11,4,1,"-"],REACT_APP_GPX_LIMIT_IMPORT:[11,4,1,"-"],REACT_APP_MAX_SINGLE_FILE_SIZE:[11,4,1,"-"],REACT_APP_MAX_ZIP_FILE_SIZE:[11,4,1,"-"],REACT_APP_THUNDERFOREST_API_KEY:[11,4,1,"-"],REDIS_URL:[11,4,1,"-"],SENDER_EMAIL:[11,4,1,"-"],UI_URL:[11,4,1,"-"],WEATHER_API_KEY:[11,4,1,"-"],WORKERS_PROCESSES:[11,4,1,"-"]},"/api/activities/map_tile/(s)/(z)/(x)/(y)":{png:[0,0,1,"get--api-activities-map_tile-(s)-(z)-(x)-(y).png"]}},objnames:{"0":["http","get","HTTP get"],"1":["http","post","HTTP post"],"2":["http","delete","HTTP delete"],"3":["http","patch","HTTP patch"],"4":["std","envvar","environment variable"]},objtypes:{"0":"http:get","1":"http:post","2":"http:delete","3":"http:patch","4":"std:envvar"},terms:{"0mb":[0,1],"1000":6,"1048576":2,"10485760":2,"10mb":11,"1232004":0,"12341":6,"1234538":0,"1267":6,"127":11,"1563529507772":0,"1mb":11,"200":[0,1,2,4,5,6,7],"201":[0,1],"2017":[0,6],"2018":[0,6,10],"2019":[0,1,4,6,7,10],"2020":10,"204":[0,1,7],"279":0,"280":0,"282":6,"2930":0,"2e1ee2c":8,"3000":[11,12],"301":11,"34614d5":8,"400":[0,1,2,5],"401":[0,1,2,4,5,6,7],"403":[0,1,2,5,6,7],"404":[0,1,5,6,7],"4109":0,"413":[0,1],"443":11,"465":11,"4c3fc34":8,"500":[0,1,2,5,7],"5000":11,"5078118":0,"5079733":0,"5432":11,"587":11,"613":6,"7380":0,"895":[1,7],"9960":6,"boolean":[2,7],"default":[0,6,7,8,11],"export":10,"float":0,"import":11,"int":[0,5],"new":[9,11],"null":[0,1,7],"return":[0,4,11],"short":0,"static":11,"true":[1,2,5,7,11],"try":[0,1,7],For:11,NOT:[0,1,5],Not:[0,1,5,6,7],One:11,That:1,The:[8,9,11],There:11,Use:8,WITH:11,With:11,__main__:11,_blank:11,accord:10,account:[7,8],acit:0,activ:[3,5,6,8,10,11],activities_count:7,activity_d:[0,4],activity_id:[0,4],activity_short_id:0,adapt:11,add:[7,8,9,10],address:11,admin:[0,1,2,4,5,6,7,8,11],administr:[0,1,7,10,13],after:11,again:[0,1,2,4,5,6,7],all:[4,5,6,7,11],allow:[0,1,2,10,11],along:0,alpinequest:10,alreadi:1,also:10,alwai:11,android:10,anoth:[7,11],anymor:8,apach:10,api:[0,1,2,4,5,6,7,8,9,10,11,12],apikei:11,app:10,app_log:11,app_secret_kei:11,app_set:11,app_work:11,applic:[0,1,2,4,5,6,7,8,9,10,11],arch:11,archiv:[2,9,11],archlinux:11,asc:7,ascent:0,attribut:11,auth:[0,1,2,4,5,6,7],auth_token:1,auth_user_id:[0,2,4,5,6,7],authent:[0,2,3,4,5,6,7,10],author:[0,1,2,4,5,6,7],avail:[4,9,10],ave_spe:0,ave_speed_from:0,ave_speed_to:0,averag:[0,4,8,9],axi:0,b862a77:8,background:8,backup:11,bad:[0,1,2,5],bearer:[0,1,2,4,5,6,7],becom:8,been:8,befor:11,bike:[0,5,8,9],bin:11,bio:[1,7],biographi:1,birth:1,birth_dat:[1,7],bound:0,by_sport:6,by_tim:6,calcul:8,calendar:[8,9],can:[7,8,9,10,11],cannot:8,chang:[9,10,11],charact:[1,13],chart:[0,8,9,11],chart_data:0,check:[2,11,12],choos:8,client:[8,11,12],clone:11,code:[0,1,2,4,5,6,7],color:8,column:13,com:[1,7,11],complet:8,config:[2,11,12],configur:[3,10,11],confirm:1,contact:[0,1,7],contain:[8,11],content:[0,1,2,4,5,6,7],contributor:[2,11],coordin:11,copi:[2,11],copyright:[2,11],correctli:[8,12],creat:[0,1,8,9,11],create_app:11,created_at:[1,7],creation:[8,9],creation_d:0,credenti:[1,11],criteria:7,current:8,custom:[11,12],cycl:[5,8,9],dai:8,dark:11,darkski:[8,9],dashboard:[8,10],data:[0,1,2,4,5,6,7,8,10,11,13],databas:[8,11],database_disable_pool:11,database_url:11,date:[0,1,6,8,9],debian:11,defin:9,definit:8,delet:[0,1,7,8,9],depend:[8,11],deploy:10,desc:0,descent:0,describ:11,descript:11,detail:[7,8,10],develop:[10,11],differ:8,directli:11,directori:11,disabl:[1,8,9,11],displai:[0,8,9,10,11],distanc:[0,4,8,9],distance_from:0,distance_to:0,distribut:11,docker:11,document:[8,10,11],doe:[0,1,6,7,8],don:1,down:8,download:11,dramatiq:11,drop:8,durat:[0,4,8,9],duration_from:0,duration_to:0,dure:[0,1],easi:8,edit:[1,8,9],elev:[0,8,9,11],els:11,email:[1,7,8],email_url:11,empti:8,enabl:[2,9],encount:11,end:[0,6],endpoint:[2,3,13],english:9,enter:[8,9],entiti:[0,1],entri:11,env:11,environ:[8,10],environn:12,error:[0,1,2,5,7,8,11],europ:[1,7],even:[8,9],exampl:[0,1,2,4,5,6,7,10,11,12],exce:[0,1],except:7,execstart:11,exist:[0,1,6,7,9,10,12],exodu:10,expir:[0,1,2,4,5,6,7],extens:[0,1],fa33f4d996844a5c73ecd1ae24456ab8:0,fals:[0,1,2,5,7,11],farest:[4,8,9],featur:[10,11],fetch:11,file:[0,1,2,8,9,10,11,12],filter:[8,9],first:[1,10],first_nam:[1,7],fitotrack:10,fittracke:[9,11],fittrackee_init_data:11,fittrackee_upgrade_db:11,fittrackee_work:11,fix:10,flask:11,flask_app:11,flaticon:11,follow:[4,9,11],forbidden:[0,1,2,5,6,7],forecast:11,form:[0,1],format:[0,1,6],former:11,forrunn:10,forward:11,found:[0,1,5,6,7],frame:6,freepik:11,french:[9,10],fri:0,from:[0,2,4,5,6,7,8,10],fullchain:11,gener:11,get:[0,1,2,4,5,6,7],gif:1,git:11,github:11,given:11,gmt:[0,1,4,7],gpl:10,gpx:[0,8,9,10,11],gpx_limit_import:2,gpxpy:11,grant:11,gunicorn:11,handl:1,has:7,has_act:5,have:[0,2,5,6,7,8],header:[0,1,2,4,5,6,7],health:2,heavi:[10,11],hike:[5,8,9],his:[7,8,9],home:11,host:11,href:[2,11],http:[0,1,2,4,5,6,7,11,12],hvybqybra7wwxpastwr4v2:4,i18n:8,icon:11,imag:[0,1,7,11],img:5,improv:10,incorrect:8,index:0,info:1,inform:[8,10,11],initi:[11,12],instal:[8,10],instanc:[2,11],integ:[0,2,4,5,6,7],integr:2,interceptor:8,intern:[0,1,2,5,7],introduc:8,invalid:[0,1,2,4,5,6,7],is_act:5,is_registration_en:2,issu:[10,11],jan:0,javascript:11,john_do:7,jpeg:7,jpg:1,json:[0,1,2,4,5,6,7,13],jul:[0,1,4,7],keep:[10,11],kei:[8,9,11],kjxavsturjvoah2wvcegef:0,label:5,languag:[1,7],larg:[0,1],last:[1,11],last_nam:[1,7],latitud:0,layer:[8,11],leaflet:[0,11],least:0,librari:11,licens:10,limit:[8,9],line:13,link:11,linux:11,list:[8,10,11],listen:11,load:8,local:[8,10,11],localhost:[11,12],locat:[1,7,11],log:[0,1,2,4,5,6,7,10,11],logfil:11,login:1,logout:[1,8],longest:[4,8,9],longitud:0,made:[11,12],mai:[10,11],mailhog:11,major:8,make:11,makefil:[11,12],manag:8,mandatori:[0,8,11],map:[0,8,9,10],map_attribut:[2,11],map_id:0,map_til:0,match:1,max:[0,2,7],max_alt:0,max_single_file_s:2,max_spe:0,max_speed_from:0,max_speed_to:0,max_us:2,max_zip_file_s:2,maxim:0,maximum:[4,8,9,11],mean:12,messag:[0,1,2],method:11,min_alt:0,minim:0,minor:10,mobil:10,modifi:7,modification_d:0,modul:11,mon:0,mondai:[1,6,9],montain:[8,9],month:[6,8,9],more:[8,10,11],morn:0,mountain:5,mous:8,move:[0,8],mpwoadmin:11,multi:11,multipart:[0,1],must:[1,2,5,8,9,11],name:[1,6,7,11],nano:11,nb_activ:[1,6,7],nb_sport:[1,7],necessari:11,need:11,network:[11,12],next_act:0,nginx:11,no_gpx:0,non:5,noopen:11,noreferr:11,note:[0,8,9,11],now:[8,9,11],number:[0,2,7,9,11],oauth:[0,1,2,4,5,6,7],object:[0,1,2,5,7],one:[0,7],onli:[0,7,8,9,11],open:[10,11],openstreetmap:[2,8,11],opentrack:10,option:11,order:[0,7,8],order_bi:7,org:[2,11],other:[7,11],out:1,outdoor:[8,10,11],over:8,own:[7,10],owner:[8,9],packag:[8,11],page:[0,7],pagin:[0,7],par_pag:7,paramet:[0,1,2,4,5,6,7,8,9,11],pari:[1,7],pars:[11,13],part:[0,1],pass:11,password:[1,8,9,11],password_conf:1,patch:[0,2,5,7],path:11,paus:[0,8],payload:[0,1,2,5],pem:11,per:[0,7],per_pag:[0,7],permiss:[0,2,5,6,7],pg_dump:11,pictur:[0,1,7,11],ping:2,pip:11,pipenv:8,pleas:[0,1,2,4,5,6,7,8],png:[0,1,5,11],poetri:[8,11],point:[8,11],pong:2,pool:11,port:11,possibl:[8,10],post:[0,1],postgr:11,postgresql:11,prefer:1,prerequisit:10,previous_act:0,prior:11,privai:10,privileg:11,privkei:11,process:[1,11],productionconfig:11,profil:1,project:11,proprietari:10,provid:[0,1,2,4,5,6,7,8,9,11],proxi:11,proxy_add_x_forwarded_for:11,proxy_pass:11,proxy_redirect:11,proxy_set_head:11,pull:11,pwd:11,pypi:10,python:[8,11],queri:[0,6,7],queue:11,react:11,react_app_allow_registr:11,react_app_api_url:[11,12],react_app_gpx_limit_import:11,react_app_max_single_file_s:11,react_app_max_zip_file_s:11,react_app_thunderforest_api_kei:11,read:8,real:11,rebuild:12,rechart:[0,11],recommend:11,record:[0,3,8,9,10],record_typ:[0,4],redi:[8,11],redis_url:11,redux:11,regist:[1,2,11],registr:[1,2,8,9],rel:11,relat:11,releas:[10,11],remote_addr:11,remov:9,renam:8,replac:[8,11],repo:11,report:10,repositori:11,request:[0,1,2,4,5,6,7,12],request_uri:11,requir:1,reset:[1,8,9],respons:[0,1,2,4,5,6,7],restart:11,restartsec:11,right:[7,9],rout:8,run:[5,8,9,11],runner:10,sam:[1,7],same:8,samr1:11,sat:7,save:9,schema:11,search:8,second:0,secret:11,see:[8,9,10,11,12],segment:[0,8,9],segment_id:0,select:[0,1],send:[8,11],sender:11,sender_email:11,serv:11,server:[0,1,2,5,7,8,10],server_nam:11,servic:11,set:9,sever:[10,11],should:11,show:8,side:8,signatur:[0,1,2,4,5,6,7],simpl:11,simplifi:8,sinc:11,singl:[2,7],size:[0,1,2,8,9,11],sky:11,smtp:11,some:[0,7,8,10,11],sorri:1,sort:[0,7],sourc:10,spawn:11,speed:[0,4,8,9,11],spinner:8,sport:[0,3,6,8,9,10,11],sport_id:[0,4,5,6],sports_list:[1,7],sql:11,sqlalchemi:11,ssl:11,ssl_certif:11,ssl_certificate_kei:11,standard:[8,11],standarderror:11,standardoutput:11,start:[0,1,6,9,11],startlimitintervalsec:11,starttl:11,stat:[6,8],staticmap:11,statist:[3,10],statu:[0,1,2,4,5,6,7],step:11,still:10,stop:11,store:[10,11],street:10,string:[0,1,5,6,7],subdomain:0,success:[0,1,2,4,5,6,7],successfulli:1,sun:[0,1,4,7],sundai:[0,6,9],support:[8,9],syslog:11,syslogidentifi:11,system:11,systemd:11,tab:12,tar:11,target:11,task:11,term:11,test:11,than:8,thei:11,them:10,thi:[0,8,9,10,11],thunderforest:[8,11],tile:[0,8],tile_server_url:11,time:[0,1,6,8,9],timezon:[1,7,8],titl:0,tls:11,todo:14,token:[0,1,2,4,5,6,7],too:[0,1],tooltip:8,total:8,total_dist:[1,6,7],total_dur:[1,6,7],track:10,tracker:10,transport:[5,8,9],troubleshoot:10,type:[0,1,2,4,5,6,7,11],ui_url:11,unauthor:[0,1,2,4,5,6,7],undefin:12,under:[10,11],unencrypt:11,unexpect:13,unit:11,unstabl:[10,11],updat:[0,1,2,5,7,8,9,11],upload:[8,9,11],upload_fold:11,uploads_dir_s:6,url:[8,11],use:11,used:11,user:[0,1,2,3,4,5,6,8,9,10,11,13],user_nam:[6,7],usernam:[1,7,11],usernanm:1,using:[0,7,10,11],uuid:[0,8],valid:[0,1,2,4,5,6,7,11],valu:[0,4],variabl:[8,10,12],venv:11,version:[10,11],view:8,virtualenv:11,wai:11,walk:[5,8,9],wantedbi:11,warn:8,weather:[8,9,11],weather_api:11,weather_api_kei:11,weather_end:0,weather_start:0,web:[0,1,2,4,5,6,7,10,11],week:[1,6,8,9],weekend:8,weekm:[1,6],were:8,wget:11,when:[8,11],where:11,which:9,with_gpx:0,without:[0,6,7,8,9,10],worker:11,workers_process:11,workingdirectori:11,workout:10,written:11,www:[2,11],xxxx:11,xzf:11,yai:11,yarn:11,year:6,yet:10,you:[0,2,5,6,7,10],your:[7,10,11],zip:[0,2,9,11],zone:1,zoom:0},titles:["Activities","Authentication","Configuration","API documentation","Records","Sports","Statistics","Users","Change log","Features","FitTrackee","Installation","Administrator","Troubleshooting","User"],titleterms:{"2018":8,"2019":8,"2020":8,"new":8,account:9,activ:[0,9],administr:[8,9,12],api:3,authent:1,avail:8,bug:8,chang:8,charact:12,close:8,column:12,configur:2,content:10,dashboard:9,data:12,deploy:11,deprec:11,detail:9,dev:11,document:3,email:11,environ:11,featur:[8,9],first:8,fittracke:[8,10],fix:8,french:8,from:11,improv:8,instal:11,issu:8,json:12,line:12,list:9,log:8,map:11,minor:8,misc:8,pars:12,prerequisit:11,prod:11,product:11,pypi:[8,11],record:4,releas:8,server:11,sourc:11,sport:5,statist:[6,8,9],tabl:10,tile:11,translat:9,troubleshoot:13,unexpect:12,upgrad:11,user:[7,14],variabl:11,version:8,workout:9}}) \ No newline at end of file +Search.setIndex({docnames:["api/activities","api/auth","api/configuration","api/index","api/records","api/sports","api/stats","api/users","changelog","features","index","installation","troubleshooting/administrator","troubleshooting/index","troubleshooting/user"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["api/activities.rst","api/auth.rst","api/configuration.rst","api/index.rst","api/records.rst","api/sports.rst","api/stats.rst","api/users.rst","changelog.md","features.rst","index.rst","installation.rst","troubleshooting/administrator.rst","troubleshooting/index.rst","troubleshooting/user.rst"],objects:{"":{"/api/activities":[0,1,1,"post--api-activities"],"/api/activities/(string:activity_short_id)":[0,3,1,"patch--api-activities-(string-activity_short_id)"],"/api/activities/(string:activity_short_id)/chart_data":[0,0,1,"get--api-activities-(string-activity_short_id)-chart_data"],"/api/activities/(string:activity_short_id)/chart_data/segment/(int:segment_id)":[0,0,1,"get--api-activities-(string-activity_short_id)-chart_data-segment-(int-segment_id)"],"/api/activities/(string:activity_short_id)/gpx":[0,0,1,"get--api-activities-(string-activity_short_id)-gpx"],"/api/activities/(string:activity_short_id)/gpx/segment/(int:segment_id)":[0,0,1,"get--api-activities-(string-activity_short_id)-gpx-segment-(int-segment_id)"],"/api/activities/map/(map_id)":[0,0,1,"get--api-activities-map-(map_id)"],"/api/activities/no_gpx":[0,1,1,"post--api-activities-no_gpx"],"/api/auth/login":[1,1,1,"post--api-auth-login"],"/api/auth/logout":[1,0,1,"get--api-auth-logout"],"/api/auth/password/reset-request":[1,1,1,"post--api-auth-password-reset-request"],"/api/auth/password/update":[1,1,1,"post--api-auth-password-update"],"/api/auth/picture":[1,1,1,"post--api-auth-picture"],"/api/auth/profile":[1,0,1,"get--api-auth-profile"],"/api/auth/profile/edit":[1,1,1,"post--api-auth-profile-edit"],"/api/auth/register":[1,1,1,"post--api-auth-register"],"/api/config":[2,3,1,"patch--api-config"],"/api/ping":[2,0,1,"get--api-ping"],"/api/records":[4,0,1,"get--api-records"],"/api/sports":[5,0,1,"get--api-sports"],"/api/sports/(int:sport_id)":[5,3,1,"patch--api-sports-(int-sport_id)"],"/api/stats/(user_name)/by_sport":[6,0,1,"get--api-stats-(user_name)-by_sport"],"/api/stats/(user_name)/by_time":[6,0,1,"get--api-stats-(user_name)-by_time"],"/api/stats/all":[6,0,1,"get--api-stats-all"],"/api/users":[7,0,1,"get--api-users"],"/api/users/(user_name)":[7,3,1,"patch--api-users-(user_name)"],"/api/users/(user_name)/picture":[7,0,1,"get--api-users-(user_name)-picture"],"APP_LOG \ud83c\udd95":[11,4,1,"envvar-APP_LOG"],"DATABASE_DISABLE_POOLING \ud83c\udd95":[11,4,1,"envvar-DATABASE_DISABLE_POOLING"],"MAP_ATTRIBUTION \ud83c\udd95":[11,4,1,"envvar-MAP_ATTRIBUTION"],"TILE_SERVER_URL \ud83c\udd95":[11,4,1,"envvar-TILE_SERVER_URL"],"UPLOAD_FOLDER \ud83c\udd95":[11,4,1,"envvar-UPLOAD_FOLDER"],APP_SECRET_KEY:[11,4,1,"-"],APP_SETTINGS:[11,4,1,"-"],APP_WORKERS:[11,4,1,"-"],DATABASE_URL:[11,4,1,"-"],EMAIL_URL:[11,4,1,"-"],FLASK_APP:[11,4,1,"-"],HOST:[11,4,1,"-"],PORT:[11,4,1,"-"],REACT_APP_ALLOW_REGISTRATION:[11,4,1,"-"],REACT_APP_API_URL:[11,4,1,"-"],REACT_APP_GPX_LIMIT_IMPORT:[11,4,1,"-"],REACT_APP_MAX_SINGLE_FILE_SIZE:[11,4,1,"-"],REACT_APP_MAX_ZIP_FILE_SIZE:[11,4,1,"-"],REACT_APP_THUNDERFOREST_API_KEY:[11,4,1,"-"],REDIS_URL:[11,4,1,"-"],SENDER_EMAIL:[11,4,1,"-"],UI_URL:[11,4,1,"-"],WEATHER_API_KEY:[11,4,1,"-"],WORKERS_PROCESSES:[11,4,1,"-"]},"/api/activities/map_tile/(s)/(z)/(x)/(y)":{png:[0,0,1,"get--api-activities-map_tile-(s)-(z)-(x)-(y).png"]}},objnames:{"0":["http","get","HTTP get"],"1":["http","post","HTTP post"],"2":["http","delete","HTTP delete"],"3":["http","patch","HTTP patch"],"4":["std","envvar","environment variable"]},objtypes:{"0":"http:get","1":"http:post","2":"http:delete","3":"http:patch","4":"std:envvar"},terms:{"0mb":[0,1],"1000":6,"1048576":2,"10485760":2,"10mb":11,"1232004":0,"12341":6,"1234538":0,"1267":6,"127":11,"1563529507772":0,"1mb":11,"200":[0,1,2,4,5,6,7],"201":[0,1],"2017":[0,6],"2018":[0,6,10],"2019":[0,1,4,6,7,10],"2020":10,"204":[0,1,7],"279":0,"280":0,"282":6,"2930":0,"2e1ee2c":8,"3000":[11,12],"301":11,"34614d5":8,"400":[0,1,2,5],"401":[0,1,2,4,5,6,7],"403":[0,1,2,5,6,7],"404":[0,1,5,6,7],"4109":0,"413":[0,1],"443":11,"465":11,"4c3fc34":8,"500":[0,1,2,5,7],"5000":11,"5078118":0,"5079733":0,"5432":11,"587":11,"613":6,"7380":0,"895":[1,7],"9960":6,"boolean":[2,7],"default":[0,6,7,8,11],"export":10,"float":0,"import":11,"int":[0,5],"new":[9,11],"null":[0,1,7],"return":[0,4,11],"short":0,"static":11,"true":[1,2,5,7,11],"try":[0,1,7],For:11,NOT:[0,1,5],Not:[0,5,6,7],One:11,That:1,The:[8,9,11],There:11,Use:8,WITH:11,With:11,__main__:11,_blank:11,accord:10,account:[7,8],acit:0,activ:[3,5,6,8,10,11],activities_count:7,activity_d:[0,4],activity_id:[0,4],activity_short_id:0,adapt:11,add:[7,8,9,10],address:11,admin:[0,1,2,4,5,6,7,8,11],administr:[0,1,7,10,13],after:11,again:[0,1,2,4,5,6,7],all:[4,5,6,7,11],allow:[0,1,2,10,11],along:0,alpinequest:10,alreadi:1,also:10,alwai:11,android:10,anoth:[7,11],anymor:8,apach:10,api:[0,1,2,4,5,6,7,8,9,10,11,12],apikei:11,app:10,app_log:11,app_secret_kei:11,app_set:11,app_work:11,applic:[0,1,2,4,5,6,7,8,9,10,11],arch:11,archiv:[2,9,11],archlinux:11,asc:7,ascent:0,attribut:11,auth:[0,1,2,4,5,6,7],auth_token:1,auth_user_id:[0,2,4,5,6,7],authent:[0,2,3,4,5,6,7,10],author:[0,1,2,4,5,6,7],avail:[4,9,10],ave_spe:0,ave_speed_from:0,ave_speed_to:0,averag:[0,4,8,9],axi:0,b862a77:8,background:8,backup:11,bad:[0,1,2,5],bearer:[0,1,2,4,5,6,7],becom:8,been:8,befor:11,bike:[0,5,8,9],bin:11,bio:[1,7],biographi:1,birth:1,birth_dat:[1,7],bound:0,by_sport:6,by_tim:6,calcul:8,calendar:[8,9],can:[7,8,9,10,11],cannot:8,chang:[9,10,11],charact:[1,13],chart:[0,8,9,11],chart_data:0,check:[2,11,12],choos:8,client:[8,11,12],clone:11,code:[0,1,2,4,5,6,7],color:8,column:13,com:[1,7,11],complet:8,config:[2,11,12],configur:[3,10,11],confirm:1,contact:[0,1,7],contain:[8,11],content:[0,1,2,4,5,6,7],contributor:[2,11],coordin:11,copi:[2,11],copyright:[2,11],correctli:[8,12],creat:[0,1,8,9,11],create_app:11,created_at:[1,7],creation:[8,9],creation_d:0,credenti:[1,11],criteria:7,current:8,custom:[11,12],cycl:[5,8,9],dai:8,dark:11,darkski:[8,9],dashboard:[8,10],data:[0,1,2,4,5,6,7,8,10,11,13],databas:[8,11],database_disable_pool:11,database_url:11,date:[0,1,6,8,9],debian:11,defin:9,definit:8,delet:[0,1,7,8,9],depend:[8,11],deploy:10,desc:0,descent:0,describ:11,descript:11,detail:[7,8,10],develop:[10,11],differ:8,directli:11,directori:11,disabl:[1,8,9,11],displai:[0,8,9,10,11],distanc:[0,4,8,9],distance_from:0,distance_to:0,distribut:11,docker:11,document:[8,10,11],doe:[0,1,6,7,8],don:1,down:8,download:11,dramatiq:11,drop:8,durat:[0,4,8,9],duration_from:0,duration_to:0,dure:[0,1],easi:8,edit:[1,8,9],elev:[0,8,9,11],els:11,email:[1,7,8],email_url:11,empti:8,enabl:[2,9],encount:11,end:[0,6],endpoint:[2,3,13],english:9,enter:[8,9],entiti:[0,1],entri:11,env:11,environ:[8,10],environn:12,error:[0,1,2,5,7,8,11],europ:[1,7],even:[8,9],exampl:[0,1,2,4,5,6,7,10,11,12],exce:[0,1],except:7,execstart:11,exist:[0,1,6,7,9,10,12],exodu:10,expir:[0,1,2,4,5,6,7],extens:[0,1],fa33f4d996844a5c73ecd1ae24456ab8:0,fals:[0,1,2,5,7,11],farest:[4,8,9],featur:[10,11],fetch:11,file:[0,1,2,8,9,10,11,12],filter:[8,9],first:[1,10],first_nam:[1,7],fitotrack:10,fittracke:[9,11],fittrackee_init_data:11,fittrackee_upgrade_db:11,fittrackee_work:11,fix:10,flask:11,flask_app:11,flaticon:11,follow:[4,9,11],forbidden:[0,1,2,5,6,7],forecast:11,form:[0,1],format:[0,1,6],former:11,forrunn:10,forward:11,found:[0,1,5,6,7],frame:6,freepik:11,french:[9,10],fri:0,from:[0,2,4,5,6,7,8,10],fullchain:11,gener:11,get:[0,1,2,4,5,6,7],gif:1,git:11,github:11,given:11,gmt:[0,1,4,7],gpl:10,gpx:[0,8,9,10,11],gpx_limit_import:2,gpxpy:11,grant:11,gunicorn:11,handl:1,has:7,has_act:5,have:[0,2,5,6,7,8],header:[0,1,2,4,5,6,7],health:2,heavi:[10,11],hike:[5,8,9],his:[7,8,9],home:11,host:11,href:[2,11],http:[0,1,2,4,5,6,7,11,12],hvybqybra7wwxpastwr4v2:4,i18n:8,icon:11,imag:[0,1,7,11],img:5,improv:10,incorrect:8,index:0,info:1,inform:[8,10,11],initi:[11,12],instal:[8,10],instanc:[2,11],integ:[0,2,4,5,6,7],integr:2,interceptor:8,intern:[0,1,2,5,7],introduc:8,invalid:[0,1,2,4,5,6,7],is_act:5,is_registration_en:2,issu:[10,11],jan:0,javascript:11,john_do:7,jpeg:7,jpg:1,json:[0,1,2,4,5,6,7,13],jul:[0,1,4,7],keep:[10,11],kei:[8,9,11],kjxavsturjvoah2wvcegef:0,label:5,languag:[1,7],larg:[0,1],last:[1,11],last_nam:[1,7],latitud:0,layer:[8,11],leaflet:[0,11],least:0,librari:11,licens:10,limit:[8,9],line:13,link:11,linux:11,list:[8,10,11],listen:11,load:8,local:[8,10,11],localhost:[11,12],locat:[1,7,11],log:[0,1,2,4,5,6,7,10,11],logfil:11,login:1,logout:[1,8],longest:[4,8,9],longitud:0,made:[11,12],mai:[10,11],mailhog:11,major:8,make:11,makefil:[11,12],manag:8,mandatori:[0,8,11],map:[0,8,9,10],map_attribut:[2,11],map_id:0,map_til:0,match:1,max:[0,2,7],max_alt:0,max_single_file_s:2,max_spe:0,max_speed_from:0,max_speed_to:0,max_us:2,max_zip_file_s:2,maxim:0,maximum:[4,8,9,11],mean:12,messag:[0,1,2],method:11,min_alt:0,minim:0,minor:10,mobil:10,modifi:7,modification_d:0,modul:11,mon:0,mondai:[1,6,9],montain:[8,9],month:[6,8,9],more:[8,10,11],morn:0,mountain:5,mous:8,move:[0,8],mpwoadmin:11,multi:11,multipart:[0,1],must:[1,2,5,8,9,11],name:[1,6,7,11],nano:11,nb_activ:[1,6,7],nb_sport:[1,7],necessari:11,need:11,network:[11,12],next_act:0,nginx:11,no_gpx:0,non:5,noopen:11,noreferr:11,note:[0,8,9,11],now:[8,9,11],number:[0,2,7,9,11],oauth:[0,1,2,4,5,6,7],object:[0,1,2,5,7],one:[0,7],onli:[0,7,8,9,11],open:[10,11],openstreetmap:[2,8,11],opentrack:10,option:11,order:[0,7,8],order_bi:7,org:[2,11],other:[7,11],out:1,outdoor:[8,10,11],over:8,own:[7,10],owner:[8,9],packag:[8,11],page:[0,7],pagin:[0,7],par_pag:7,paramet:[0,1,2,4,5,6,7,8,9,11],pari:[1,7],pars:[11,13],part:[0,1],pass:11,password:[1,8,9,11],password_conf:1,patch:[0,2,5,7],path:11,paus:[0,8],payload:[0,1,2,5],pem:11,per:[0,7],per_pag:[0,7],permiss:[0,2,5,6,7],pg_dump:11,pictur:[0,1,7,11],ping:2,pip:11,pipenv:8,pleas:[0,1,2,4,5,6,7,8],png:[0,1,5,11],poetri:[8,11],point:[8,11],pong:2,pool:11,port:11,possibl:[8,10],post:[0,1],postgr:11,postgresql:11,prefer:1,prerequisit:10,previous_act:0,prior:11,privai:10,privileg:11,privkei:11,process:[1,11],productionconfig:11,profil:1,project:11,proprietari:10,provid:[0,1,2,4,5,6,7,8,9,11],proxi:11,proxy_add_x_forwarded_for:11,proxy_pass:11,proxy_redirect:11,proxy_set_head:11,pull:11,pwd:11,pypi:10,python:[8,11],queri:[0,6,7],queue:11,react:11,react_app_allow_registr:11,react_app_api_url:[11,12],react_app_gpx_limit_import:11,react_app_max_single_file_s:11,react_app_max_zip_file_s:11,react_app_thunderforest_api_kei:11,read:8,real:11,rebuild:12,rechart:[0,11],recommend:11,record:[0,3,8,9,10],record_typ:[0,4],redi:[8,11],redis_url:11,redux:11,regist:[1,2,11],registr:[1,2,8,9],rel:11,relat:11,releas:[10,11],remote_addr:11,remov:9,renam:8,replac:[8,11],repo:11,report:10,repositori:11,request:[0,1,2,4,5,6,7,12],request_uri:11,requir:1,reset:[1,8,9],respons:[0,1,2,4,5,6,7],restart:11,restartsec:11,right:[7,9],rout:8,run:[5,8,9,11],runner:10,sam:[1,7],same:8,samr1:11,sat:7,save:9,schema:11,search:8,second:0,secret:11,see:[8,9,10,11,12],segment:[0,8,9],segment_id:0,select:[0,1],send:[8,11],sender:11,sender_email:11,serv:11,server:[0,1,2,5,7,8,10],server_nam:11,servic:11,set:9,sever:[10,11],should:11,show:8,side:8,signatur:[0,1,2,4,5,6,7],simpl:11,simplifi:8,sinc:11,singl:[2,7],size:[0,1,2,8,9,11],sky:11,smtp:11,some:[0,7,8,10,11],sorri:1,sort:[0,7],sourc:10,spawn:11,speed:[0,4,8,9,11],spinner:8,sport:[0,3,6,8,9,10,11],sport_id:[0,4,5,6],sports_list:[1,7],sql:11,sqlalchemi:11,ssl:11,ssl_certif:11,ssl_certificate_kei:11,standard:[8,11],standarderror:11,standardoutput:11,start:[0,1,6,9,11],startlimitintervalsec:11,starttl:11,stat:[6,8],staticmap:11,statist:[3,10],statu:[0,1,2,4,5,6,7],step:11,still:10,stop:11,store:[10,11],street:10,string:[0,1,5,6,7],subdomain:0,success:[0,1,2,4,5,6,7],successfulli:1,sun:[0,1,4,7],sundai:[0,6,9],support:[8,9],syslog:11,syslogidentifi:11,system:11,systemd:11,tab:12,tar:11,target:11,task:11,term:11,test:11,than:8,thei:11,them:10,thi:[0,8,9,10,11],thunderforest:[8,11],tile:[0,8],tile_server_url:11,time:[0,1,6,8,9],timezon:[1,7,8],titl:0,tls:11,todo:14,token:[0,1,2,4,5,6,7],too:[0,1],tooltip:8,total:8,total_dist:[1,6,7],total_dur:[1,6,7],track:10,tracker:10,transport:[5,8,9],troubleshoot:10,type:[0,1,2,4,5,6,7,11],ui_url:11,unauthor:[0,1,2,4,5,6,7],undefin:12,under:[10,11],unencrypt:11,unexpect:13,unit:11,unstabl:[10,11],updat:[0,1,2,5,7,8,9,11],upload:[8,9,11],upload_fold:11,uploads_dir_s:6,url:[8,11],use:11,used:11,user:[0,1,2,3,4,5,6,8,9,10,11,13],user_nam:[6,7],usernam:[1,7,11],usernanm:1,using:[0,7,10,11],uuid:[0,8],valid:[0,1,2,4,5,6,7,11],valu:[0,4],variabl:[8,10,12],venv:11,version:[10,11],view:8,virtualenv:11,wai:11,walk:[5,8,9],wantedbi:11,warn:8,weather:[8,9,11],weather_api:11,weather_api_kei:11,weather_end:0,weather_start:0,web:[0,1,2,4,5,6,7,10,11],week:[1,6,8,9],weekend:8,weekm:[1,6],were:8,wget:11,when:[8,11],where:11,which:9,with_gpx:0,without:[0,6,7,8,9,10],worker:11,workers_process:11,workingdirectori:11,workout:10,written:11,www:[2,11],xxxx:11,xzf:11,yai:11,yarn:11,year:6,yet:10,you:[0,2,5,6,7,10],your:[7,10,11],zip:[0,2,9,11],zone:1,zoom:0},titles:["Activities","Authentication","Configuration","API documentation","Records","Sports","Statistics","Users","Change log","Features","FitTrackee","Installation","Administrator","Troubleshooting","User"],titleterms:{"2018":8,"2019":8,"2020":8,"new":8,account:9,activ:[0,9],administr:[8,9,12],api:3,authent:1,avail:8,bug:8,chang:8,charact:12,close:8,column:12,configur:2,content:10,dashboard:9,data:12,deploy:11,deprec:11,detail:9,dev:11,document:3,email:11,environ:11,featur:[8,9],first:8,fittracke:[8,10],fix:8,french:8,from:11,improv:8,instal:11,issu:8,json:12,line:12,list:9,log:8,map:11,minor:8,misc:8,pars:12,prerequisit:11,prod:11,product:11,pypi:[8,11],record:4,releas:8,server:11,sourc:11,sport:5,statist:[6,8,9],tabl:10,tile:11,translat:9,troubleshoot:13,unexpect:12,upgrad:11,user:[7,14],variabl:11,version:8,workout:9}}) \ No newline at end of file diff --git a/fittrackee/activities/activities.py b/fittrackee/activities/activities.py index 6061e2e2..f80953f7 100644 --- a/fittrackee/activities/activities.py +++ b/fittrackee/activities/activities.py @@ -5,7 +5,15 @@ from datetime import datetime, timedelta import requests from fittrackee import appLog, db -from flask import Blueprint, Response, current_app, jsonify, request, send_file +from fittrackee.responses import ( + DataInvalidPayloadErrorResponse, + DataNotFoundErrorResponse, + InternalServerErrorResponse, + InvalidPayloadErrorResponse, + NotFoundErrorResponse, + handle_error_and_return_response, +) +from flask import Blueprint, Response, current_app, request, send_file from sqlalchemy import exc from ..users.utils import ( @@ -251,7 +259,7 @@ def get_activities(auth_user_id): .paginate(page, per_page, False) .items ) - response_object = { + return { 'status': 'success', 'data': { 'activities': [ @@ -259,15 +267,8 @@ def get_activities(auth_user_id): ] }, } - code = 200 except Exception as e: - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - code = 500 - return jsonify(response_object), code + return handle_error_and_return_response(e) @activities_blueprint.route( @@ -360,27 +361,17 @@ def get_activity(auth_user_id, activity_short_id): """ activity_uuid = decode_short_id(activity_short_id) activity = Activity.query.filter_by(uuid=activity_uuid).first() - activities_list = [] + if not activity: + return DataNotFoundErrorResponse('activities') - if activity: - response_object, code = can_view_activity( - auth_user_id, activity.user_id - ) - if response_object: - return jsonify(response_object), code + error_response = can_view_activity(auth_user_id, activity.user_id) + if error_response: + return error_response - activities_list.append(activity.serialize()) - status = 'success' - code = 200 - else: - status = 'not found' - code = 404 - - response_object = { - 'status': status, - 'data': {'activities': activities_list}, + return { + 'status': 'success', + 'data': {'activities': [activity.serialize()]}, } - return jsonify(response_object), code def get_activity_data( @@ -389,59 +380,44 @@ def get_activity_data( """Get data from an activity gpx file""" activity_uuid = decode_short_id(activity_short_id) activity = Activity.query.filter_by(uuid=activity_uuid).first() - content = '' - if activity: - response_object, code = can_view_activity( - auth_user_id, activity.user_id + if not activity: + return DataNotFoundErrorResponse( + data_type=data_type, + message=f'Activity not found (id: {activity_short_id})', ) - if response_object: - return jsonify(response_object), code - if not activity.gpx or activity.gpx == '': - message = ( - f'No gpx file for this activity (id: {activity_short_id})' - ) - response_object = {'status': 'error', 'message': message} - return jsonify(response_object), 404 - try: - absolute_gpx_filepath = get_absolute_file_path(activity.gpx) - if data_type == 'chart': - content = get_chart_data(absolute_gpx_filepath, segment_id) - else: # data_type == 'gpx' - with open(absolute_gpx_filepath, encoding='utf-8') as f: - content = f.read() - if segment_id is not None: - content = extract_segment_from_gpx_file( - content, segment_id - ) - except ActivityGPXException as e: - appLog.error(e.message) - response_object = {'status': e.status, 'message': e.message} - code = 404 if e.status == 'not found' else 500 - return jsonify(response_object), code - except Exception as e: - appLog.error(e) - response_object = {'status': 'error', 'message': 'internal error'} - return jsonify(response_object), 500 + error_response = can_view_activity(auth_user_id, activity.user_id) + if error_response: + return error_response + if not activity.gpx or activity.gpx == '': + return NotFoundErrorResponse( + f'No gpx file for this activity (id: {activity_short_id})' + ) - status = 'success' - message = '' - code = 200 - else: - status = 'not found' - message = f'Activity not found (id: {activity_short_id})' - code = 404 + try: + absolute_gpx_filepath = get_absolute_file_path(activity.gpx) + if data_type == 'chart_data': + content = get_chart_data(absolute_gpx_filepath, segment_id) + else: # data_type == 'gpx' + with open(absolute_gpx_filepath, encoding='utf-8') as f: + content = f.read() + if segment_id is not None: + content = extract_segment_from_gpx_file( + content, segment_id + ) + except ActivityGPXException as e: + appLog.error(e.message) + if e.status == 'not found': + return NotFoundErrorResponse(e.message) + return InternalServerErrorResponse(e.message) + except Exception as e: + return handle_error_and_return_response(e) - response_object = { - 'status': status, - 'message': message, - 'data': ( - {'chart_data': content} - if data_type == 'chart' - else {'gpx': content} - ), + return { + 'status': 'success', + 'message': '', + 'data': ({data_type: content}), } - return jsonify(response_object), code @activities_blueprint.route( @@ -558,7 +534,7 @@ def get_activity_chart_data(auth_user_id, activity_short_id): :statuscode 500: """ - return get_activity_data(auth_user_id, activity_short_id, 'chart') + return get_activity_data(auth_user_id, activity_short_id, 'chart_data') @activities_blueprint.route( @@ -681,7 +657,7 @@ def get_segment_chart_data(auth_user_id, activity_short_id, segment_id): """ return get_activity_data( - auth_user_id, activity_short_id, 'chart', segment_id + auth_user_id, activity_short_id, 'chart_data', segment_id ) @@ -718,18 +694,11 @@ def get_map(map_id): try: activity = Activity.query.filter_by(map_id=map_id).first() if not activity: - response_object = { - 'status': 'error', - 'message': 'Map does not exist', - } - return jsonify(response_object), 404 - else: - absolute_map_filepath = get_absolute_file_path(activity.map) - return send_file(absolute_map_filepath) + return NotFoundErrorResponse('Map does not exist.') + absolute_map_filepath = get_absolute_file_path(activity.map) + return send_file(absolute_map_filepath) except Exception as e: - appLog.error(e) - response_object = {'status': 'error', 'message': 'internal error.'} - return jsonify(response_object), 500 + return handle_error_and_return_response(e) @activities_blueprint.route( @@ -887,16 +856,13 @@ def post_activity(auth_user_id): :statuscode 500: """ - response_object, response_code = verify_extension_and_size( - 'activity', request - ) - if response_object['status'] != 'success': - return jsonify(response_object), response_code + error_response = verify_extension_and_size('activity', request) + if error_response: + return error_response activity_data = json.loads(request.form['data']) if not activity_data or activity_data.get('sport_id') is None: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() activity_file = request.files['file'] upload_dir = os.path.join( @@ -921,20 +887,19 @@ def post_activity(auth_user_id): ] }, } - code = 201 else: - response_object = {'status': 'fail', 'data': {'activities': []}} - code = 400 + return DataInvalidPayloadErrorResponse('activities', 'fail') except ActivityException as e: db.session.rollback() if e.e: appLog.error(e.e) - response_object = {'status': e.status, 'message': e.message} - code = 500 if e.status == 'error' else 400 + if e.status == 'error': + return InternalServerErrorResponse(e.message) + return InvalidPayloadErrorResponse(e.message) shutil.rmtree(folders['extract_dir'], ignore_errors=True) shutil.rmtree(folders['tmp_dir'], ignore_errors=True) - return jsonify(response_object), code + return response_object, 201 @activities_blueprint.route('/activities/no_gpx', methods=['POST']) @@ -1059,8 +1024,7 @@ def post_activity_no_gpx(auth_user_id): or activity_data.get('distance') is None or activity_data.get('activity_date') is None ): - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() try: user = User.query.filter_by(id=auth_user_id).first() @@ -1068,20 +1032,21 @@ def post_activity_no_gpx(auth_user_id): db.session.add(new_activity) db.session.commit() - response_object = { - 'status': 'created', - 'data': {'activities': [new_activity.serialize()]}, - } - return jsonify(response_object), 201 + return ( + { + 'status': 'created', + 'data': {'activities': [new_activity.serialize()]}, + }, + 201, + ) except (exc.IntegrityError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'fail', - 'message': 'Error during activity save.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response( + error=e, + message='Error during activity save.', + status='fail', + db=db, + ) @activities_blueprint.route( @@ -1207,41 +1172,27 @@ def update_activity(auth_user_id, activity_short_id): """ activity_data = request.get_json() if not activity_data: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() try: activity_uuid = decode_short_id(activity_short_id) activity = Activity.query.filter_by(uuid=activity_uuid).first() - if activity: - response_object, code = can_view_activity( - auth_user_id, activity.user_id - ) - if response_object: - return jsonify(response_object), code + if not activity: + return DataNotFoundErrorResponse('activities') - activity = edit_activity(activity, activity_data, auth_user_id) - db.session.commit() - response_object = { - 'status': 'success', - 'data': {'activities': [activity.serialize()]}, - } - code = 200 - else: - response_object = { - 'status': 'not found', - 'data': {'activities': []}, - } - code = 404 - except (exc.IntegrityError, exc.OperationalError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', + response_object = can_view_activity(auth_user_id, activity.user_id) + if response_object: + return response_object + + activity = edit_activity(activity, activity_data, auth_user_id) + db.session.commit() + return { + 'status': 'success', + 'data': {'activities': [activity.serialize()]}, } - code = 500 - return jsonify(response_object), code + + except (exc.IntegrityError, exc.OperationalError, ValueError) as e: + return handle_error_and_return_response(e) @activities_blueprint.route( @@ -1284,34 +1235,19 @@ def delete_activity(auth_user_id, activity_short_id): try: activity_uuid = decode_short_id(activity_short_id) activity = Activity.query.filter_by(uuid=activity_uuid).first() - if activity: - response_object, code = can_view_activity( - auth_user_id, activity.user_id - ) - if response_object: - return jsonify(response_object), code + if not activity: + return DataNotFoundErrorResponse('activities') + error_response = can_view_activity(auth_user_id, activity.user_id) + if error_response: + return error_response - db.session.delete(activity) - db.session.commit() - response_object = {'status': 'no content'} - code = 204 - else: - response_object = { - 'status': 'not found', - 'data': {'activities': []}, - } - code = 404 + db.session.delete(activity) + db.session.commit() + return {'status': 'no content'}, 204 except ( exc.IntegrityError, exc.OperationalError, ValueError, OSError, ) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - code = 500 - return jsonify(response_object), code + return handle_error_and_return_response(e, db=db) diff --git a/fittrackee/activities/records.py b/fittrackee/activities/records.py index 48cb585b..c25320b5 100644 --- a/fittrackee/activities/records.py +++ b/fittrackee/activities/records.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify +from flask import Blueprint from ..users.utils import authenticate from .models import Record @@ -103,14 +103,12 @@ def get_records(auth_user_id): - Invalid token. Please log in again. """ - records = ( Record.query.filter_by(user_id=auth_user_id) .order_by(Record.sport_id.asc(), Record.record_type.asc()) .all() ) - response_object = { + return { 'status': 'success', 'data': {'records': [record.serialize() for record in records]}, } - return jsonify(response_object), 200 diff --git a/fittrackee/activities/sports.py b/fittrackee/activities/sports.py index c8d9bf9c..c85ea7ed 100644 --- a/fittrackee/activities/sports.py +++ b/fittrackee/activities/sports.py @@ -1,5 +1,10 @@ -from fittrackee import appLog, db -from flask import Blueprint, jsonify, request +from fittrackee import db +from fittrackee.responses import ( + DataNotFoundErrorResponse, + InvalidPayloadErrorResponse, + handle_error_and_return_response, +) +from flask import Blueprint, request from sqlalchemy import exc from ..users.models import User @@ -143,14 +148,12 @@ def get_sports(auth_user_id): - Invalid token. Please log in again. """ - user = User.query.filter_by(id=int(auth_user_id)).first() sports = Sport.query.order_by(Sport.id).all() - response_object = { + return { 'status': 'success', 'data': {'sports': [sport.serialize(user.admin) for sport in sports]}, } - return jsonify(response_object), 200 @sports_blueprint.route('/sports/', methods=['GET']) @@ -238,19 +241,14 @@ def get_sport(auth_user_id, sport_id): :statuscode 404: sport not found """ - user = User.query.filter_by(id=int(auth_user_id)).first() sport = Sport.query.filter_by(id=sport_id).first() if sport: - response_object = { + return { 'status': 'success', 'data': {'sports': [sport.serialize(user.admin)]}, } - code = 200 - else: - response_object = {'status': 'not found', 'data': {'sports': []}} - code = 404 - return jsonify(response_object), code + return DataNotFoundErrorResponse('sports') @sports_blueprint.route('/sports/', methods=['PATCH']) @@ -325,28 +323,19 @@ def update_sport(auth_user_id, sport_id): """ sport_data = request.get_json() if not sport_data or sport_data.get('is_active') is None: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() try: sport = Sport.query.filter_by(id=sport_id).first() - if sport: - sport.is_active = sport_data.get('is_active') - db.session.commit() - response_object = { - 'status': 'success', - 'data': {'sports': [sport.serialize(True)]}, - } - code = 200 - else: - response_object = {'status': 'not found', 'data': {'sports': []}} - code = 404 - except (exc.IntegrityError, exc.OperationalError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', + if not sport: + return DataNotFoundErrorResponse('sports') + + sport.is_active = sport_data.get('is_active') + db.session.commit() + return { + 'status': 'success', + 'data': {'sports': [sport.serialize(True)]}, } - code = 500 - return jsonify(response_object), code + + except (exc.IntegrityError, exc.OperationalError, ValueError) as e: + return handle_error_and_return_response(e, db=db) diff --git a/fittrackee/activities/stats.py b/fittrackee/activities/stats.py index 41c8d980..4e8087fc 100644 --- a/fittrackee/activities/stats.py +++ b/fittrackee/activities/stats.py @@ -1,7 +1,13 @@ from datetime import datetime, timedelta -from fittrackee import appLog, db -from flask import Blueprint, jsonify, request +from fittrackee import db +from fittrackee.responses import ( + InvalidPayloadErrorResponse, + NotFoundErrorResponse, + UserNotFoundErrorResponse, + handle_error_and_return_response, +) +from flask import Blueprint, request from sqlalchemy import func from ..users.models import User @@ -17,11 +23,7 @@ def get_activities(user_name, filter_type): try: user = User.query.filter_by(username=user_name).first() if not user: - response_object = { - 'status': 'not found', - 'message': 'User does not exist.', - } - return jsonify(response_object), 404 + return UserNotFoundErrorResponse() params = request.args.copy() date_from = params.get('from') @@ -42,11 +44,7 @@ def get_activities(user_name, filter_type): if sport_id: sport = Sport.query.filter_by(id=sport_id).first() if not sport: - response_object = { - 'status': 'not found', - 'message': 'Sport does not exist.', - } - return jsonify(response_object), 404 + return NotFoundErrorResponse('Sport does not exist.') activities = ( Activity.query.filter( @@ -103,11 +101,9 @@ def get_activities(user_name, filter_type): activity.activity_date, "%Y" ) else: - response_object = { - 'status': 'fail', - 'message': 'Invalid time period.', - } - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse( + 'Invalid time period.', 'fail' + ) sport_id = activity.sport_id if time_period not in activities_list: activities_list[time_period] = {} @@ -125,19 +121,12 @@ def get_activities(user_name, filter_type): 'total_duration' ] += convert_timedelta_to_integer(activity.moving) - response_object = { + return { 'status': 'success', 'data': {'statistics': activities_list}, } - code = 200 except Exception as e: - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - code = 500 - return jsonify(response_object), code + return handle_error_and_return_response(e) @stats_blueprint.route('/stats//by_time', methods=['GET']) @@ -371,7 +360,7 @@ def get_application_stats(auth_user_id): .group_by(Activity.sport_id) .count() ) - response_object = { + return { 'status': 'success', 'data': { 'activities': nb_activities, @@ -380,4 +369,3 @@ def get_application_stats(auth_user_id): 'uploads_dir_size': get_upload_dir_size(), }, } - return jsonify(response_object), 200 diff --git a/fittrackee/application/app_config.py b/fittrackee/application/app_config.py index 64609546..3cc97bce 100644 --- a/fittrackee/application/app_config.py +++ b/fittrackee/application/app_config.py @@ -1,5 +1,9 @@ -from fittrackee import appLog, db -from flask import Blueprint, current_app, jsonify, request +from fittrackee import db +from fittrackee.responses import ( + InvalidPayloadErrorResponse, + handle_error_and_return_response, +) +from flask import Blueprint, current_app, request from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from ..users.utils import authenticate_as_admin @@ -46,15 +50,11 @@ def get_application_config(): try: config = AppConfig.query.one() - response_object = {'status': 'success', 'data': config.serialize()} - return jsonify(response_object), 200 + return {'status': 'success', 'data': config.serialize()} except (MultipleResultsFound, NoResultFound) as e: - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error on getting configuration.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response( + e, message='Error on getting configuration.' + ) @config_blueprint.route('/config', methods=['PATCH']) @@ -111,8 +111,7 @@ def update_application_config(auth_user_id): """ config_data = request.get_json() if not config_data: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() try: config = AppConfig.query.one() @@ -129,18 +128,12 @@ def update_application_config(auth_user_id): db.session.commit() update_app_config_from_database(current_app, config) - - response_object = {'status': 'success', 'data': config.serialize()} - code = 200 + return {'status': 'success', 'data': config.serialize()} except Exception as e: - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error on updating configuration.', - } - code = 500 - return jsonify(response_object), code + return handle_error_and_return_response( + e, message='Error on updating configuration.' + ) @config_blueprint.route('/ping', methods=['GET']) @@ -169,4 +162,4 @@ def health_check(): :statuscode 200: success """ - return jsonify({'status': 'success', 'message': 'pong!'}) + return {'status': 'success', 'message': 'pong!'} diff --git a/fittrackee/responses.py b/fittrackee/responses.py new file mode 100644 index 00000000..9ff343b1 --- /dev/null +++ b/fittrackee/responses.py @@ -0,0 +1,117 @@ +from json import dumps + +from fittrackee import appLog +from flask import Response + + +def get_empty_data_for_datatype(data_type): + return '' if data_type in ['gpx', 'chart_data'] else [] + + +class HttpResponse(Response): + def __init__( + self, + response=None, + status_code=None, + content_type=None, + ): + if isinstance(response, dict): + response = dumps(response) + content_type = ( + 'application/json' if content_type is None else content_type + ) + super().__init__( + response=response, + status=status_code, + content_type=content_type, + ) + + +class GenericErrorResponse(HttpResponse): + def __init__(self, status_code, message, status=None): + response = { + 'status': 'error' if status is None else status, + 'message': message, + } + super().__init__( + response=response, + status_code=status_code, + ) + + +class InvalidPayloadErrorResponse(GenericErrorResponse): + def __init__(self, message=None, status=None): + message = 'Invalid payload.' if message is None else message + super().__init__(status_code=400, message=message, status=status) + + +class DataInvalidPayloadErrorResponse(HttpResponse): + def __init__(self, data_type, status=None): + response = { + 'status': 'error' if status is None else status, + 'data': {data_type: get_empty_data_for_datatype(data_type)}, + } + super().__init__(response=response, status_code=400) + + +class UnauthorizedErrorResponse(GenericErrorResponse): + def __init__(self, message=None): + message = ( + 'Invalid token. Please request a new token.' + if message is None + else message + ) + super().__init__(status_code=401, message=message) + + +class ForbiddenErrorResponse(GenericErrorResponse): + def __init__(self, message=None): + message = ( + 'You do not have permissions.' if message is None else message + ) + super().__init__(status_code=403, message=message) + + +class NotFoundErrorResponse(GenericErrorResponse): + def __init__(self, message): + super().__init__(status_code=404, message=message, status='not found') + + +class UserNotFoundErrorResponse(NotFoundErrorResponse): + def __init__(self): + super().__init__(message='User does not exist.') + + +class DataNotFoundErrorResponse(HttpResponse): + def __init__(self, data_type, message=None): + response = { + 'status': 'not found', + 'data': {data_type: get_empty_data_for_datatype(data_type)}, + } + if message: + response['message'] = message + super().__init__(response=response, status_code=404) + + +class PayloadTooLargeErrorResponse(GenericErrorResponse): + def __init__(self, message): + super().__init__(status_code=413, message=message, status='fail') + + +class InternalServerErrorResponse(GenericErrorResponse): + def __init__(self, message=None, status=None): + message = ( + 'Error. Please try again or contact the administrator.' + if message is None + else message + ) + super().__init__(status_code=500, message=message, status=status) + + +def handle_error_and_return_response( + error, message=None, status=None, db=None +): + if db is not None: + db.session.rollback() + appLog.error(error) + return InternalServerErrorResponse(message=message, status=status) diff --git a/fittrackee/tests/activities/test_activities_api_0_get.py b/fittrackee/tests/activities/test_activities_api_0_get.py index 50d83f46..f327e09a 100644 --- a/fittrackee/tests/activities/test_activities_api_0_get.py +++ b/fittrackee/tests/activities/test_activities_api_0_get.py @@ -833,7 +833,7 @@ class TestGetActivity: data = json.loads(response.data.decode()) assert response.status_code == 404 - assert 'error' in data['status'] + assert 'not found' in data['status'] assert ( f'No gpx file for this activity (id: {activity_short_id})' in data['message'] @@ -860,7 +860,7 @@ class TestGetActivity: data = json.loads(response.data.decode()) assert response.status_code == 404 - assert 'error' in data['status'] + assert 'not found' in data['status'] assert ( f'No gpx file for this activity (id: {activity_short_id})' in data['message'] @@ -888,7 +888,10 @@ class TestGetActivity: data = json.loads(response.data.decode()) assert response.status_code == 500 assert 'error' in data['status'] - assert 'internal error' in data['message'] + assert ( + 'Error. Please try again or contact the administrator.' + in data['message'] + ) assert 'data' not in data def test_it_returns_500_on_getting_chart_data_if_an_activity_has_invalid_gpx_pathname( # noqa @@ -913,7 +916,10 @@ class TestGetActivity: data = json.loads(response.data.decode()) assert response.status_code == 500 assert 'error' in data['status'] - assert 'internal error' in data['message'] + assert ( + 'Error. Please try again or contact the administrator.' + in data['message'] + ) assert 'data' not in data def test_it_returns_404_if_activity_has_no_map(self, app, user_1): @@ -933,5 +939,5 @@ class TestGetActivity: data = json.loads(response.data.decode()) assert response.status_code == 404 - assert 'error' in data['status'] + assert 'not found' in data['status'] assert 'Map does not exist' in data['message'] diff --git a/fittrackee/tests/activities/test_activities_api_1_post.py b/fittrackee/tests/activities/test_activities_api_1_post.py index 97b4bffc..b492c6bc 100644 --- a/fittrackee/tests/activities/test_activities_api_1_post.py +++ b/fittrackee/tests/activities/test_activities_api_1_post.py @@ -843,7 +843,10 @@ class TestPostAndGetActivityWithGpx: assert response.status_code == 500 assert data['status'] == 'error' - assert data['message'] == 'internal error.' + assert ( + data['message'] + == 'Error. Please try again or contact the administrator.' + ) def test_it_gets_an_activity_created_with_gpx( self, app, user_1, sport_1_cycling, gpx_file diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index 3c1bdd86..03a79935 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -162,7 +162,7 @@ class TestUserRegistration: assert response.content_type == 'application/json' assert response.status_code == 400 - def test_it_returns_error_if_paylaod_is_invalid(self, app): + def test_it_returns_error_if_payload_is_invalid(self, app): client = app.test_client() response = client.post( '/api/auth/register', @@ -278,43 +278,49 @@ class TestUserRegistration: class TestUserLogin: def test_user_can_register(self, app, user_1): client = app.test_client() + response = client.post( '/api/auth/login', data=json.dumps(dict(email='test@test.com', password='12345678')), content_type='application/json', ) + + assert response.content_type == 'application/json' + assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'Successfully logged in.' assert data['auth_token'] - assert response.content_type == 'application/json' - assert response.status_code == 200 def test_it_returns_error_if_user_does_not_exists(self, app): client = app.test_client() + response = client.post( '/api/auth/login', data=json.dumps(dict(email='test@test.com', password='12345678')), content_type='application/json', ) + + assert response.content_type == 'application/json' + assert response.status_code == 401 data = json.loads(response.data.decode()) assert data['status'] == 'error' assert data['message'] == 'Invalid credentials.' - assert response.content_type == 'application/json' - assert response.status_code == 404 def test_it_returns_error_on_invalid_payload(self, app): client = app.test_client() + response = client.post( '/api/auth/login', data=json.dumps(dict()), content_type='application/json', ) + + assert response.content_type == 'application/json' + assert response.status_code == 400 data = json.loads(response.data.decode()) assert data['status'] == 'error' assert data['message'] == 'Invalid payload.' - assert response.content_type == 'application/json' - assert response.status_code == 400 def test_it_returns_error_if_password_is_invalid(self, app, user_1): client = app.test_client() @@ -325,11 +331,11 @@ class TestUserLogin: content_type='application/json', ) + assert response.content_type == 'application/json' + assert response.status_code == 401 data = json.loads(response.data.decode()) assert data['status'] == 'error' assert data['message'] == 'Invalid credentials.' - assert response.content_type == 'application/json' - assert response.status_code == 404 class TestUserLogout: diff --git a/fittrackee/tests/users/test_users_api.py b/fittrackee/tests/users/test_users_api.py index 49b59dea..d06e1411 100644 --- a/fittrackee/tests/users/test_users_api.py +++ b/fittrackee/tests/users/test_users_api.py @@ -111,7 +111,7 @@ class TestGetUser: data = json.loads(response.data.decode()) assert response.status_code == 404 - assert 'fail' in data['status'] + assert 'not found' in data['status'] assert 'User does not exist.' in data['message'] @@ -972,7 +972,7 @@ class TestGetUserPicture: data = json.loads(response.data.decode()) assert response.status_code == 404 - assert 'fail' in data['status'] + assert 'not found' in data['status'] assert 'User does not exist.' in data['message'] diff --git a/fittrackee/users/auth.py b/fittrackee/users/auth.py index 54dd2c1a..9745b672 100644 --- a/fittrackee/users/auth.py +++ b/fittrackee/users/auth.py @@ -3,8 +3,15 @@ import os import jwt from fittrackee import appLog, bcrypt, db +from fittrackee.responses import ( + ForbiddenErrorResponse, + InvalidPayloadErrorResponse, + PayloadTooLargeErrorResponse, + UnauthorizedErrorResponse, + handle_error_and_return_response, +) from fittrackee.tasks import reset_password_email -from flask import Blueprint, current_app, jsonify, request +from flask import Blueprint, current_app, request from sqlalchemy import exc, or_ from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.utils import secure_filename @@ -84,11 +91,8 @@ def register_user(): """ if not current_app.config.get('is_registration_enabled'): - response_object = { - 'status': 'error', - 'message': 'Error. Registration is disabled.', - } - return jsonify(response_object), 403 + return ForbiddenErrorResponse('Error. Registration is disabled.') + # get post data post_data = request.get_json() if ( @@ -98,8 +102,7 @@ def register_user(): or post_data.get('password') is None or post_data.get('password_conf') is None ): - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() username = post_data.get('username') email = post_data.get('email') password = post_data.get('password') @@ -108,53 +111,36 @@ def register_user(): try: ret = register_controls(username, email, password, password_conf) except TypeError as e: - db.session.rollback() - appLog.error(e) + return handle_error_and_return_response(e, db=db) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - return jsonify(response_object), 500 if ret != '': - response_object = {'status': 'error', 'message': ret} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse(ret) try: # check for existing user user = User.query.filter( or_(User.username == username, User.email == email) ).first() - if not user: - # add new user to db - new_user = User(username=username, email=email, password=password) - new_user.timezone = 'Europe/Paris' - db.session.add(new_user) - db.session.commit() - # generate auth token - auth_token = new_user.encode_auth_token(new_user.id) - response_object = { - 'status': 'success', - 'message': 'Successfully registered.', - 'auth_token': auth_token, - } - return jsonify(response_object), 201 - else: - response_object = { - 'status': 'error', - 'message': 'Sorry. That user already exists.', - } - return jsonify(response_object), 400 + if user: + return InvalidPayloadErrorResponse( + 'Sorry. That user already exists.' + ) + + # add new user to db + new_user = User(username=username, email=email, password=password) + new_user.timezone = 'Europe/Paris' + db.session.add(new_user) + db.session.commit() + # generate auth token + auth_token = new_user.encode_auth_token(new_user.id) + return { + 'status': 'success', + 'message': 'Successfully registered.', + 'auth_token': auth_token, + }, 201 # handler errors except (exc.IntegrityError, exc.OperationalError, ValueError) as e: - db.session.rollback() - appLog.error(e) - - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response(e, db=db) @auth_blueprint.route('/auth/login', methods=['POST']) @@ -200,15 +186,15 @@ def login_user(): := user_mandatory_data: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() + first_name = post_data.get('first_name') last_name = post_data.get('last_name') bio = post_data.get('bio') @@ -471,8 +438,7 @@ def edit_user(auth_user_id): if password is not None and password != '': message = check_passwords(password, password_conf) if message != '': - response_object = {'status': 'error', 'message': message} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse(message) password = bcrypt.generate_password_hash( password, current_app.config.get('BCRYPT_LOG_ROUNDS') ).decode() @@ -495,22 +461,15 @@ def edit_user(auth_user_id): user.weekm = weekm db.session.commit() - response_object = { + return { 'status': 'success', 'message': 'User profile updated.', 'data': user.serialize(), } - return jsonify(response_object), 200 # handler errors except (exc.IntegrityError, exc.OperationalError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response(e, db=db) @auth_blueprint.route('/auth/picture', methods=['POST']) @@ -557,20 +516,16 @@ def edit_picture(auth_user_id): """ try: - response_object, response_code = verify_extension_and_size( - 'picture', request - ) + response_object = verify_extension_and_size('picture', request) except RequestEntityTooLarge as e: appLog.error(e) max_file_size = current_app.config['MAX_CONTENT_LENGTH'] - response_object = { - 'status': 'fail', - 'message': 'Error during picture update, file size exceeds ' - f'{display_readable_file_size(max_file_size)}.', - } - return jsonify(response_object), 413 - if response_object['status'] != 'success': - return jsonify(response_object), response_code + return PayloadTooLargeErrorResponse( + 'Error during picture update, file size exceeds ' + f'{display_readable_file_size(max_file_size)}.' + ) + if response_object: + return response_object file = request.files['file'] filename = secure_filename(file.filename) @@ -593,21 +548,15 @@ def edit_picture(auth_user_id): file.save(absolute_picture_path) user.picture = relative_picture_path db.session.commit() - - response_object = { + return { 'status': 'success', 'message': 'User picture updated.', } - return jsonify(response_object), 200 except (exc.IntegrityError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'fail', - 'message': 'Error during picture update.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response( + e, message='Error during picture update.', status='fail', db=db + ) @auth_blueprint.route('/auth/picture', methods=['DELETE']) @@ -647,18 +596,11 @@ def del_picture(auth_user_id): os.remove(picture_path) user.picture = None db.session.commit() - - response_object = {'status': 'no content'} - return jsonify(response_object), 204 - + return {'status': 'no content'}, 204 except (exc.IntegrityError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'fail', - 'message': 'Error during picture deletion.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response( + e, message='Error during picture deletion.', status='fail', db=db + ) @auth_blueprint.route('/auth/password/reset-request', methods=['POST']) @@ -693,8 +635,7 @@ def request_password_reset(): """ post_data = request.get_json() if not post_data or post_data.get('email') is None: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() email = post_data.get('email') user = User.query.filter(User.email == email).first() @@ -718,11 +659,10 @@ def request_password_reset(): 'email': user.email, } reset_password_email.send(user_data, email_data) - response_object = { + return { 'status': 'success', 'message': 'Password reset request processed.', } - return jsonify(response_object), 200 @auth_blueprint.route('/auth/password/update', methods=['POST']) @@ -766,45 +706,31 @@ def update_password(): or post_data.get('password_conf') is None or post_data.get('token') is None ): - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() password = post_data.get('password') password_conf = post_data.get('password_conf') token = post_data.get('token') - invalid_token_response_object = { - 'status': 'error', - 'message': 'Invalid token. Please request a new token.', - } try: user_id = decode_user_token(token) except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): - return jsonify(invalid_token_response_object), 401 + return UnauthorizedErrorResponse() message = check_passwords(password, password_conf) if message != '': - response_object = {'status': 'error', 'message': message} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse(message) user = User.query.filter(User.id == user_id).first() if not user: - return jsonify(invalid_token_response_object), 401 + return UnauthorizedErrorResponse() try: user.password = bcrypt.generate_password_hash( password, current_app.config.get('BCRYPT_LOG_ROUNDS') ).decode() db.session.commit() - response_object = { + return { 'status': 'success', 'message': 'Password updated.', } - return jsonify(response_object), 200 - except (exc.OperationalError, ValueError) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - return jsonify(response_object), 500 + return handle_error_and_return_response(e, db=db) diff --git a/fittrackee/users/users.py b/fittrackee/users/users.py index 467e990d..321c5722 100644 --- a/fittrackee/users/users.py +++ b/fittrackee/users/users.py @@ -1,8 +1,15 @@ import os import shutil -from fittrackee import appLog, db -from flask import Blueprint, jsonify, request, send_file +from fittrackee import db +from fittrackee.responses import ( + ForbiddenErrorResponse, + InvalidPayloadErrorResponse, + NotFoundErrorResponse, + UserNotFoundErrorResponse, + handle_error_and_return_response, +) +from flask import Blueprint, request, send_file from sqlalchemy import exc from ..activities.utils_files import get_absolute_file_path @@ -156,7 +163,7 @@ def get_users(auth_user_id): .paginate(page, per_page, False) ) users = users_pagination.items - response_object = { + return { 'status': 'success', 'data': {'users': [user.serialize() for user in users]}, 'pagination': { @@ -167,7 +174,6 @@ def get_users(auth_user_id): 'total': users_pagination.total, }, } - return jsonify(response_object), 200 @users_blueprint.route('/users/', methods=['GET']) @@ -232,20 +238,16 @@ def get_single_user(auth_user_id, user_name): :statuscode 404: - User does not exist. """ - - response_object = {'status': 'fail', 'message': 'User does not exist.'} try: user = User.query.filter_by(username=user_name).first() - if not user: - return jsonify(response_object), 404 - else: - response_object = { + if user: + return { 'status': 'success', 'data': {'users': [user.serialize()]}, } - return jsonify(response_object), 200 except ValueError: - return jsonify(response_object), 404 + pass + return UserNotFoundErrorResponse() @users_blueprint.route('/users//picture', methods=['GET']) @@ -274,21 +276,16 @@ def get_picture(user_name): - No picture. """ - response_object = {'status': 'not found', 'message': 'No picture.'} try: user = User.query.filter_by(username=user_name).first() if not user: - response_object = { - 'status': 'fail', - 'message': 'User does not exist.', - } - return jsonify(response_object), 404 + return UserNotFoundErrorResponse() if user.picture is not None: picture_path = get_absolute_file_path(user.picture) return send_file(picture_path) - return jsonify(response_object), 404 except Exception: - return jsonify(response_object), 404 + pass + return NotFoundErrorResponse('No picture.') @users_blueprint.route('/users/', methods=['PATCH']) @@ -359,34 +356,23 @@ def update_user(auth_user_id, user_name): - User does not exist. :statuscode 500: """ - response_object = {'status': 'fail', 'message': 'User does not exist.'} user_data = request.get_json() if 'admin' not in user_data: - response_object = {'status': 'error', 'message': 'Invalid payload.'} - return jsonify(response_object), 400 + return InvalidPayloadErrorResponse() try: user = User.query.filter_by(username=user_name).first() if not user: - return jsonify(response_object), 404 - else: - user.admin = user_data['admin'] - db.session.commit() - response_object = { - 'status': 'success', - 'data': {'users': [user.serialize()]}, - } - return jsonify(response_object), 200 + return UserNotFoundErrorResponse() - except exc.StatementError as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', + user.admin = user_data['admin'] + db.session.commit() + return { + 'status': 'success', + 'data': {'users': [user.serialize()]}, } - code = 500 - return jsonify(response_object), code + except exc.StatementError as e: + return handle_error_and_return_response(e, db=db) @users_blueprint.route('/users/', methods=['DELETE']) @@ -435,62 +421,43 @@ def delete_user(auth_user_id, user_name): try: auth_user = User.query.filter_by(id=auth_user_id).first() user = User.query.filter_by(username=user_name).first() - if user: - if user.id != auth_user_id and not auth_user.admin: - response_object = { - 'status': 'error', - 'message': 'You do not have permissions.', - } - return response_object, 403 - if ( - user.admin is True - and User.query.filter_by(admin=True).count() == 1 - ): - response_object = { - 'status': 'error', - 'message': ( - 'You can not delete your account, ' - 'no other user has admin rights.' - ), - } - return response_object, 403 - for activity in Activity.query.filter_by(user_id=user.id).all(): - db.session.delete(activity) - db.session.flush() - user_picture = user.picture - db.session.delete(user) - db.session.commit() - if user_picture: - picture_path = get_absolute_file_path(user.picture) - if os.path.isfile(picture_path): - os.remove(picture_path) - shutil.rmtree( - get_absolute_file_path(f'activities/{user.id}'), - ignore_errors=True, + if not user: + return UserNotFoundErrorResponse() + + if user.id != auth_user_id and not auth_user.admin: + return ForbiddenErrorResponse() + if ( + user.admin is True + and User.query.filter_by(admin=True).count() == 1 + ): + return ForbiddenErrorResponse( + 'You can not delete your account, ' + 'no other user has admin rights.' ) - shutil.rmtree( - get_absolute_file_path(f'pictures/{user.id}'), - ignore_errors=True, - ) - response_object = {'status': 'no content'} - code = 204 - else: - response_object = { - 'status': 'not found', - 'message': 'User does not exist.', - } - code = 404 + + for activity in Activity.query.filter_by(user_id=user.id).all(): + db.session.delete(activity) + db.session.flush() + user_picture = user.picture + db.session.delete(user) + db.session.commit() + if user_picture: + picture_path = get_absolute_file_path(user.picture) + if os.path.isfile(picture_path): + os.remove(picture_path) + shutil.rmtree( + get_absolute_file_path(f'activities/{user.id}'), + ignore_errors=True, + ) + shutil.rmtree( + get_absolute_file_path(f'pictures/{user.id}'), + ignore_errors=True, + ) + return {'status': 'no content'}, 204 except ( exc.IntegrityError, exc.OperationalError, ValueError, OSError, ) as e: - db.session.rollback() - appLog.error(e) - response_object = { - 'status': 'error', - 'message': 'Error. Please try again or contact the administrator.', - } - code = 500 - return jsonify(response_object), code + return handle_error_and_return_response(e, db=db) diff --git a/fittrackee/users/utils.py b/fittrackee/users/utils.py index e6b08eb7..492e1ac8 100644 --- a/fittrackee/users/utils.py +++ b/fittrackee/users/utils.py @@ -3,7 +3,13 @@ from datetime import timedelta from functools import wraps import humanize -from flask import current_app, jsonify, request +from fittrackee.responses import ( + ForbiddenErrorResponse, + InvalidPayloadErrorResponse, + PayloadTooLargeErrorResponse, + UnauthorizedErrorResponse, +) +from flask import current_app, request from .models import User @@ -38,17 +44,12 @@ def register_controls(username, email, password, password_conf): def verify_extension_and_size(file_type, req): - response_object = {'status': 'success'} - code = 400 - if 'file' not in req.files: - response_object = {'status': 'fail', 'message': 'No file part.'} - return response_object, code + return InvalidPayloadErrorResponse('No file part.', 'fail') file = req.files['file'] if file.filename == '': - response_object = {'status': 'fail', 'message': 'No selected file.'} - return response_object, code + return InvalidPayloadErrorResponse('No selected file.', 'fail') allowed_extensions = ( 'ACTIVITY_ALLOWED_EXTENSIONS' @@ -67,52 +68,43 @@ def verify_extension_and_size(file_type, req): file_extension and file_extension in current_app.config.get(allowed_extensions) ): - response_object = { - 'status': 'fail', - 'message': 'File extension not allowed.', - } - elif file_extension != 'zip' and req.content_length > max_file_size: - response_object = { - 'status': 'fail', - 'message': 'Error during picture update, file size exceeds ' - f'{display_readable_file_size(max_file_size)}.', - } - code = 413 + return InvalidPayloadErrorResponse( + 'File extension not allowed.', 'fail' + ) - return response_object, code + if file_extension != 'zip' and req.content_length > max_file_size: + return PayloadTooLargeErrorResponse( + 'Error during picture update, file size exceeds ' + f'{display_readable_file_size(max_file_size)}.' + ) + + return None def verify_user(current_request, verify_admin): - response_object = { - 'status': 'error', - 'message': 'Something went wrong. Please contact us.', - } - code = 401 + default_message = 'Provide a valid auth token.' auth_header = current_request.headers.get('Authorization') if not auth_header: - response_object['message'] = 'Provide a valid auth token.' - return response_object, code, None - auth_token = auth_header.split(" ")[1] + return UnauthorizedErrorResponse(default_message), None + auth_token = auth_header.split(' ')[1] resp = User.decode_auth_token(auth_token) if isinstance(resp, str): - response_object['message'] = resp - return response_object, code, None + return UnauthorizedErrorResponse(resp), None user = User.query.filter_by(id=resp).first() if not user: - return response_object, code, None + return UnauthorizedErrorResponse(default_message), None if verify_admin and not is_admin(resp): - response_object['message'] = 'You do not have permissions.' - return response_object, 403, None - return None, None, resp + return ForbiddenErrorResponse(), None + return None, resp def authenticate(f): @wraps(f) def decorated_function(*args, **kwargs): verify_admin = False - response_object, code, resp = verify_user(request, verify_admin) + response_object, resp = verify_user(request, verify_admin) if response_object: - return jsonify(response_object), code + return response_object return f(resp, *args, **kwargs) return decorated_function @@ -122,9 +114,9 @@ def authenticate_as_admin(f): @wraps(f) def decorated_function(*args, **kwargs): verify_admin = True - response_object, code, resp = verify_user(request, verify_admin) + response_object, resp = verify_user(request, verify_admin) if response_object: - return jsonify(response_object), code + return response_object return f(resp, *args, **kwargs) return decorated_function @@ -132,12 +124,8 @@ def authenticate_as_admin(f): def can_view_activity(auth_user_id, activity_user_id): if auth_user_id != activity_user_id: - response_object = { - 'status': 'error', - 'message': 'You do not have permissions.', - } - return response_object, 403 - return None, None + return ForbiddenErrorResponse() + return None def display_readable_file_size(size_in_bytes):