diff --git a/.coverage b/.coverage deleted file mode 100644 index ef6f6f0..0000000 --- a/.coverage +++ /dev/null @@ -1 +0,0 @@ -!coverage.py: This is a private format, don't read it directly!{"lines": {"/home/benx/Desktop/Andela/StackOverflow-lite/tests/__init__.py": [1, 2, 4], "/home/benx/Desktop/Andela/StackOverflow-lite/tests/test_models.py": [1, 3, 4, 6, 9, 11, 16, 23, 30, 35, 12, 13, 14, 17, 18, 19, 20, 21, 31, 32, 33, 24, 25, 26, 27, 28, 36, 37, 38, 39], "/home/benx/Desktop/Andela/StackOverflow-lite/tests/base.py": [1, 3, 4, 6, 7, 8, 11, 12, 16, 19, 22, 13, 14, 20, 17], "/home/benx/Desktop/Andela/StackOverflow-lite/tests/test_route.py": [1, 3, 4, 6, 9, 11, 15, 20, 24, 33, 42, 55, 12, 13, 56, 57, 58, 21, 22, 16, 17, 18, 26, 27, 28, 30, 31, 45, 46, 48, 49, 50, 51, 35, 36, 37, 39, 40]}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4294566..094122f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ venv/ +.vscode/ __pycache__/ app/__pycache__/ app/routes/__pycache__ .vscode/ .vscode/settings.json .pytest_cache/ +.coverage +.env \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5ac7816..ad5ed4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,22 @@ + language: python python: - - "3.6" - + - "3.6" +cache: pip install: - - pip install -r requirements.txt + - pip install -r requirements.txt + - pip install python-coveralls + - pip install pytest + - pip install pytest-cov + - pip install coveralls +services: + - postgresql +before_script: + - psql -c 'create database clvx;' -U postgres + script: - - pytest - -after_success: - - coveralls + - pytest --cov=tests/ +after_success: + - coveralls diff --git a/README.md b/README.md index c13f5ed..a33eb5e 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,29 @@ [![Test Coverage](https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/test_coverage)](https://codeclimate.com/github/codeclimate/codeclimate/test_coverage) +![Build](https://travis-ci.org/cdvx/StackOverflow-lite.svg?branch=Challenge-3) -[![Coverage Status](https://coveralls.io/repos/github/cdvx/StackOverflow-lite/badge.svg?branch=Challenge-2)](https://coveralls.io/github/cdvx/StackOverflow-lite?branch=Challenge-2) +[![Maintainability](https://api.codeclimate.com/v1/badges/529402579dc667521f19/maintainability)](https://codeclimate.com/github/cdvx/StackOverflow-lite/maintainability) -git hub pages(challenge one) output: +[![Coverage Status](https://coveralls.io/repos/github/cdvx/StackOverflow-lite/badge.svg?branch=Challenge-3)](https://coveralls.io/github/cdvx/StackOverflow-lite?branch=Challenge-3) + + + +# github pages(challenge one) output: https://cdvx.github.io/StackOverflow-lite/UI/user.html -chalenge one setup: + +# Challenge one setup: Challenge one output is set up in the gh-pages branch, and hosted on github pages the pages are: user.html, question.html, other links are accessible through the links on the pages +# Challenge 2: +Heroku link: https://stackoverflow-lite-cdvx.herokuapp.com/ + +# Challenge 3: +Heroku link: https://stackoverflow-lite-cdvx2.herokuapp.com/api/v1/questions + pivotal tracker output: https://www.pivotaltracker.com/n/projects/2189643 diff --git a/UI/README.md b/UI/README.md new file mode 100644 index 0000000..375ed97 --- /dev/null +++ b/UI/README.md @@ -0,0 +1 @@ +# StackOverflow-lite \ No newline at end of file diff --git a/UI/css/login.css b/UI/css/login.css new file mode 100644 index 0000000..c171423 --- /dev/null +++ b/UI/css/login.css @@ -0,0 +1,243 @@ + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + /*background-color: #35424a;*/ + background-color:#f4f4f4; + background: -webkit-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: -moz-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: linear-gradient( to right, #a1d3b0, #f6f1d3); + font-family: "Simplifica"; + font-size: 20px; +} + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + +.button_1{ + height: 30px; + margin:auto; + color: #ffffff; + padding-left: 20px; + padding-right: 20px; + border-radius: 15px; + background: #e8491d; + border-color:#35424a; +} + +@font-face { + font-family: 'Simplifica'; + src:url('../fonts/Simplifica.ttf.woff') format('woff'), + url('../fonts/Simplifica.ttf.svg#Simplifica') format('svg'), + url('../fonts/Simplifica.ttf.eot'), + url('../fonts/Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + + +/*Header*/ +header { + background-color: #1c1d1c; + /* background:url("../img/img2.jpg") no-repeat; */ + color: #f4f4f4; + /*color:orange;*/ + font-family: "Simplifica"; + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + /* border-bottom: black 1px solid; */ +} + +header #hr{ + /* border-bottom: #e8491d 5px solid; */ + margin-bottom: 0; + padding: 0; +} + +header a{ + color: rgb(68, 182, 106); + text-decoration:none; + text-transform: uppercase; + font-size: 25px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +header #branding{ + float: left; + +} + +header #branding a{ + text-decoration: none; + text-transform: none; + font-size: 35px; +} +header #branding h1{ + margin:0; +} + +header nav{ + padding-top: 30px; + float:right; + margin-top: 10px; + margin-bottom: 0; +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; + +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; +} + +#container2{ + width: 30%; + max-height: 10px; + display: inline; +} + +#container2 #remember{ + float: left; + width:10%; + height: 5px; + margin-top: 10px; + margin-left: 20px; + +} + +#container2 h3{ + float: right; + height: 90%; + width:50%; + margin-right: 30%; + margin-bottom: 10px; +} + +#login{ + min-height: 500px; + color: #f4f4f4; +} + + +#register{ + background:/*url("../img/kybrd.jpeg")*/#1c1d1c; + background: -webkit-linear-gradient( #572e03, #1c1d1c); + background: -moz-linear-gradient( #572e03, #1c1d1c); /*#a79c0a*/ + background: linear-gradient( #572e03, #1c1d1c); + + border-radius: 10px; + /*background-color: #35424a;*/ + /*border: #35424a 2px solid;*/ + margin-left:20%; + margin-right: 20%; + min-height:60%; + padding-top: 20px; + padding-bottom: 10px; + width :55%; + +} + + +#register h3{ + + color:rgb(68, 182, 106); + margin-bottom: 0; +} +/* +#register .box{ + + background:url("../img/015.jpg") no-repeat ; + border: #35424a 4px solid; +} +*/ +#register h2{ + margin-top: 20px; + color: rgb(68, 182, 106); + font-weight: bold; + font-size: 35px; + text-decoration:none; +} + +#register a{ + color: #e8491d; +} + +#register form{ + min-height: 200px; + margin-bottom: 100px; + +} + +#register form h3{ + margin-bottom:0; + color: rgb(68, 182, 106); +} + +#register form input{ + box-shadow: 10px; + min-height:30px; + padding:3px ; + width: 90%; + border-radius: 10px; + /*border: #e8491d 2px solid;*/ + } + + +#register form a { + text-decoration: none; + + border-radius: 15px; + border-top: 2px solid #35424a; + border-left: 2px solid #35424a; + border-right: 2px solid black; + border-bottom: 2px solid black; + color: rgb(68, 182, 106); + padding: 1.5%; + background-color: #1c1c1c; +} + + +/*footer */ +footer{ + clear: both; + margin-bottom: 0; + width: 100%; + background-color: #1c1d1c; + /* background:url("../img/img2.jpg") no-repeat; */ + /* border-top:black 4px solid; */ + text-decoration: none; +} + +footer p{ + padding:10px; + margin:0; + color: rgb(68, 182, 106); + font-size:25px; + text-align: center; + +} \ No newline at end of file diff --git a/UI/css/question.css b/UI/css/question.css new file mode 100644 index 0000000..337f8ac --- /dev/null +++ b/UI/css/question.css @@ -0,0 +1,253 @@ + + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + background-color: #ffffff; + background: -webkit-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: -moz-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: linear-gradient( to right, #a1d3b0, #f6f1d3); + font-family: "Simplifica"; + font-size: 20px; + +} + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + +@font-face { + font-family: 'Simplifica'; + src:url('../fonts/Simplifica.ttf.woff') format('woff'), + url('../fonts/Simplifica.ttf.svg#Simplifica') format('svg'), + url('../fonts/Simplifica.ttf.eot'), + url('../fonts/Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + +.button_1{ + height: 30px; + margin :20px; + color: #ffffff; + padding-left: 20px; + padding-right: 20px; + border-radius: 15px; + background: #1c1c1c; + border-color:#35424a; +} + +/*Header*/ +header { + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + color: #f4f4f4; + /*color:orange;*/ + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + /* border-bottom: black 1px solid; */ +} + +header #hr{ + /* border-bottom: #e8491d 5px solid; */ + margin-bottom: 0; + padding: 0; +} + +header a{ + color: rgb(68, 182, 106); + text-decoration:none; + text-transform: uppercase; + font-size: 25px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +header #branding{ + float: left; +} + +header #branding a{ + text-decoration: none; + text-transform: none; + font-size: 35px; +} + +header #branding h1{ + margin:0; +} + +header nav{ + padding-top: 30px; + float:right; + margin-top: 10px; + margin-right: 0; + margin-bottom: 0; + +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; +} + + +/*showcase*/ +#showcase{ + min-height: 400px; + width: 90%; + /*background:url("../img/015.jpg") no-repeat;*/ + /*margin:20px;*/ + color : black; + +} + +#showcase strong h3{ + text-decoration: none; + text-align: center; + color: #1c1c1c; + /* border-bottom : 2px solid #35424a; */ + /* padding: 3px 10px 3px 10px; */ + /*border-radius: 5px solid rgb(201, 73, 73)*/; + font-size: 30px; + padding-top:10px; + margin:0; + /*background: rgb(236, 151, 81);*/ +} + +#showcase strong{ + background: #e8491d; +} + +#showcase h4{ + border-top: 2px solid #35424a; + border-left: 2px solid #35424a; + border-right: 2px solid black; + border-bottom: 2px solid black; + margin-left: 35%; + margin-top:0; + max-width: 150px; + color: rgb(68, 182, 106); + font-weight: bold; + text-align: center; + border-radius: 25px ; + background: #1c1c1c; + padding: 2px; + /*border : 2px solid #e8491d ;*/ + /* border-bottom : 2px solid #e8491d; */ +} +#showcase hr{ + size: 2px solid #35424a; +} + +/*content*/ +/*answer box*/ +#content { + width: 100%; +} + + +#answer{ + + width:100%; +} + +#answers p{ + border-radius: 10px /*solid rgb(201, 73, 73)*/; + border-bottom: 1px solid #c0c0c0; + padding:10px; + background: #f4f4f4; +} + +#answer .box{ + /*background: grey;*/ + align-self: center; + margin-left: 22.5%; + width:50%; + + border-radius: 10px; + background:/*url("../img/kybrd.jpeg")*/#1c1d1c; + background: -webkit-linear-gradient( #572e03, #1c1d1c); + background: -moz-linear-gradient( #572e03, #1c1d1c); /*#a79c0a*/ + background: linear-gradient( #572e03, #1c1d1c); + border-radius: 15px; + font-family: "Simplifica"; + /*background-color: #35424a;*/ + /*border-top: #35424a 4px solid;*/ + margin-bottom : 10px; + /*border-bottom: #35424a 4px solid;*/ +} + + + +#answer h2{ + margin-top: 20px; + color: rgb(68, 182, 106); + font-weight: bold; + margin-left: 25px; + text-decoration:none; +} + +#answer form{ + min-height: 30px; + +} + +#answer form input{ + min-height:20px; + margin-left: 25px; + padding:3px ; + width: 80%; + /*border: #e8491d 2px solid;*/ +} +#answer form .button_1{ + color: rgb(68, 182, 106); + margin-top: 10px; + margin-bottom: 10px; + +} + +/*footer */ +footer{ + clear: both; + /*position: absolute ;*/ + /*bottom: 0;*/ + margin-bottom: 0; + width: 100%; + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + /* border-top: black 4px solid; */ + text-decoration: none; +} + +footer p{ + padding:10px; + margin:0; + color: #ffffff; + text-align: center; + +} \ No newline at end of file diff --git a/UI/css/signUp.css b/UI/css/signUp.css new file mode 100644 index 0000000..d842eec --- /dev/null +++ b/UI/css/signUp.css @@ -0,0 +1,204 @@ + + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + background-color: #f4f4f4; + background: -webkit-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: -moz-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: linear-gradient( to right, #a1d3b0, #f6f1d3); + font-family: "Simplifica"; + font-size: 20px; + +} + +@font-face { + font-family: 'Simplifica'; + src:url('../fonts/Simplifica.ttf.woff') format('woff'), + url('../fonts/Simplifica.ttf.svg#Simplifica') format('svg'), + url('../fonts/Simplifica.ttf.eot'), + url('../fonts/Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + +.button_1{ + height: 30px; + margin:auto; + color: #ffffff; + padding-left: 20px; + padding-right: 20px; + background: #e8491d; + border-radius: 15px; + border-color:#35424a; +} + + +/*Header*/ +header { + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + color: #f4f4f4; + /*color:orange;*/ + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + /* border-bottom: black 1px solid; */ +} + + +header #hr{ + /* border-bottom: #e8491d 5px solid; */ + margin-bottom: 0; + padding: 0; +} + +header a{ + color: rgb(68, 182, 106); + text-decoration:none; + text-transform: uppercase; + font-size: 25px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +header #branding{ + float: left; +} +header #branding a{ + text-decoration: none; + text-transform: none; + font-size: 35px; +} + +header #branding h1{ + margin:0; +} + +header nav{ + padding-top: 30px; + float:right; + margin-top: 10px; + margin-bottom: 0; +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; +} + + +#register{ + background:/*url("../img/kybrd.jpeg")*/#1c1d1c; + background: -webkit-linear-gradient( #572e03, #1c1d1c); + background: -moz-linear-gradient( #572e03, #1c1d1c); /*#a79c0a*/ + background: linear-gradient( #572e03, #1c1d1c); + + border-radius: 8px; + /*border: #35424a 4px solid;*/ + margin-left:20%; + margin-right: 20%; + padding-top: 20px; + padding-bottom : 10px; + width :60%; + +} + + +h3{ + color:#f4f4f4; +} +/* +#register .box{ + + background:url("../img/015.jpg") no-repeat ; + border: #35424a 4px solid; +} +*/ +#register h2{ + margin-top: 20px; + color: rgb(68, 182, 106); + font-weight: bold; + text-decoration:none; + font-size:35px; +} + +#register a{ + color: #e8491d; +} + +#register form{ + min-height: 200px; + +} + +#register form h3{ + margin-bottom:0; + color: rgb(68, 182, 106); +} + +#register form input{ + min-height:30px; + padding:3px ; + width: 90%; + border-radius: 8px; + /*border: #e8491d 2px solid;*/ +} + + #register #signUp a { + text-decoration: none; + border-radius: 15px; + border-top: 2px solid #35424a; + border-left: 2px solid #35424a; + border-right: 2px solid black; + border-bottom: 2px solid black; + color: rgb(68, 182, 106); + padding: 1.5%; + background-color: #1c1c1c; +} + +/*footer */ +footer{ + clear: both; + margin-bottom: 0; + width: 100%; + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + /* border-top: black 4px solid; */ + text-decoration: none; +} + +footer p{ + padding:10px; + margin:0; + color: rgb(68, 182, 106); + text-align: center; + +} \ No newline at end of file diff --git a/UI/css/style.css b/UI/css/style.css new file mode 100644 index 0000000..ee84786 --- /dev/null +++ b/UI/css/style.css @@ -0,0 +1,163 @@ + + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + background-color: #f4f4f4; + +} + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + +.button_1{ + height: 30px; + margin :20px; + color: #ffffff + padding-left: 20px; + padding-right: 20px; + background: #e8491d; + border-color:#35424a; +} + +/*Header*/ +header { + /*background-color: #35424a;*/ + background:url("../img/img2.jpg") no-repeat; + color: #f4f4f4; + /*color:orange;*/ + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + border-bottom: #e8491d 4px solid; +} + +header a{ + color: #ffffff; + text-decoration:none; + text-transform: uppercase; + font-size: 15px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +head #branding{ + float: left; +} + +header #branding h1{ + margin:0; +} + +header nav{ + float:right; + margin-top: 10px; +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; +} + + +/*showcase*/ +#showcase{ + min-height: 400px; + width: 80%; + /*background:url("../img/015.jpg") no-repeat;*/ + /*margin:20px;*/ + color : black; + float: left; +} + +#showcase h3{ + text-decoration: none; +} + +/*content*/ +/*question box*/ +#content { + width: 100%; +} + + +#question{ + margin-top: 20px; + padding-top: 20px; + width:20%; + padding-bottom: 10px; + float: right; +} +#question .box{ + +<<<<<<< HEAD + background:url("../img/015.jpg") no-repeat ; +======= + background:url("../img/015.JPG") no-repeat ; +>>>>>>> 1fcf8137b2a5018aa69ee9cebea35565080a9044 + border: #35424a 4px solid; +} + +#question h2{ + margin-top: 20px; + color: #ffffff; + font-weight: bold; + text-decoration:none; +} + +#question form{ + min-height: 200px; + +} + +#question form input{ + min-height:30px; + padding:3px ; + width: 90%; + border: #e8491d 2px solid; + } + + +/*footer */ +footer{ + clear: both; + position: absolute ; + bottom: 0; + width: 100%; + /*background-color: #35424a;*/ + background:url("../img/img2.jpg") no-repeat; + border-top:#e8491d 4px solid; + text-decoration: none; +} + +footer p{ + padding:10px; + margin:0; + color: #ffffff; + text-align: center; + +} \ No newline at end of file diff --git a/UI/css/style2.css b/UI/css/style2.css new file mode 100644 index 0000000..35f1e57 --- /dev/null +++ b/UI/css/style2.css @@ -0,0 +1,235 @@ + + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + background-color: #ffffff; + background: -webkit-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: -moz-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: linear-gradient( to right, #a1d3b0, #f6f1d3); + font-family: "Simplifica"; + font-size: 20px; +} + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + + @font-face { + font-family: 'Simplifica'; + src:url('../fonts/Simplifica.ttf.woff') format('woff'), + url('../fonts/Simplifica.ttf.svg#Simplifica') format('svg'), + url('../fonts/Simplifica.ttf.eot'), + url('../fonts/Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + + +.button_1{ + height: 30px; + margin :20px; + font-family: "Simplifica"; + font-size:20px ; + color: rgb(68, 182, 106); + /*padding-left: 20px; + padding-right: 20px;*/ + padding: 5px 20px 10px 20px; + background: #1c1d1c; + border-radius: 15px; + border-color:#35424a; +} + +/*Header*/ +header { + background-color: #1c1d1c; + /* background:url("../img/img2.jpg") no-repeat; */ + color: #f4f4f4; + /*color:orange;*/ + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + /* border-bottom: black 1px solid; */ +} + +header #hr{ + /* border-bottom: #e8491d 3px solid; */ + margin-bottom: 0; + padding: 0; +} +header a{ + color: /*#ffffff*/ rgb(68, 182, 106); + text-decoration:none; + text-transform: uppercase; + font-size: 25px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +header #branding{ + float: left; + +} + +header #branding a{ + text-decoration: none; + text-transform: none; + font-size: 35px; +} + +header #branding h1{ + margin:0; +} + +header nav{ + padding-top: 30px; + float:right; + margin-top: 10px; + margin-right: 0; + margin-bottom: 0; +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; + +} + + +/*showcase*/ +#showcase{ + min-height: 400px; + width: 70%; + padding:1px; + /*background:url("../img/015.jpg") no-repeat;*/ + /*margin:20px;*/ + color : black; + float: left; +} + +#showcase a{ + text-decoration: none; + color: black; +} + +#showcase a p{ + border-radius: 10px /*solid rgb(201, 73, 73)*/; + border-bottom: 1px solid #c0c0c0; + padding:10px; + background: #f4f4f4; +} + +#showcase h3{ + text-decoration: none; + border-radius: 25px; + margin-left: 25%; + max-width: 200px; + color: rgb(68, 182, 106); + border-top: 2px solid #5b6469; + border-left: 2px solid #5b6469; + border-right: 2px solid black; + border-bottom: 2px solid black; + font-weight: bold; + text-align: center; + border-radius: 25px ; + background: #1c1d1c; + padding: 2px; +} + +/*content*/ +/*question box*/ +#content { + width: 100%; + min-height: 800px; +} + + +#question{ + margin-top: 20px; + padding-top: 20px; + margin-right: 5px; + width:25%; + padding-bottom: 10px; + float: right; +} +#question .box{ + + background:/*url("../img/kybrd.jpeg")*/#1c1d1c; + background: -webkit-linear-gradient( #572e03, #1c1d1c); + background: -moz-linear-gradient( #572e03, #1c1d1c); /*#a79c0a*/ + background: linear-gradient( #572e03, #1c1d1c); + border-radius: 10px; + /*border: #35424a 4px solid;*/ + +} + +#question h2{ + margin-top: 20px; + color: rgb(68, 182, 106); + font-weight: bold; + text-decoration:none; +} + +#question form{ + min-height: 200px; + +} + +#question form h3{ + color: rgb(68, 182, 106); + margin-bottom:0; +} + +#question form input{ + border-radius :8px; + min-height:30px; + padding:3px ; + width: 90%; + /*border: #e8491d 2px solid;*/ + } + + +/*footer */ +footer{ + clear: both; + margin-bottom: 0; + width: 100%; + background-color: #1c1d1c; + + /* background:url("../img/img2.jpg") no-repeat; */ + /* border-top:black 4px solid; */ + text-decoration: none; +} + +footer p{ + text-align: center; + padding:10px; + font-size: 25px; + margin:0; + color: rgb(68, 182, 106); + text-align: center; + +} \ No newline at end of file diff --git a/UI/css/user.css b/UI/css/user.css new file mode 100644 index 0000000..1dfd4e3 --- /dev/null +++ b/UI/css/user.css @@ -0,0 +1,240 @@ + + +body { + /*font-family: Arial, Helvetica, sans-seriff; + font-size: 15px; + line-height: 1.5; same as*/ + font: 15px/1.5 Arial,Helvetica, sans-seriff; + padding: 0; + margin: 0; + background-color: #ffffff; + background: -webkit-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: -moz-linear-gradient( to right, #a1d3b0, #f6f1d3); + background: linear-gradient( to right, #a1d3b0, #f6f1d3); + font-family: "Simplifica"; + font-size: 20px; + +} + +@font-face { + font-family: 'Simplifica'; + src:url('../fonts/Simplifica.ttf.woff') format('woff'), + url('../fonts/Simplifica.ttf.svg#Simplifica') format('svg'), + url('../fonts/Simplifica.ttf.eot'), + url('../fonts/Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + + +/*Global*/ +.container{ + width: 90%; + margin: auto; + overflow: hidden; +} + +ul{ + padding: 0; + margin: 0; + + } + +.button_1{ + height: 30px; + margin :20px; + color: rgb(68, 182, 106); + padding-left: 20px; + padding-right: 20px; + background: #1c1c1c; + border-color:#35424a; +} + +/*Header*/ +header { + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + color: rgb(68, 182, 106); + /*color:orange;*/ + font-size: 20px; + padding-top:30px; + padding-bottom: 0; + min-height: 70px; + /* border-bottom: black 1px solid; */ +} + +header #hr{ + /* border-bottom: #e8491d 5px solid; */ + margin-bottom: 0; + padding: 0; +} + + +header a{ + color: rgb(68, 182, 106); + text-decoration:none; + text-transform: uppercase; + font-size: 25px; + } + +header li{ + float: left; + display: inline; + padding:0 20px 0 20px; + } + +header #branding{ + float: left; +} + +header #branding a{ + text-decoration: none; + text-transform: none; + font-size: 35px; +} + + +header #branding h1{ + margin:0; +} + +header nav{ + padding-top: 30px; + float:right; + margin-top: 10px; + margin-bottom: 0; +} + +header .highlight, header .current a{ + color:#e8491d; + font-weight:bold; +} + +header a:hover{ + color: #CCCCCC; + font-weight:bold; +} + +/*content*/ +#content{ + margin: auto; + width: 100; +} + +/*showcase*/ +#showcase{ + min-height: 400px; + width: 25%; + /*background:url("../img/015.jpg") no-repeat;*/ + /*margin:20px;*/ + color : black; + margin-left: 20px; + float: left; +} + +#showcase h3{ + margin-top:0; + margin-left: 20px; + text-decoration: none; + border-radius: 15px /*solid rgb(201, 73, 73)*/; + text-align: center; + max-width:150px; + color:rgb(68, 182, 106); + padding:5px; + background: #1c1c1c; +} + +#activity h3{ + margin-top:10px; + margin-left: 25%; + max-width:150px; + text-decoration: none; + border-radius: 15px /*solid rgb(201, 73, 73)*/; + border-top: 2px solid #646c70; + border-left: 2px solid #646c70; + border-right: 2px solid black; + border-bottom: 2px solid black; + text-align: center; + color:rgb(68, 182, 106); + padding:2px; + background: #1c1c1c; +} + +#activity #two{ + max-width:240px; + margin-left: 20%; +} + +#content img{ + max-width: 100%; + height:auto; + width: auto\9; + padding-bottom: 0; +} + +#content{ + align-content: center; +} + +#user p{ + border-radius: 10px /*solid rgb(201, 73, 73)*/; + border-bottom: 1px solid #c0c0c0; + padding:10px; + background: #f4f4f4; +} + +aside{ + margin-top: 0; + width: 60%; + float: right; + display: inline; +} +aside .box{ + min-height:520px; + margin: 0; + padding: 0; + /*border: #35424a 4px solid;*/ + /*margin-bottom: 0;*/ +} + +aside h2{ + margin-top: 20px; + color: #35424a; + font-weight: bold; + text-decoration:none; +} + +#activity li{ + margin-left: 0; + margin-bottom : 8px; + border-radius: 10px /*solid rgb(201, 73, 73)*/; + border-bottom: 1px solid #c0c0c0; + padding:10px; + background: #f4f4f4; +} + +#activity a{ + text-decoration: none; + color: black; +} + + + +/*footer */ +footer{ + clear: both; + margin-bottom: 0; + background-color: #1c1c1c; + /* background:url("../img/img2.jpg") no-repeat; */ + /* border-top: black 4px solid; */ + text-decoration: none; +} + +footer p{ + padding:10px; + margin:0; + font-size:25px; + color: rgb(68, 182, 106); + text-align: center; + +} \ No newline at end of file diff --git a/UI/fonts/Simplifica.ttf.eot b/UI/fonts/Simplifica.ttf.eot new file mode 100644 index 0000000..388faa2 Binary files /dev/null and b/UI/fonts/Simplifica.ttf.eot differ diff --git a/UI/fonts/Simplifica.ttf.svg b/UI/fonts/Simplifica.ttf.svg new file mode 100644 index 0000000..f6bf047 --- /dev/null +++ b/UI/fonts/Simplifica.ttf.svg @@ -0,0 +1,679 @@ + + + + +Created by FontForge 20090622 at Sun May 14 16:48:09 2017 + By ffonts +KAIWA (Kamil Iwaszczyszyn) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/fonts/Simplifica.ttf.woff b/UI/fonts/Simplifica.ttf.woff new file mode 100644 index 0000000..2250cc5 Binary files /dev/null and b/UI/fonts/Simplifica.ttf.woff differ diff --git a/UI/fonts/example.html b/UI/fonts/example.html new file mode 100644 index 0000000..6fed015 --- /dev/null +++ b/UI/fonts/example.html @@ -0,0 +1,173 @@ + + + + +Simplifica WebFont + + + + + + +
+
+ +
+

Simplifica WebFont

+ + +
+
+
Copyright info
+
+KAIWA [Kamil Iwaszczyszyn] +
+ +
+
+
Information 64px
+
+ Quisque vitae urna sit amet ipsum lobortis iaculis volutpat sit amet nibh. +
+ +
+
+
Information 32px
+
+ Vivamus bibendum augue vitae mi imperdiet volutpat tempor tellus lacinia. Integer in sem et neque iaculis malesuada. +
+ +
+
+
Information 16px
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus erat elit, accumsan id lacinia in, vulputate auctor sapien. Phasellus euismod consequat magna, ut lobortis tellus semper sed. Fusce sollicitudin fermentum tortor, ut malesuada mauris aliquet fermentum. Maecenas luctus sapien vel turpis iaculis vitae commodo nibh cursus. Praesent vel suscipit orci. Praesent elit velit, bibendum at laoreet a, tristique non metus. Maecenas vitae vulputate justo. Aliquam velit ligula, scelerisque eget sagittis nec, tempus eu risus. +
+ +
+
+
Information
+
+
!!
""
##
$$
%%
&&
''
((
))
**
++
,,
--
..
//
00
11
22
33
44
55
66
77
88
99
::
;;
<<
==
>>
??
@@
AA
BB
CC
DD
EE
FF
GG
HH
II
JJ
KK
LL
MM
NN
OO
PP
QQ
RR
SS
TT
UU
VV
WW
XX
YY
ZZ
[[
\\
]]
^^
__
``
aa
bb
cc
dd
ee
ff
gg
hh
ii
jj
kk
ll
mm
nn
oo
pp
qq
rr
ss
tt
uu
vv
ww
xx
yy
zz
{{
||
}}
~~


ƒƒ
ˆˆ
ŠŠ
ŒŒ

ŽŽ


˜˜
šš
œœ

žž
ŸŸ
  
¡¡
¢¢
££
¤¤
¥¥
¦¦
§§
¨¨
©©
ªª
««
¬¬
­­
®®
¯¯
°°
±±
²²
³³
´´
µµ
··
¸¸
¹¹
ºº
»»
¼¼
½½
¾¾
¿¿
ÀÀ
ÁÁ
ÂÂ
ÃÃ
ÄÄ
ÅÅ
ÆÆ
ÇÇ
ÈÈ
ÉÉ
ÊÊ
ËË
ÌÌ
ÍÍ
ÎÎ
ÏÏ
ÐÐ
ÑÑ
ÒÒ
ÓÓ
ÔÔ
ÕÕ
ÖÖ
××
ØØ
ÙÙ
ÚÚ
ÛÛ
ÜÜ
ÝÝ
ÞÞ
ßß
àà
áá
ââ
ãã
ää
åå
ææ
çç
èè
éé
êê
ëë
ìì
íí
îî
ïï
ðð
ññ
òò
óó
ôô
õõ
öö
÷÷
øø
ùù
úú
ûû
üü
ýý
þþ
ÿÿ
+
+ +
+
+
Installing Webfonts
+
+1. Upload the files from this zip to your domain.
+2. Add this code to your website:
+ +@font-face { + font-family: 'Simplifica'; + src:url('Simplifica.ttf.woff') format('woff'), + url('Simplifica.ttf.svg#Simplifica') format('svg'), + url('Simplifica.ttf.eot'), + url('Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} + +3. Integrate the fonts into your CSS:
+Add the font name to your CSS styles. For example: + +h1 { + font-family: 'Simplifica'; +} + +
+ +
+
+
Troubleshooting Webfonts
+
+1. You may be using the fonts on different domain or subdomain.
+2. Check if you have linked the fonts properly in the CSS.
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/UI/fonts/images/bgtile.gif b/UI/fonts/images/bgtile.gif new file mode 100644 index 0000000..182b4e2 Binary files /dev/null and b/UI/fonts/images/bgtile.gif differ diff --git a/UI/fonts/images/button.gif b/UI/fonts/images/button.gif new file mode 100644 index 0000000..06a6df7 Binary files /dev/null and b/UI/fonts/images/button.gif differ diff --git a/UI/fonts/images/fontboxbottom.gif b/UI/fonts/images/fontboxbottom.gif new file mode 100644 index 0000000..dbeea79 Binary files /dev/null and b/UI/fonts/images/fontboxbottom.gif differ diff --git a/UI/fonts/images/fontboxtile.gif b/UI/fonts/images/fontboxtile.gif new file mode 100644 index 0000000..34c6e06 Binary files /dev/null and b/UI/fonts/images/fontboxtile.gif differ diff --git a/UI/fonts/images/fontboxtop.gif b/UI/fonts/images/fontboxtop.gif new file mode 100644 index 0000000..5296d2d Binary files /dev/null and b/UI/fonts/images/fontboxtop.gif differ diff --git a/UI/fonts/images/foot.gif b/UI/fonts/images/foot.gif new file mode 100644 index 0000000..e8fb10f Binary files /dev/null and b/UI/fonts/images/foot.gif differ diff --git a/UI/fonts/images/foottile.gif b/UI/fonts/images/foottile.gif new file mode 100644 index 0000000..8bd24cf Binary files /dev/null and b/UI/fonts/images/foottile.gif differ diff --git a/UI/fonts/images/free.gif b/UI/fonts/images/free.gif new file mode 100644 index 0000000..f637a3e Binary files /dev/null and b/UI/fonts/images/free.gif differ diff --git a/UI/fonts/images/headertile.gif b/UI/fonts/images/headertile.gif new file mode 100644 index 0000000..dee77ad Binary files /dev/null and b/UI/fonts/images/headertile.gif differ diff --git a/UI/fonts/images/logo.gif b/UI/fonts/images/logo.gif new file mode 100644 index 0000000..17a8dff Binary files /dev/null and b/UI/fonts/images/logo.gif differ diff --git a/UI/fonts/install-a-webfont.txt b/UI/fonts/install-a-webfont.txt new file mode 100644 index 0000000..267d9cc --- /dev/null +++ b/UI/fonts/install-a-webfont.txt @@ -0,0 +1,24 @@ +Installing Webfonts + +1. Upload the files from this zip to your domain. +2. Add this code to your website: + +@font-face { + font-family: 'Simplifica'; + src:url('Simplifica.ttf.woff') format('woff'), + url('Simplifica.ttf.svg#Simplifica') format('svg'), + url('Simplifica.ttf.eot'), + url('Simplifica.ttf.eot?#iefix') format('embedded-opentype'); + font-weight: normal; + font-style: normal; +} +3. Integrate the fonts into your CSS: +Add the font name to your CSS styles. For example: + +h1 { + font-family: 'Simplifica'; +} + +Troubleshooting Webfonts +1. You may be using the fonts on different domain or subdomain. +2. Check if you have link the fonts properly in the CSS. \ No newline at end of file diff --git a/UI/fonts/webfonts.ffonts.net.htm b/UI/fonts/webfonts.ffonts.net.htm new file mode 100644 index 0000000..5e4e9d9 --- /dev/null +++ b/UI/fonts/webfonts.ffonts.net.htm @@ -0,0 +1,21 @@ + + +Webfonts www.FFonts.net - Redirect + + + + + +
+ +If this page does not automatically redirect you, click below to go to the Webfonts FFonts homepage +

+http://webfonts.ffonts.net + +
+ + \ No newline at end of file diff --git a/UI/fonts/webfonts.ffonts.net.txt b/UI/fonts/webfonts.ffonts.net.txt new file mode 100644 index 0000000..80f7724 --- /dev/null +++ b/UI/fonts/webfonts.ffonts.net.txt @@ -0,0 +1,10 @@ +Download Webfonts from webfonts.ffonts.net: + + + +http://www.ffonts.net + + + + +Free WebFonts for your website \ No newline at end of file diff --git a/UI/img/011.png b/UI/img/011.png new file mode 100644 index 0000000..f605e7a Binary files /dev/null and b/UI/img/011.png differ diff --git a/UI/img/015.JPG b/UI/img/015.JPG new file mode 100644 index 0000000..8ec4b9f Binary files /dev/null and b/UI/img/015.JPG differ diff --git a/UI/img/gears.jpeg b/UI/img/gears.jpeg new file mode 100644 index 0000000..881811e Binary files /dev/null and b/UI/img/gears.jpeg differ diff --git a/UI/img/img2.jpg b/UI/img/img2.jpg new file mode 100644 index 0000000..249fc3b Binary files /dev/null and b/UI/img/img2.jpg differ diff --git a/UI/img/img3.jpg b/UI/img/img3.jpg new file mode 100644 index 0000000..3472d96 Binary files /dev/null and b/UI/img/img3.jpg differ diff --git a/UI/img/img4.jpg b/UI/img/img4.jpg new file mode 100644 index 0000000..62943b9 Binary files /dev/null and b/UI/img/img4.jpg differ diff --git a/UI/img/kybrd.jpeg b/UI/img/kybrd.jpeg new file mode 100644 index 0000000..90316d3 Binary files /dev/null and b/UI/img/kybrd.jpeg differ diff --git a/UI/img/mthrbrd.jpeg b/UI/img/mthrbrd.jpeg new file mode 100644 index 0000000..93bd8ab Binary files /dev/null and b/UI/img/mthrbrd.jpeg differ diff --git a/UI/index.html b/UI/index.html new file mode 100644 index 0000000..9150ce2 --- /dev/null +++ b/UI/index.html @@ -0,0 +1,87 @@ + + + + + + + + + StackOverflow-lite + + +
+
+ + +
+
+
+
+
+ +
+ + + +
+ + + + \ No newline at end of file diff --git a/UI/js/base.js b/UI/js/base.js new file mode 100644 index 0000000..4747a4d --- /dev/null +++ b/UI/js/base.js @@ -0,0 +1,2 @@ +/* Resgister new user */ +let signUp = (e) \ No newline at end of file diff --git a/UI/js/main.js b/UI/js/main.js new file mode 100644 index 0000000..d558056 --- /dev/null +++ b/UI/js/main.js @@ -0,0 +1,46 @@ +console.log('Hello guy') + +function sum(...x){ + let s = 0; + if (x.length < 2){ + return " Cannot find sum of one item" + } + for (let i=x.length; i>0 ; i--){ + let x1 = x.pop() + if (Number(x1) == NaN){ + return `some of them items entered are not numbers` + } + s += x1 + } + return s +} + + + +function range(x, y, step = 1){ + let rng = []; + + if (xy && step*-1 !==-Math.abs(step)){ + console.log(`${step*-1} step*-1 , + 1 = ${(step*-1)+1}`) + for (let i=x; x >=y; i--){ + console.log(x) + rng.push(x), x-= Math.abs(step); + }; + } + if (x == y){ + return "range(start, end), start s figure should not be equal tothe end!"; + } + return rng; +} + +//console.log(range(20, 2,-2)) +let step =-1 + +console.log(step) +//console.log(sum(...range(1, 10))) \ No newline at end of file diff --git a/UI/login.html b/UI/login.html new file mode 100644 index 0000000..980607d --- /dev/null +++ b/UI/login.html @@ -0,0 +1,55 @@ + + + + + + + + + StackOverflow-lite + + +
+
+ + +
+
+
+ +
+
+
+
+

Sign in

+
+

Username

+ +

Password

+ +

+
+

Remember me

+

+
+ +
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/UI/question.html b/UI/question.html new file mode 100644 index 0000000..a46c840 --- /dev/null +++ b/UI/question.html @@ -0,0 +1,82 @@ + + + + + + + + + StackOverflow-lite + + +
+
+ + +
+
+
+ +
+
+
+

What are data structures in python programming? + please include examples and how they are used. +

+ + +

Answers

+
+

programming is the

+

What are data structures?

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vivamus nec tortor ac purus luctus lobortis id et magna. + Pellentesque id odio volutpat, fermentum neque non, vestibulum enim. + Vivamus aliquet libero quis orci mattis tincidunt. +

+

A program is .....

+

data structures are ......

+

How to write a restful API + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vivamus nec tortor ac purus luctus lobortis id et magna. + Pellentesque id odio volutpat, fermentum neque non, vestibulum enim. + Vivamus aliquet libero quis orci mattis tincidunt. +

+

What is django + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vivamus nec tortor ac purus luctus lobortis id et magna. + Pellentesque id odio volutpat, fermentum neque non, vestibulum enim. + Vivamus aliquet libero quis orci mattis tincidunt. +

+
+
+
+ +
+
+
+

Answer Question

+
+ + + +
+
+
+ +
+ +
+

StackOverflow-lite © 2018

+
+ + \ No newline at end of file diff --git a/UI/signUp.html b/UI/signUp.html new file mode 100644 index 0000000..5167374 --- /dev/null +++ b/UI/signUp.html @@ -0,0 +1,56 @@ + + + + + + + + + StackOverflow-lite + + +
+
+ + +
+
+
+ +
+
+
+

Sign up

+
+

Username

+ +

Email

+ +

Password

+ +

Repeat Password

+ +
+

Sign Up

+
+ +

Already have an account? Sign in

+
+
+
+ +
+ +
+

StackOverflow-lite © 2018

+
+ + \ No newline at end of file diff --git a/UI/user.html b/UI/user.html new file mode 100644 index 0000000..cc3d3e3 --- /dev/null +++ b/UI/user.html @@ -0,0 +1,71 @@ + + + + + + + + + StackOverflow-lite + + +
+
+ + +
+
+
+
+
+
+
+


+

Username

+
+

Number of answers given: 20

+

Number of guestions asked: 15

+
+
+
+ + +
+
+

StackOverflow-lite © 2018

+
+ + \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index a089413..5abf48f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,10 @@ from flask import Flask +from flask_jwt_extended import JWTManager + from config import Config app = Flask(__name__) -app.config.from_object(Config) +app.config.from_object('config.Config') +app.config['JWT_SECRET_KEY'] = 'wuiq2739%W%$%^FhjY^^' +jwt = JWTManager(app) diff --git a/app/connect.py b/app/connect.py new file mode 100644 index 0000000..439037b --- /dev/null +++ b/app/connect.py @@ -0,0 +1,203 @@ +import os + +import psycopg2 + +# DSN_APP = "dbname='clvx' user='postgres' host='localhost' password='Tesxting' port='5432'" +# DSN_TESTING = "dbname='test_db' user='postgres' host='localhost' password='Tesxting' port='5432'" + + +class DatabaseConnection(object): + def __init__(self): + if os.getenv('APP_SETTINGS') == "testing": + self.dbname = "test_db" + + else: + self.dbname = "clvx" + + try: + + self.connection = psycopg2.connect(dbname=f"{self.dbname}", user='postgres', host='localhost', password='Tesxting', port='5432') + self.connection.autocommit = True + self.cursor = self.connection.cursor() + self.last_ten_queries = [] + except: + print("Cannot connect to database.") + + def create_Users_table(self): + try: + self.tablename = 'users' + create_table_command = """CREATE TABLE IF NOT EXISTS users( + id serial PRIMARY KEY, + username varchar(100) NOT NULL, + email varchar(100) NOT NULL, + password_hash varchar(200) NOT NULL, + user_id varchar(150) NOT NULL + );""" + self.cursor.execute(create_table_command) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def create_Questions_table(self): + try: + self.tablename = 'questions' + create_table_command = """CREATE TABLE IF NOT EXISTS questions( + id serial PRIMARY KEY, + topic varchar(100) NOT NULL, + body varchar(600) NOT NULL, + author varchar(100) NOT NULL, + question_id varchar(150) NOT NULL + );""" + self.cursor.execute(create_table_command) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def create_Answers_table(self): + try: + self.tablename = 'answers' + create_table_command = """CREATE TABLE IF NOT EXISTS answers( + id serial PRIMARY KEY, + Qn_Id varchar(150) NOT NULL, + body varchar(600) NOT NULL, + answer_id varchar(150) NOT NULL, + author varchar(100) NOT NULL, + prefered boolean + );""" + self.cursor.execute(create_table_command) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def insert_new_record(self, tablename, data): + tables = ['users', 'questions', 'answers'] + try: + if tablename == tables[0]: + insert_command = """INSERT INTO users( + username, + email, + password_hash, + user_id + ) VALUES( + %s, + %s, + %s, + %s + );""" + self.cursor.execute(insert_command, ( + data['username'], + data['email'], + data['password'], + data['user_id']) + ) + elif tablename == tables[1]: + insert_command = """INSERT INTO questions( + topic, + body, + author, + question_id + ) VALUES( + %s, + %s, + %s, + %s + );""" + self.cursor.execute(insert_command, ( + data['topic'], + data['body'], + data['author'], + data['questionId']) + ) + elif tablename == tables[2]: + insert_command = """INSERT INTO answers( + Qn_Id, + body, + answer_id, + author, + prefered + ) VALUES( + %s, + %s, + %s, + %s, + %s + );""" + self.cursor.execute(insert_command, ( + data['Qn_Id'], + data['body'], + data['answerId'], + data['author'], + data['prefered']) + ) + else: + if tablename not in tables: + msg = f"User table {tablename} does not exit." + return msg + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def query_all(self, tablename): + queries = [] + try: + self.cursor.execute(f"SELECT * FROM {tablename}") + items = self.cursor.fetchall() + if items: + for item in items: + queries.append(item) + if len(self.last_ten_queries) == 11: + self.last_ten_queries.pop() + self.last_ten_queries.append(item) + return queries + except (Exception, psycopg2.DatabaseError) as error: + print(error) + else: + return queries + + def update_question(self, new_topic, new_body, questionId): + try: + update_command = """UPDATE questions SET topic = %s, body = %s + WHERE question_id = %s""" + self.cursor.execute(update_command, (new_topic, new_body, questionId)) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def update_answer(self, answerId): + try: + update_command = """UPDATE answers SET prefered = %s WHERE answer_id = %s""" + self.cursor.execute(update_command, (True, str(answerId))) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + + def delete_entry(self, tablename, id_value): + try: + if tablename == 'questions': + delete_command = """DELETE FROM questions + WHERE question_id = %s""" + self.cursor.execute(delete_command, (id_value, )) + + if tablename == 'answers': + delete_command = """DELETE FROM answers + WHERE answer_id = %s""" + self.cursor.execute(delete_command, (id_value, )) + + if tablename == 'users': + delete_command = """DELETE FROM users + WHERE user_id = %s""" + self.cursor.execute(delete_command, (id_value, )) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + def drop_table(self, tablename): + + try: + drop_table_command = f"DROP TABLE {tablename} CASCADE" + self.cursor.execute(drop_table_command) + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + +conn = DatabaseConnection() +#conn.drop_table('answers') +conn.create_Answers_table() +#conn.drop_table('users') +conn.create_Users_table() +#conn.drop_table('questions') +conn.create_Questions_table() diff --git a/app/models.py b/app/models.py index 9a6c316..ffd141d 100644 --- a/app/models.py +++ b/app/models.py @@ -1,30 +1,129 @@ +import uuid + +from werkzeug.security import generate_password_hash + +from app.connect import conn + + class Question: - def __init__(self, questionId=0, topic='', body=''): - self.id = questionId - self.topic = topic - self.body = body - self.answers = [] + def __init__(self, topic, body, author=None): + self.id = uuid.uuid4().int + self.topic = str(topic).strip() + self.body = str(body).strip() + self.author = author def __repr__(self): return { - 'questionId': self.id, 'topic': self.topic, - 'body': self.body + 'body': self.body, + 'questionId': self.id, + 'author': self.author } class Answer: - def __init__(self, answerId=0, body='', Qn_Id=0): - self.answerId = answerId - self.body = body + def __init__(self, body, Qn_Id, author=None, pref=False): + self.answerId = uuid.uuid4().int + self.body = str(body).strip() self.Qn_Id = Qn_Id + self.author = author + self.prefered = pref def __repr__(self): return { 'answerId': self.answerId, 'Qn_Id': self.Qn_Id, - 'body': self.body + 'body': self.body, + 'author': self.author, + 'prefered': self.prefered + } + + +class User: + def __init__(self, username, email, password): + self.id = uuid.uuid4().int + self.username = str(username).strip() + self.email = str(email).strip() + self.password_hash = generate_password_hash(str(password)) + + def __repr__(self): + return { + 'username': self.username, + 'email': self.email, + 'password': self.password_hash, + 'user_id': self.id } - -questionsList = [] -answersList = [] + + +def valid_username(username): + users = conn.query_all('users') + if len(users) != 0: + for user in users: + existing_user = [user[1] + for user in users if user[1] == username] + if not existing_user: + return True + elif len(users) == 0: + return True + return False + + +def valid_question(questionObject): + if 'topic' in questionObject.keys() and 'body' in questionObject.keys(): + + questionsList = conn.query_all('questions') + input_topic = questionObject['topic'] + input_body = questionObject['body'] + + empty_field = len(str(input_topic).strip()) == 0 or len(str(input_body).strip()) == 0 + check_type = type(input_topic) == int or type(input_body) == int + if empty_field or check_type: + value = (False, {"hint_1":"Question topic or body should not be empty!", + "hint_2":"body and topic fileds should not consist entirely of integer-type data"}) + return value + if questionsList: + topics = [question[1] for question in questionsList if question[1] == input_topic] + if len(topics) != 0: + value = (False, "Question topic already exists!") + return value + else: + if len(topics) == 0: + return (True, ) + return (True, ) + else: + if 'topic' or 'body' not in questionObject.keys(): + return (False, ) + + +def valid_answer(answerObject): + if 'body' in answerObject.keys(): + input_body = answerObject['body'] + empty_field = len(input_body.strip()) == 0 + check_type = type(input_body) == int + if empty_field or check_type: + return (False, {'hint_1': "Answer body should not be empty!", + 'hint_2': """body and Qn_Id fileds should not contain + numbers only and string-type data respectively"""} + ) + return (True, ) + else: + return (False, ) + + +def valid_signup_data(request_data): + keys = request_data.keys() + condition_1 = 'username' in keys and 'email' in keys + condition_2 = 'password' in keys and 'repeat_password' in keys + if condition_1 and condition_2: + return True + else: + return False + + +def valid_login_data(request_data): + keys = request_data.keys() + condition_1 = 'username' in keys and 'password' in keys + if condition_1: + return True + else: + return False diff --git a/app/routes/routes.py b/app/routes/routes.py index f79599e..c847687 100644 --- a/app/routes/routes.py +++ b/app/routes/routes.py @@ -1,145 +1,483 @@ -from flask import (Flask, Response, flash, json, jsonify, - request, session, url_for) +from datetime import datetime, timedelta + +from flask import Flask, Response, json, jsonify, request, url_for +from flask_jwt_extended import (JWTManager, create_access_token, + get_jwt_identity, jwt_required) +from werkzeug.security import check_password_hash from app import app -from app.models import Answer, Question, answersList, questionsList +from app.connect import conn +from app.models import (Answer, Question, User, valid_answer, valid_login_data, + valid_question, valid_signup_data, valid_username) + @app.route('/') def show_api_works(): return jsonify({'Welcome to my app': [{'message': "endpoints work"}]}) +@app.route('/api/v1/auth/login', methods=['POST']) +def login(): + + request_data = request.get_json() + if not request_data: + return jsonify({'message': 'JSON missing in request!'}), 400 + + if valid_login_data(request_data): + username = request_data['username'] + password = request_data['password'] + + if not username: + return jsonify({ + 'message': 'Required parameter: username missing!' + }), 400 + elif not password: + return jsonify({ + 'message': 'Required parameter: password missing!' + }), 400 + + users = conn.query_all('users') + user = [user for user in users if check_password_hash( + user[3], str(password)) and user[1] == username] + if not user: + return jsonify({'message': 'Invalid username or password'}), 401 + + access_token = create_access_token( + identity=username, + fresh=timedelta(minutes=60) + ) + msg = {'access_token': f'{access_token}'} + + return jsonify({f'User:{username} logged in.': msg}), 200 + msg = {"error": "Invalid login data", + "Hint": """required formart is: {'username':'xyz', + 'password': 'xyh12',}"""} + return jsonify(msg) + + +@app.route('/api/v1/auth/signup', methods=['POST']) +def signup(): + request_data = request.get_json() + if not request_data: + return jsonify({'message': 'JSON missing in request!'}), 400 + + if valid_signup_data(request_data): + print('valid data') + username = str(request_data['username']).split() + email = request_data['email'] + password = request_data['password'] + repeat_password = request_data['repeat_password'] + + if not username: + print('not username') + return jsonify({ + 'message': 'Required parameter: username missing!' + }), 400 + elif username: + print('username') + if len(username) > 1: + username_ = username[0] + " " + username[1] + username = username_ + if not valid_username(username): + return jsonify({'message': f'Username: {username} already taken!'}), 401 + else: + if len(username) == 1: + username = username[0] + if not valid_username(username): + return jsonify({'message': f'Username: {username} already taken!'}), 401 + + if not email or len(email.strip()) == 0: + print("not email, valid username") + return jsonify({'message': 'Required parameter: email missing!'}), 400 + if username and email: + print('valid username and email') + if not password or len(str(password).strip()) == 0: + print("not password") + return jsonify({'message': 'Required parameter: password missing!'}), 400 + + if username and email and password: + if not repeat_password or len(str(repeat_password).strip()) == 0: + print("not_repeat pw") + msg = 'Required parameter: repeat_password missing!' + return jsonify({'message': f'{msg}'}), 400 + else: + if repeat_password: + if repeat_password == password: + user = User(username, email, password) + conn.insert_new_record('users', user.__repr__()) + + return jsonify({ + 'success': f"{username}'s account created successfully" + }), 200 + else: + if repeat_password != password: + return jsonify({ + 'message': 'Password does not match repeat_password' + }), 401 + msg = {"error": "Invalid signup data", + "Hint": """required formart is: {'username':'xyz', + 'email':'xyz@gmail.com', + 'password': 'xyh12', + 'repeat_password':'xyh12'}"""} + return jsonify(msg) + + @app.route('/api/v1/questions', methods=['GET']) def get_questions(): + questionsList = conn.query_all('questions') + questions = [] if questionsList: - return jsonify({'questions': questionsList}) - return jsonify({'messgae': 'No Questions added yet'}) + for qn in questionsList: + temp = { + 'questionId': qn[4], + 'author': qn[3], + 'topic': qn[1], + 'body': qn[2] + } + questions.append(temp) + return jsonify({'questions': questions}), 200 + return jsonify({'message': 'No Questions added.'}), 404 + @app.route('/api/v1/questions/', methods=['GET']) def get_question(questionId): + questionsList = conn.query_all('questions') + answersList = conn.query_all('answers') + ans_list= [] + if questionsList: - for question in questionsList: - if question['questionId'] == questionId: + question = [qn for qn in questionsList if int(qn[4])==questionId] + if question and not answersList: + print(question) + temp = { + 'questionId': question[0][4], + 'topic': question[0][1], + 'body': question[0][2], + 'author': question[0][3] + } + return jsonify(temp), 200 + if question and answersList: + answers = [ans for ans in answersList if int(ans[1]) == questionId] + if answers: + for ans in answers: + temp1 = { + 'answerId': ans[3], + 'body': ans[2], + 'author': ans[4], + 'prefered': ans[5], + 'questionId': ans[1] + } + + ans_list.append(temp1) temp = { - 'questionId': question['questionId'], - 'topic': question['topic'], - 'body': question['body'] + 'questionId': question[0][4], + 'topic': question[0][1], + 'body': question[0][2], + 'author': question[0][3], + 'answers': ans_list } - return jsonify(temp) - return Response(json.dumps(['Question not Found']), + return jsonify(temp), 200 + + return Response(json.dumps(['Question not Found']), status=404, mimetype='application/json') - return jsonify({f'Question {questionId}': 'Has not been added yet'}) + + return jsonify({'message': 'No questions added.'}), 200 + + +@app.route('/api/v1/questions//answers', methods=['GET']) +def get_answers(questionId): + answersList = conn.query_all('answers') + questionsList = conn.query_all('questions') + if questionsList: + questions = [qn for qn in questionsList if int(qn[4]) == questionId] + if questions: + answers = [] + if answersList: + for answer in answersList: + if int(answer[1]) == questionId: + temp = { + 'answerId': answer[3], + 'author': answer[4], + 'body': answer[2], + 'prefered': answer[5], + 'questionId': answer[1] + } + answers.append(temp) + return jsonify({'answers': answers}), 200 + return jsonify({'message': 'Answer not found!'}), 404 + return jsonify({'message': 'No Answers added.'}), 404 + return jsonify({'message': 'Question not found!'}), 404 + return jsonify({'message': 'No questions added!'}), 404 + + +@app.route('/api/v1/questions//answers/', + methods=['GET']) +def get_answer(questionId, answerId): + questionsList = conn.query_all('questions') + answersList = conn.query_all('answers') + if questionsList: + if answersList: + for answer in answersList: + + if int(answer[3]) == answerId: + + temp = { + 'answerId': answer[3], + 'author': answer[4], + 'body': answer[2], + 'prefered': answer[5], + 'QuestionId': answer[1] + } + return jsonify(temp), 200 + return Response(json.dumps(['Answer not found!']), + status=404, mimetype='application/json') + return jsonify({'message': 'Question not found!'}), 404 + @app.route('/api/v1/questions', methods=['POST']) +@jwt_required def add_question(): - request_data = request.get_json() - if (valid_question(request_data)): - temp = { - 'questionId': request_data['questionId'], - 'topic': request_data['topic'], - 'body': request_data['body'] - } - questionsList.append(temp) - response = Response('', 201, mimetype='application/json') - response.headers['location'] = ('questions/' + - str(request_data['questionId'])) - - return response - else: - bad_object = { - "error": "Invalid question object", - "hint": '''Request format should be,{'questionId':1, 'topic': 'python', - 'body': 'what is python in programming' }''' - } - response = Response(json.dumps([bad_object]), - status=400, mimetype='application/json') - return response + current_user = get_jwt_identity() + if current_user: + request_data = request.get_json() -@app.route('/api/v1/questions//answers', methods=['POST']) -def add_answer(questionId): - request_data = request.get_json() - if questionsList: - if (valid_answer(request_data)): + duplicate_check = valid_question(request_data) + + if duplicate_check[0]: temp = { - 'answerId': request_data['answerId'], - 'Qn_Id': request_data['Qn_Id'], + 'topic': request_data['topic'], 'body': request_data['body'] } - answersList.append(temp) - for question in questionsList: - if question['questionId'] == request_data['Qn_Id']: - question = Question(question['questionId'], - question['topic'], question['body']) - question.answers.append(temp) - - response = Response('', status=201, mimetype='application/json') - response.headers['location'] = ('answers/' + - str(request_data['answerId'])) - return response - + + question = Question(temp['topic'], temp['body']) + question.author = current_user + conn.insert_new_record('questions', question.__repr__()) + + return jsonify({ + 'message': 'Question posted successfully', + 'question': question.__repr__() + }), 201 + else: + if not duplicate_check[0] and len(duplicate_check) > 1: + reason = duplicate_check[1] + return jsonify({"error": f"{reason}"}) + else: bad_object = { - "error": "Invalid answer object", - "hint": '''Request format should be {'answerId':1, - 'body': 'this is the body', - 'Qn_Id': 2}''' + "error": "Invalid question object", + "hint": '''Request format should be,{'topic': 'python', + 'body': 'what is python in programming' }''' } - response = Response(json.dumps([bad_object]), + response = Response(json.dumps([bad_object]), status=400, mimetype='application/json') return response - return jsonify({f'Attempt to answer Question {questionId}': - f'Question {questionId} does not exit.'}) + return jsonify({ + 'message': 'To post a question, you need to be logged in', + 'info': 'Signup or login, to get acces_token' + }), 401 + + +@app.route('/api/v1/questions//answers', methods=['POST']) +@jwt_required +def add_answer(questionId): + current_user = get_jwt_identity() + if current_user: + request_data = request.get_json() + questionsList = conn.query_all('questions') + if questionsList: + answer_check = valid_answer(request_data) + ids = [int(qn[4]) for qn in questionsList] + if answer_check[0] and questionId in ids: + temp = { + 'Qn_Id': questionId, + 'body': request_data['body'] + } + answer = Answer(temp['body'], temp['Qn_Id']) + answer.author = current_user + conn.insert_new_record('answers', answer.__repr__()) + + return jsonify({ + 'message': 'Answer posted successfully', + 'answer': answer.__repr__() + }), 201 + + else: + + if not answer_check[0] and len(answer_check) > 1: + reason = answer_check[1] + return jsonify({"error": f"{reason}"}) + else: + bad_object = { + "error": "Invalid answer object", + "hint": '''Request format should be { + 'body': 'this is the body', + 'Qn_Id': 2}''' + } + response = Response(json.dumps([bad_object]), + status=400, mimetype='application/json') + return response + return jsonify({f'Attempt to answer Question with Id:{questionId}': + 'Question not found!.'}), 404 + + return jsonify({ + 'message': 'To post an answer, you need to be logged in', + 'info': 'Signup or login, to get access_token' + }), 401 + + +@app.route('/api/v1/questions//answers/', methods=['PUT']) +@jwt_required +def select_answer_as_preferred(questionId, answerId): + current_user = get_jwt_identity() + if current_user: + # request_data = request.get_json() + questionsList = conn.query_all('questions') + answersList = conn.query_all('answers') + + if answersList or questionsList: + + #answer_check = valid_answer(request_data) + usr = [qn[3] for qn in questionsList if int(qn[4]) == questionId] + + answer = [ans for ans in answersList if int(ans[1]) == questionId and int(ans[3]) == answerId] + + if usr and usr[0] == current_user: + + if answer[0]: + + conn.update_answer(str(answerId)) + temp = { + 'answerId': answer[0][3], + 'body': answer[0][2], + 'author': answer[0][4], + 'prefered': True, + 'questionId': answer[0][1] + } + return jsonify({ + 'message': "Answer marked as preferred", + 'answer': temp + }), 201 + + return jsonify({'message': 'Answer not found!'}), 404 + return jsonify({'Access denied': + f'Only question auhtor:{current_user} can perform this action!'}) + return jsonify({'message': + 'Question not found'}), 404 + + return jsonify({ + 'message': 'To post an answer, you need to be logged in', + 'info': 'Signup or login, to get access_token' + }), 401 @app.route('/api/v1/questions/', methods=['PATCH']) +@jwt_required def update_question(questionId): - request_data = request.get_json() - if questionsList: - updated_question = dict() - ids = [question['questionId'] for question in questionsList] - - if questionId in ids: - if "topic" in request_data: - updated_question["topic"] = request_data["topic"] - if "body" in request_data: - updated_question["body"] = request_data["topic"] - - for question in questionsList: - if question["questionId"] == questionId: - question.update(updated_question) + current_user = get_jwt_identity() + if current_user: + request_data = request.get_json() + questionsList = conn.query_all('questions') + + if questionsList: + usr = [qn[3] for qn in questionsList if int(qn[4]) == questionId] + if usr and usr[0] == current_user: + updated_question = dict() + ids = [int(question[4]) for question in questionsList] + + if questionId in ids: + if "topic" in request_data: + updated_question["topic"] = request_data["topic"] + if "body" in request_data: + updated_question["body"] = request_data["body"] + condition_1 = len(updated_question['topic']) + condition_2 = len(updated_question['body']) + if condition_1 != 0 and condition_2 != 0: + for question in questionsList: + if int(question[4]) == questionId: + conn.update_question( + updated_question['topic'], + updated_question['body'], + str(questionId)) + temp = { + 'new_topic': updated_question['topic'], + 'new_body': updated_question['body'] + } + msg = 'Question updated successfully.' + return jsonify({'message': msg, + 'updated_question': temp}), 200 + return jsonify({ + 'msg': 'body and topic fields should not be empty'}) + msg = f'Only question auhtor:{current_user} can perform this action!' + return jsonify({'Access denied': msg}) + response = Response(json.dumps(['Question not found']), status=404) + return response - response = Response('', status=204) - response.headers['Location'] = "/questions" + str(questionId) - return response - response = Response(json.dumps(['Question not found']), status=404) - return response + return jsonify({ + 'message': 'To update a question, you need to be logged in', + 'info': 'Signup or login, to get access_token' + }) @app.route('/api/v1/questions/', methods=['DELETE']) +@jwt_required def delete_question(questionId): - if questionsList: - ids = [question['questionId'] for question in questionsList] - if questionId in ids: - for question in questionsList: - if questionId == question['questionId']: - questionsList.remove(question) - response = Response('', status=200, mimetype='application/json') - return response - response = Response(json.dumps(['Question not found']), - status=404, mimetype='application/json') - return response - + current_user = get_jwt_identity() + if current_user: + questionsList = conn.query_all('questions') + if questionsList: + usr = [qn[3] for qn in questionsList if int(qn[4]) == questionId] + if usr and usr[0] == current_user: + ids = [int(question[4]) for question in questionsList] + if questionId in ids: + + for question in questionsList: + if questionId == int(question[4]): + + questionsList.remove(question) + conn.delete_entry('questions', str(questionId)) + + message = { + 'success': f"Question deleted!"} + response = Response( + json.dumps(message), status=202, mimetype='application/json') + return response + msg = f'Only question auhtor:{current_user} can perform this action!' + return jsonify({'Access denied': msg}) + response = Response(json.dumps(['Question not found']), + status=404, mimetype='application/json') + return response + return jsonify({ + 'message': 'To delete a question, you need to be logged in', + 'info': 'Signup or login, to get access_token' + }), 401 + + +@app.errorhandler(500) +def internal_sserver_error(e): + msg = "Sorry,we are experiencing some technical difficulties" + msg2 = "Please report this to cedriclusiba@gmail.com and check back with us soon" + return jsonify({'error': msg, "hint": msg2}), 500 + + +@app.errorhandler(404) +def url_unknown(e): + msg = "Sorry, resource you are looking for does not exist" + return jsonify({"error": msg}), 404 + + +@app.errorhandler(405) +def method_not_allowed(e): + msg = "Sorry, this action is not supported for this url" + return jsonify({'error': msg}), 405 + + +@app.errorhandler(403) +def forbidden_resource(e): + msg = "Sorry, resource you are trying to access is forbidden" + return jsonify({'error': msg}), 403 + -def valid_question(questionObject): - if 'topic' in questionObject and 'body' in questionObject: - return True - else: - return False - -def valid_answer(answerObject): - if 'Qn_Id' in answerObject and 'answerId' in answerObject and 'body' in answerObject : - return True - else: - return False - \ No newline at end of file +@app.errorhandler(410) +def deleted_resource(e): + return jsonify({'error': "Sorry, this resource was deleted"}), 410 diff --git a/config.py b/config.py index e4666ff..f495540 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,36 @@ import os -basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): - SECRET_KEY = os.environ.get('SECRET_KEY') or 'tesxting' \ No newline at end of file + DEBUG = False + CSRF_ENABLED = True + SECRET = os.getenv('SECRET') + POSTGRES_DATABASE_URI = os.getenv('DATABASE_URL') + + +class DevelopmentConfig(Config): + DEBUG = True + + +class TestingConfig(Config): + DEBUG = True + TESTING = True + POSTGRES_DATABASE_URI = "postgresql://postgres:sudo!localhost:5432/clvx" + + +class StagingConfig(Config): + DEBUG = True + + +class ProductionConfig(Config): + DEBUG = False + TESTING = True + + +app_config = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'staging': StagingConfig, + 'production': ProductionConfig +} + diff --git a/requirements.txt b/requirements.txt index 341aa0f..8883db2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,17 @@ nose==1.3.7 +bcrypt==3.1.4 +pytest-cov==2.5.1 +PyJWT==1.6.4 certifi==0.0.8 chardet==1.0.1 coverage==4.0.3 coveralls==1.3.0 Flask==1.0.2 +Flask-JWT-Extended==3.12.1 Flask-Testing==0.7.1 gunicorn==19.9.0 pep8==1.7.1 +psycopg2==2.7.5 pylint==2.1.1 pytest==3.7.1 python-coveralls==2.9.1 diff --git a/run.py b/run.py index c3f8bdc..c245aa7 100644 --- a/run.py +++ b/run.py @@ -2,4 +2,4 @@ from app.routes import routes if __name__ == '__main__': - app.run() \ No newline at end of file + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/tests/base.py b/tests/base.py index fa61460..f4fb677 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,48 +1,53 @@ import unittest +from datetime import timedelta -from flask import current_app +from flask import current_app, Response +from flask_jwt_extended import (create_access_token, get_jwt_identity, + jwt_required) from flask_testing import TestCase from app import app -from app.routes import routes +from app.connect import DatabaseConnection from app.models import Answer, Question +from app.routes import routes from config import Config class APITestCase(TestCase): + def create_app(self): app.config['DEBUG'] = True return app def setUp(self): self.app = app.test_client() - - def tearDown(self): - pass - - def post_question(self): - pass - - + self.conn = DatabaseConnection() + print(self.conn.dbname) + self.conn.create_Answers_table() + self.conn.create_Questions_table() + self.conn.create_Users_table() + self.data = { + "username": "Kakai", + "email": "dhhj@gmail.com", + "password": "jjq123", + "repeat_password": "jjq123" + } + self.conn.insert_new_record('users', self.data) + + def is_logged_in(self): + self.access_token = create_access_token( + identity='Kakai', + fresh=timedelta(minutes=200) + ) + res = Response(mimetype='application/json') + res.headers['Authorization'] = f'Bearer {self.access_token}' + self.current_user = get_jwt_identity() + return self.current_user - - -ans_List = [ - - { - 'answerId': 1, - 'Qn_Id': 1, - 'body': ""}, - {'answerId': 2, - 'Qn_Id': 2, - 'body': ""}, - {'answerId': 3, - 'Qn_Id': 3, - 'body': ""}, - {'answerId': 4, - 'Qn_Id': 4, - 'body': ""} -] + def tearDown(self): + self.conn.drop_table('users') + self.conn.drop_table('questions') + self.conn.drop_table('answers') def createQnsList(): @@ -55,12 +60,7 @@ def createQnsList(): topics = [0, '', '', '', '', ''] for i in range(1, 6): - Qn = Question(i, topics[i], body) - - for answer in ans_List: - if answer['Qn_Id'] == Qn.id: - Qn.answers.append(answer) - + Qn = Question( topics[i], body) QnsList.append(Qn.__repr__()) return QnsList @@ -77,9 +77,8 @@ def createAnsList(): qnIds[:0] = [0] for i in range(1, 6): - Ans = Answer(i, body, qnIds[i]) + Ans = Answer(body, qnIds[i]) AnsList.append(Ans.__repr__()) return AnsList answersList = createAnsList() - diff --git a/tests/test_models.py b/tests/test_models.py index 577861a..ebe8ce8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,10 @@ from flask import json +from flask_testing import TestCase from tests import app -from app.models import Answer, Question +from app.connect import DatabaseConnection +from app.models import (Answer, Question, User, valid_answer, valid_login_data, + valid_question, valid_signup_data, valid_username) from .base import APITestCase, answersList, questionsList @@ -9,31 +12,191 @@ class TestModels(APITestCase): def setUp(self): - self.question1 = Question(1, 'computers', 'what is python ?') - self.answer1 = Answer(1, 'it is a programming language', 1) - self.question1.answers.append(self.answer1.__repr__()) - - def test_answerList_created_properly(self): - self.assertEqual(5, len(answersList)) - for answer in answersList: - self.assertIn('answerId', answer) - self.assertIn('body', answer) - self.assertIn('Qn_Id', answer) - - def test_questionsList_created_properly(self): - self.assertEqual(5, len(questionsList)) - for question in questionsList: - self.assertIn('questionId', question) - self.assertIn('topic', question) - self.assertIn('body', question) - - def test_questionObject_has_answers_attribute(self): - self.assertTrue(self.question1.answers) - self.assertTrue(type(self.question1.answers) == list) - self.assertTrue(type(self.question1.answers[0]) == dict) + + self.conn = DatabaseConnection() + self.conn.create_Questions_table() + self.conn.create_Answers_table() + self.conn.create_Users_table() + + self.user1 = User('Peter', 'ptr@gmail.com','1234') + self.user2 = User('Jane', 'jenn@gmail.com','1234') + self.question1 = Question('computers', 'what is python ?') + self.question1.author = self.user1.username + self.question2 = Question('api', 'what is Flask ?') + self.question2.author = self.user2.username + self.answer1 = Answer('it is a programming language', + self.question1.id) + self.answer1.author = self.user1.username + self.answer2 = Answer('it a microframework for building python apps', + self.question2.id) + self.answer2.author = self.user2.username + + def insert_new_records(queries): + for query in queries: + self.conn.insert_new_record(query[0], query[1]) + + self.queries = (('questions', self.question1.__repr__()), ('questions', self.question2.__repr__()), + ('answers', self.answer1.__repr__()), ('answers', self.answer2.__repr__()), + ('users', self.user1.__repr__()), ('users', self.user2.__repr__())) + + insert_new_records(self.queries) + + self.ansList = self.conn.query_all('answers') + self.qnsList = self.conn.query_all('questions') + self.usersList = self.conn.query_all('users') + + + + def tearDown(self): + self.conn.drop_table('users') + self.conn.drop_table('questions') + self.conn.drop_table('answers') + + + def test_answers_questions_have_uniqueIds(self): + self.assertTrue(self.question1.id != self.question2.id) + self.assertTrue(self.answer1.answerId != self.answer2.answerId) + self.assertTrue(self.user1.id != self.user2.id) + + def test_question_answer_relationship(self): + self.assertTrue(self.question1.id == self.answer1.Qn_Id) + self.assertTrue(self.answer2.Qn_Id == self.question2.id) + self.assertTrue(self.question1.author == self.user1.username) + self.assertTrue(self.answer2.author == self.user2.username) + def test_repr_turnsObject_into_dict(self): res1 = self.answer1.__repr__() res2 = self.question1.__repr__() - self.assertTrue(type(res1)) - self.assertTrue(type(res2)) + res3 = self.user1.__repr__() + self.assertTrue(type(res1) == dict) + self.assertTrue(type(res2) == dict) + self.assertTrue(type(res3) == dict) + + def test_recors_inserted_properly(self): + if self.qnsList: + for question in self.qnsList: + self.assertEqual(5, len(question)) + self.assertEqual(2, len(self.qnsList)) + + if self.ansList: + for answer in self.ansList: + self.assertEqual(6, len(answer)) + self.assertEqual(2, len(self.ansList)) + + if self.usersList: + for user in self.usersList: + self.assertEqual(5, len(user)) + self.assertEqual(2, len(self.usersList)) + + def test_user_can_query_db(self): + question = Question('computer science', 'What is a program?') + question.author = self.user1.username + body = '''a program is a set of instructions + given to a computer to perform certain tasks''' + answer = Answer(body, question.id) + answer.author = self.user2.username + + self.conn.insert_new_record('questions', question.__repr__()) + self.conn.insert_new_record('answers', answer.__repr__()) + ansL = [ans for ans in self.conn.query_all('answers') if int(ans[1])==question.id] + + qnsL = [qn for qn in self.conn.query_all('questions') if int(qn[4])==question.id] + + self.assertEqual(body, ansL[0][2]) + self.assertEqual(self.user2.username, ansL[0][4]) + self.assertEqual('computer science', qnsL[0][1]) + self.assertEqual('What is a program?', qnsL[0][2]) + + def test_valid_question(self): + data = { + 'topic': 'computers and tech', + 'body': 'What is the relevance of computers in technology!' + } + res = valid_question(data) + self.assertEqual((True, ), res) + data['topic'] = 'api' + res = valid_question(data) + value = (False, "Question topic already exists!") + self.assertEqual(value, res) + topic = data['topic'] + body = data['body'] + condition1 = topic == ' ' or topic == '' or topic == 1234 + condition2 = body == ' ' or body == '' or body == 1234 + if condition1 or condition2: + res = valid_question(data) + value = (False, {"hint_1":"Question topic or body should not be empty!", + "hint_2":"body and topic fileds should not consist entirely of integer-type data"}) + self.assertEqual(value, res) + elif condition1 and condition2 : + res = valid_question(data) + value = (False, {"hint_1":"Question topic or body should not be empty!", + "hint_2":"body and topic fileds should not consist entirely of integer-type data"}) + self.assertEqual(value, res) + else: + data = {} + res = valid_question(data) + self.assertEqual((False, ), res) + + def test_valid_username(self): + res = valid_username('Peter') + self.assertTrue(res == False) + res = valid_username('Kangol') + self.assertTrue(res == True) + + def test_valid_answer(self): + data ={ + 'body': 'It is a scripting language', + } + res = valid_answer(data) + self.assertTrue(res == (True, )) + value = (False, {'hint_1': "Answer body should not be empty!", + 'hint_2': """body and Qn_Id fileds should not contain + numbers only and string-type data respectively"""}) + if len(data['body']) == 0 or type(data['body']) == int: + res = valid_answer(data) + self.assertTrue(value == res) + elif len(data['body']) == 0 and type(data['body']) == int: + res = valid_answer(data) + self.assertTrue(value == res) + else: + if data == {}: + res = valid_answer(data) + self.assertTrue((False, ) == res) + + def test_valid_signup_data(self): + data = { + 'username': 'Kangol', + 'email': 'kangol@gmail.com', + 'password': '123e', + 'repeat_password': '123e' + } + res = valid_signup_data(data) + self.assertTrue(res == True) + del data['username'] + res = valid_signup_data(data) + self.assertTrue(res == False) + del data['email'] + res = valid_signup_data(data) + self.assertTrue(res == False) + del data['password'] + res = valid_signup_data(data) + self.assertTrue(res == False) + del data['repeat_password'] + res = valid_signup_data(data) + self.assertTrue(res == False) + + def test_valid_login_data(self): + data = { + 'username': 'Tom Peter', + 'password': 1234 + } + res = valid_login_data(data) + self.assertTrue(res == True) + del data['username'] + res = valid_login_data(data) + self.assertTrue(res == False) + del data['password'] + res = valid_login_data(data) + res = valid_login_data(data) + self.assertTrue(res == False) \ No newline at end of file diff --git a/tests/test_route.py b/tests/test_route.py index bddc04a..c31e465 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -1,7 +1,11 @@ +from datetime import timedelta + from flask import json, url_for +from flask_jwt_extended import (create_access_token, get_jwt_identity, + jwt_required) from tests import app -from .base import APITestCase +from .base import APITestCase, questionsList class TestRoutes(APITestCase): @@ -10,23 +14,106 @@ def test_root_route(self): res = self.client.get('/') self.assertEqual(res.status_code, 200) + def test_user_can_signup(self): + self.data = { + "username": "Kakai", + "email": "cedriclusiba@gmail.com", + "password": "jjq123", + "repeat_password": "jjq123" + } + res = self.client.post( + '/api/v1/auth/signup', content_type="application/json", data=json.dumps(self.data)) + msg = {'success': "Kakai's account created successfully"} + self.assertEqual(res.json, msg) + self.assertEqual(res.status_code, 200) + + def test_user_can_login(self): + self.data = { + "username": "Kakai", + "email": "cedriclusiba@gmail.com", + "password": "jjq123", + "repeat_password": "jjq123" + } + res = self.client.post( + '/api/v1/auth/signup', content_type="application/json", data=json.dumps(self.data)) + + self.data2 = { + "username": "Kakai", + "password": "jjq123" + } + res2 = self.client.post( + "/api/v1/auth/login", content_type="application/json", data=json.dumps(self.data2)) + + self.assertEqual(res2.status_code, 200) + def test_user_can_get_questions(self): with self.client: - res = self.client.get('/api/v1/questions') - self.assertEqual(res.status_code, 200) + questionsList = self.conn.query_all('questions') + if questionsList: + res = self.client.get('/api/v1/questions') + self.assertEqual(res.status_code, 200) + + else: + res = self.client.get('/api/v1/questions') + self.assertEqual(res.status_code, 404) + self.assertEqual(res.json, {'message': 'No Questions added.'}) def test_user_can_get_question(self): - res = self.client.get('/api/v1/questions/2') - self.assertEqual(res.status_code, 200) + questionsList = self.conn.query_all('questions') + if questionsList and [qn for qn in questionsList if qn[4]==2]: + res = self.client.get('/api/v1/questions/2') + self.assertEqual(res.status_code, 200) + else: + res = self.client.get('/api/v1/questions/2') + self.assertEqual(res.status_code, 200) + self.assertEqual(res.json, {'message': 'No questions added.'}) + + def test_user_can_get_answers(self): + answersList = self.conn.query_all('answers') + if answersList: + res = self.client.get('/api/v1/questions/2/answers') + self.assertEqual(res.status_code, 200) + else: + res = self.client.get('/api/v1/questions/2/answers') + self.assertEqual(res.status_code, 404) + + def test_user_can_get_answer(self): + answersList = self.conn.query_all('answers') + if answersList and [ans for ans in questionsList if ans[3]==3 and ans[1]==2]: + res = self.client.get('/api/v1/questions/2/answers/3') + for answer in answersList: + temp = { + 'answerId': answer[3], + 'author': answer[4], + 'body': answer[2], + 'prefered': answer[5], + 'QuestionId': answer[1] + } + self.assertEqual(res.status_code, 200) + self.assertEqual(res.json, temp) + elif answersList and not [ans for ans in questionsList if ans[1]==2]: + res = self.client.get('/api/v1/questions/2/answers/3') + self.assertEqual(res.json, ['Answer not Found']) + self.assertEqual(res.status_code, 404) + + else: + res = self.client.get('/api/v1/questions/2/answers/3') + self.assertEqual(res.status_code, 404) + self.assertEqual(res.json, {'message': 'Question not found!'}) def test_user_can_post_question(self): question = { - "questionId": 34, - "topic": "computer science", - "body": "what is software?" - } - res = self.client.post('/api/v1/questions', json=question) - self.assertEqual(res.status_code, 201) + "questionId": 34, + "topic": "computer science", + "body": "what is software?" + } + if self.is_logged_in() == 'Kakai': + res = self.client.post('/api/v1/questions', json=question) + self.assertEqual(res.status_code, 201) + else: + res = self.client.post('/api/v1/questions', json=question) + self.assertEqual(res.status_code, 401) + def test_user_post_answer(self): answer = { @@ -35,7 +122,7 @@ def test_user_post_answer(self): "Qn_Id": 2 } res = self.client.post('/api/v1/questions/4/answers', json=answer) - self.assertEqual(res.status_code, 201) + self.assertEqual(res.status_code, 401) def test_user_can_update_question(self): @@ -44,8 +131,8 @@ def test_user_can_update_question(self): "body": "what is software?" } res = self.client.patch('/api/v1/questions/4', json=new_question) - self.assertEqual(res.status_code, 404) + self.assertEqual(res.status_code, 401) def test_user_can_delete_question(self): res = self.client.delete('/api/v1/questions/5') - self.assertEqual(res.status_code, 404) \ No newline at end of file + self.assertEqual(res.status_code, 401)