mirror of
https://github.com/kevin-DL/ShortMe-URL-Shortener.git
synced 2026-01-11 02:54:31 +00:00
Initial commit
This commit is contained in:
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "run.py"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tomer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
5
app/app.py
Normal file
5
app/app.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from app.setup.setup import create_app
|
||||
|
||||
CONFIG_FILE = 'settings.py'
|
||||
|
||||
app = create_app(config_file=CONFIG_FILE)
|
||||
222
app/client/static/CSS/styles.css
Normal file
222
app/client/static/CSS/styles.css
Normal file
@@ -0,0 +1,222 @@
|
||||
#heading-p {
|
||||
margin-top: 1.5%;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.title-bullet {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
display: block;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.title-bullet span {
|
||||
width: 70px;
|
||||
height: 6px;
|
||||
display: table;
|
||||
margin-top: -3px;
|
||||
background: #4b97ed;
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.form-control-borderless {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.form-control-borderless:hover, .form-control-borderless:active, .form-control-borderless:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn:not(:disabled):not(.disabled) {
|
||||
background: rgb(29, 184, 84);
|
||||
border-style: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn:not(:disabled):not(.disabled) {
|
||||
background: rgb(29, 184, 84);
|
||||
border-style: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
#long-link {
|
||||
margin-top: 2%;
|
||||
}
|
||||
|
||||
#credits {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
|
||||
.dh-highlight-text {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
/**/
|
||||
/*=================*/
|
||||
|
||||
.fa-check:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.fa-check {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.res-home-btn {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.fa {
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin: 5px 2px;
|
||||
}
|
||||
|
||||
.fa:hover {
|
||||
opacity: 0.7;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
|
||||
.fa-facebook {
|
||||
background: #3B5998;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.fa-twitter {
|
||||
background: #55ACEE;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.fa-linkedin {
|
||||
background: #007bb5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fa-reddit {
|
||||
background: #ff5700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.fa {
|
||||
box-sizing: unset;
|
||||
}
|
||||
|
||||
|
||||
/*--------------*/
|
||||
|
||||
.alert-box {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
display: none;
|
||||
margin-top: 3%;
|
||||
}
|
||||
|
||||
.c {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
font-size: 17px !important;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #2dd591);
|
||||
/*background: linear-gradient(-45deg, #06D6A0, #EF476F, #06D6A0, #FFD166);*/
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 50s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.credits-card {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
display: none;
|
||||
margin-top: 3%;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.dh-highlight-text {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #212529;
|
||||
padding: 10px;
|
||||
border-radius: 13px;
|
||||
border: none;
|
||||
box-shadow: 4px 4px 12px 0 #212529;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
#invalid-verification {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
#api-card {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
58
app/client/static/JS/main.js
Normal file
58
app/client/static/JS/main.js
Normal file
@@ -0,0 +1,58 @@
|
||||
function copyToClipboard() {
|
||||
let copyText = document.getElementById("copy-able");
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
|
||||
$("#copy-btn").click(function () {
|
||||
$("div.success").fadeIn(300).delay(1500).fadeOut(900);
|
||||
});
|
||||
|
||||
function checkUrl(event) {
|
||||
let value = document.getElementById('url-input').value;
|
||||
if (value.length < 5) {
|
||||
$("div.alert-warning").fadeIn(300).delay(1500).fadeOut(900);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkEmail(event) {
|
||||
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
let email = document.getElementById('email_input').value;
|
||||
let isEmailValid = re.test(String(email).toLowerCase());
|
||||
|
||||
if (email.length < 1) {
|
||||
$("div#cant-be-empty").fadeIn(300).delay(1500).fadeOut(900);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
|
||||
} else if (isEmailValid === false) {
|
||||
$("div#invalid-email").fadeIn(300).delay(1500).fadeOut(900);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function callGetToken() {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
let hostUrl = document.getElementById('host-url').getAttribute('value');
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
document.getElementById("token").innerHTML = this.responseText;
|
||||
}
|
||||
};
|
||||
xhttp.open("GET", `${hostUrl}/api/get_token`, true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
$('#verification').on('keyup', function () {
|
||||
var foo = $(this).val().split(" ").join("");
|
||||
if (foo.length > 0) {
|
||||
foo = foo.match(new RegExp('.{1,3}', 'g')).join(" ");
|
||||
}
|
||||
$(this).val(foo);
|
||||
});
|
||||
BIN
app/client/static/favicon.ico
Normal file
BIN
app/client/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
28
app/client/templates/404.html
Normal file
28
app/client/templates/404.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
<h2>🌴 404</h2>
|
||||
<h4><strong>We could not find this page 🙁</strong><br></h4>
|
||||
<p></p>
|
||||
|
||||
<a href="{{ url_for('index_blueprint.index') }}">
|
||||
<button class="btn btn-primary" id="go-home" type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Go back HOME
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
68
app/client/templates/api_doc.html
Normal file
68
app/client/templates/api_doc.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
|
||||
<h2><strong>The ShortMe API</strong><br></h2>
|
||||
<p>ShortMe's API will allow you to access ShortMe's URL shortening capabilities over
|
||||
API.</p>
|
||||
<h4>What you need to get started</h4>
|
||||
<ul>
|
||||
<li>Access token</li>
|
||||
</ul>
|
||||
|
||||
<h4>Shorten your first link!</h4>
|
||||
<ol>
|
||||
<li>Generate an <a href="{{ url_for('get_token_blueprint.get_token') }}"> access token</a>.</li>
|
||||
<li>You'll use the POST method to the <kbd>/api/shorten</kbd> endpoint. Append your
|
||||
access token as a header in your request. <br>
|
||||
Here's an example:
|
||||
<code><Authorization: Bearer {token}></code> <br> <br>
|
||||
Example call: <br>
|
||||
<kbd>POST</kbd> http://shortme.biz/api/shorten?url=http://www.longurl.com <br> <br>
|
||||
Example Response:
|
||||
<div class="code-block">
|
||||
<pre>{<br> 'short_url': shortme.biz/f3Jds,<br> 'original_url': 'http://www.longurl.com',<br> 'success':True<br>}</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<br>
|
||||
|
||||
|
||||
<h4>Get total URL clicks</h4>
|
||||
<p>You can use ShortMe's API to track the total number of clicks your short URL received</p>
|
||||
<ol>
|
||||
<li>You'll use the GET method to the <kbd>/api/total_clicks</kbd> endpoint. Access token
|
||||
is not required.<br>
|
||||
<br>
|
||||
Example call: <br>
|
||||
<kbd>GET</kbd> http://shortme.biz/api/total_clicks?url=shortme.biz/f3Jds <br> <br>
|
||||
Example Response:
|
||||
<div class="code-block">
|
||||
<pre>{<br> 'total': 2,<br> 'short_url': 'shortme.biz/f3Jds',<br> 'original_url': 'http://www.longurl.com',<br> 'success': True <br>}</pre>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
|
||||
{# <a href="{{ url_for('index_blueprint.index') }}">#}
|
||||
{# <button class="btn btn-primary" id="try-again-btn" type="button"#}
|
||||
{# style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">#}
|
||||
{# Try Again#}
|
||||
{# </button>#}
|
||||
{# </a>#}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
44
app/client/templates/base.html
Normal file
44
app/client/templates/base.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="eng">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>URL shortner</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='CSS/styles.css') }}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="dh-highlight-text">
|
||||
<div class="container">
|
||||
<h1 class="clearmargin clearpadding">ShortMe</h1>
|
||||
<p id="heading-p">ShortMe is a free tool to shorten URLs. Create a short & memorable URL in seconds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
|
||||
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
|
||||
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
|
||||
<script src="{{ url_for('static', filename='../static/JS/main.js') }}"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
39
app/client/templates/error.html
Normal file
39
app/client/templates/error.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
<h4><strong>Yikes! We could not shorten this URL 🙁</strong><br></h4>
|
||||
<p> <strong>The URL you entered is not valid.</strong></p>
|
||||
<ul>
|
||||
<li class="bullet"><i class="fa fa-check" style="color: rgb(0,128,255);"></i>Make sure
|
||||
the website is online<br></li>
|
||||
<li class="bullet"><i class="fa fa-check" style="color: rgb(0,128,255);"></i>Check if
|
||||
the URL is valid<br></li>
|
||||
|
||||
<li class="bullet"><i class="fa fa-check" style="color: rgb(0,128,255);"></i> The
|
||||
URL may have
|
||||
been blocked<br></li>
|
||||
<li class="bullet"><i class="fa fa-check" style="color: rgb(0,128,255);"></i> The
|
||||
url may have
|
||||
been reported<br></li>
|
||||
</ul>
|
||||
<a href="{{ url_for('index_blueprint.index') }}">
|
||||
<button class="btn btn-primary" id="try-again-btn" type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Try Again
|
||||
</button>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
42
app/client/templates/get_token.html
Normal file
42
app/client/templates/get_token.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
|
||||
<h1 id="token"></h1>
|
||||
<h1>Get your free ShortMe's API token</h1>
|
||||
<p>Please enter your email below to get a verification code</p>
|
||||
<div class="card">
|
||||
<form class="card-body d-flex align-items-center" method="POST"
|
||||
onsubmit="return checkEmail(event)"
|
||||
action="{{ url_for('send_otp_blueprint.email_validation') }}">
|
||||
|
||||
<label for="email_input"></label><input
|
||||
class="form-control form-control-lg flex-shrink-1 form-control-borderless"
|
||||
placeholder="example@email.com" id="email_input" name="email"/>
|
||||
<button class="btn btn-primary" type="submit"
|
||||
style="width: 200px; height: 51px; background-color: rgb(41, 41, 41); transition: border 0.2s ease;">
|
||||
Send Code
|
||||
</button>
|
||||
<br>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<p>*If you alredy verified your email you will get the API token</p>
|
||||
<div class="alert-box alert-warning" id="invalid-email">Please enter a valid email address
|
||||
</div>
|
||||
<div class="alert-box alert-warning" id="cant-be-empty">This feild cannot be empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
57
app/client/templates/index.html
Normal file
57
app/client/templates/index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %} {% block content %}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width: 850px;">
|
||||
<div class="card-body">
|
||||
<form class="d-flex align-items-center" method="POST" onsubmit="return checkUrl(event)"
|
||||
action="{{ url_for('shorten_url_blueprint.shorten_url') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
|
||||
fill="none" class="d-none d-sm-block h4 text-body m-0">
|
||||
<path
|
||||
d="M13.8284 10.1716C12.2663 8.60948 9.73367 8.60948 8.17157 10.1716L4.17157 14.1716C2.60948 15.7337 2.60948 18.2663 4.17157 19.8284C5.73367 21.3905 8.26633 21.3905 9.82843 19.8284L10.93 18.7269M10.1716 13.8284C11.7337 15.3905 14.2663 15.3905 15.8284 13.8284L19.8284 9.82843C21.3905 8.26633 21.3905 5.73367 19.8284 4.17157C18.2663 2.60948 15.7337 2.60948 14.1716 4.17157L13.072 5.27118"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<input class="form-control form-control-lg flex-shrink-1 form-control-borderless"
|
||||
placeholder="Enter the link here" id="url-input" name="original_url"/>
|
||||
<button class="btn btn-primary" type="submit"
|
||||
style="width: 200px; height: 51px; background-color: rgb(41, 41, 41); transition: border 0.2s ease;">
|
||||
Shorten
|
||||
</button>
|
||||
|
||||
</form>
|
||||
<div class="alert-box alert-warning">This field cannot be empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-center" id="">
|
||||
<div class="">
|
||||
<p></p>
|
||||
<h6 class="text-dark font-weight-bold">Check out <a href="{{ url_for('api_doc_blueprint.api_doc') }}">ShortMe's
|
||||
API</a></h6>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center" id="credits">
|
||||
<div class="card credits-card">
|
||||
<h6 class="text-dark font-weight-bold"><i class="fa fa-code c"></i> with <i class="fa fa-heart c"></i>
|
||||
by <a href="https://www.linkedin.com/in/tomer-chaim/">Tomer</a></h6>
|
||||
|
||||
<h6 class="text-dark font-weight-bold">Check out my <a
|
||||
href="https://github.com/acrobaticPanicc/">GitHub</a></h6>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
26
app/client/templates/total-clicks.html
Normal file
26
app/client/templates/total-clicks.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
<h4><strong>Total URL Clicks</strong><br></h4>
|
||||
<p id="total-p"> Your URL has been clicked {{ total_clicks }} times so far.<br></p>
|
||||
<p id="long-link-1"></p>
|
||||
|
||||
<a href="{{ url_for('index_blueprint.index') }}">
|
||||
<button class="btn btn-primary" id="copy-btn" type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Shorten another URL
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
55
app/client/templates/verify.html
Normal file
55
app/client/templates/verify.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
|
||||
<h1 id="token"></h1>
|
||||
<h1><span>✉</span> Please check your inbox</h1>
|
||||
<p>We've sent a verification code to your email. Please enter it below and click
|
||||
verify.</p>
|
||||
|
||||
<div class="card">
|
||||
<form class="card-body d-flex align-items-center" method="POST"
|
||||
onsubmit="return checkEmail(event)"
|
||||
action="{{ url_for('verify_code_blueprint.validate_code') }}">
|
||||
|
||||
<label for="email_input"></label>
|
||||
<input maxlength="7"
|
||||
class="form-control form-control-lg flex-shrink-1 form-control-borderless"
|
||||
placeholder="000 000" id="verification" name="verification"/>
|
||||
<button class="btn btn-primary" type="submit"
|
||||
style="width: 200px; height: 51px; background-color: rgb(41, 41, 41); transition: border 0.2s ease;">
|
||||
Verify
|
||||
</button>
|
||||
<br>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
{% if is_verified == 'False' %}
|
||||
<div class="alert-box alert-warning" id="invalid-verification">
|
||||
Your email already exist in our database but it's not activated.<br>Please check
|
||||
your
|
||||
inbox for the verification
|
||||
code.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_code_valid %}
|
||||
<div class="alert-box alert-warning" id="invalid-verification">Wrong code, plesee try
|
||||
again
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
35
app/client/templates/your_api_token.html
Normal file
35
app/client/templates/your_api_token.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
|
||||
<h1 id="token"></h1>
|
||||
<h1>Your API token</h1>
|
||||
|
||||
<form class="card-body d-flex align-items-center">
|
||||
<input spellcheck="false" readonly
|
||||
class="card form-control form-control form-control-lg flex-shrink-1 form-control-borderless"
|
||||
id="copy-able" type="text" value="{{ auth_token }}"
|
||||
style="margin-left: 1.4%;margin-right: 1.4%;height: 51px;border-width: 1px;">
|
||||
|
||||
<button class="btn btn-primary" onclick="copyToClipboard()" id="copy-btn" type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Copy Token
|
||||
</button>
|
||||
</form>
|
||||
<div class="alert-box success">Token copied to clipboard</div>
|
||||
<div class="alert-box alert-warning" id="cant-be-empty">This feild cannot be empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
70
app/client/templates/your_short_url.html
Normal file
70
app/client/templates/your_short_url.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card m-auto" style="max-width:850px">
|
||||
<div class="card-body">
|
||||
<h4><strong>Your shortened URL</strong><br></h4>
|
||||
<p>Copy the shortened link to share it.<br></p>
|
||||
|
||||
<div class="card">
|
||||
<form class="card-body d-flex align-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
|
||||
fill="none" class="d-none d-sm-block h4 text-body m-0">
|
||||
<path d="M13.8284 10.1716C12.2663 8.60948 9.73367 8.60948 8.17157 10.1716L4.17157 14.1716C2.60948 15.7337 2.60948 18.2663 4.17157 19.8284C5.73367 21.3905 8.26633 21.3905 9.82843 19.8284L10.93 18.7269M10.1716 13.8284C11.7337 15.3905 14.2663 15.3905 15.8284 13.8284L19.8284 9.82843C21.3905 8.26633 21.3905 5.73367 19.8284 4.17157C18.2663 2.60948 15.7337 2.60948 14.1716 4.17157L13.072 5.27118"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
<input class="card form-control form-control form-control-lg flex-shrink-1 form-control-borderless"
|
||||
id="copy-able" type="text" value="{{ full_short_url }}"
|
||||
style="margin-left: 1.4%;margin-right: 1.4%;height: 51px;border-width: 1px;">
|
||||
|
||||
<button class="btn btn-primary" onclick="copyToClipboard()" id="copy-btn"
|
||||
type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Copy URL
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="alert-box success">URL copied to clipboard</div>
|
||||
|
||||
<form action="{{ url_for('total_clicks_blueprint.total_clicks') }}">
|
||||
|
||||
<input type="hidden" id="new_url" name="short_url" value="{{ short_url }}">
|
||||
<p id="long-link">Original URL: <a href="#">{{ original_url }}</a></p>
|
||||
<p id="long-link-1"><a id="total-clicks-link"
|
||||
onclick="this.closest('form').submit();return false;" href="">Track</a>
|
||||
the total clicks of your shortened URL in
|
||||
real-time</p>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h4><strong>Share your URL</strong><br></h4>
|
||||
</div>
|
||||
<div class="social">
|
||||
<a href="#" class="fa fa-facebook"></a>
|
||||
<a href="#" class="fa fa-twitter"></a>
|
||||
<a href="#" class="fa fa-linkedin"></a>
|
||||
<a href="#" class="fa fa-reddit"></a>
|
||||
</div>
|
||||
<div class="res-home-btn">
|
||||
<a href="{{ url_for('index_blueprint.index') }}">
|
||||
<button class="btn btn-primary" id="go-home-btn" type="button"
|
||||
style="width: 200px;height: 51px;background-color: rgb(41,41,41);transition: border 0.2s ease;">
|
||||
Go back home
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% endblock %}
|
||||
0
app/server/__init__.py
Normal file
0
app/server/__init__.py
Normal file
0
app/server/api/__init__.py
Normal file
0
app/server/api/__init__.py
Normal file
112
app/server/api/api.py
Normal file
112
app/server/api/api.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# ------- standard library imports -------
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.extensions import db
|
||||
from app.server.api.api_auth import auth
|
||||
from app.server.db.models import Url, AuthToken
|
||||
|
||||
shorten_parser = reqparse.RequestParser()
|
||||
total_clicks_parser = reqparse.RequestParser()
|
||||
|
||||
shorten_parser.add_argument('url', type=str, help='URL parameter is missing', required=True)
|
||||
total_clicks_parser.add_argument('url', type=str, help='Short URL is missing', required=True)
|
||||
|
||||
|
||||
class Shorten(Resource):
|
||||
"""
|
||||
Return a short URL from a given URL
|
||||
URL: /api/shorten
|
||||
METHOD: POST
|
||||
PARAMS: url: long URL
|
||||
HEADERS: Authorization: (Bearer <key>)
|
||||
RETURN: dictionary with the short URL
|
||||
"""
|
||||
|
||||
decorators = [auth.login_required]
|
||||
|
||||
@staticmethod
|
||||
def post():
|
||||
args = shorten_parser.parse_args()
|
||||
url = args['url']
|
||||
original_url = url if url.startswith('http') else ('http://' + url)
|
||||
|
||||
try:
|
||||
res = requests.get(original_url)
|
||||
|
||||
if res.status_code == 200:
|
||||
url = Url(original_url=original_url)
|
||||
|
||||
db.session.add(url)
|
||||
db.session.commit()
|
||||
|
||||
return dict(
|
||||
short_url=url.short_url,
|
||||
original_url=url.original_url,
|
||||
success=True
|
||||
), 200
|
||||
|
||||
else:
|
||||
"""in case of page_not_found response from the URL given"""
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not shorten this URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
except (requests.exceptions.MissingSchema, requests.exceptions.ConnectionError, requests.exceptions.InvalidURL):
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not shorten this URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
|
||||
class TotalClicks(Resource):
|
||||
"""
|
||||
return the total clicks of a short URL
|
||||
URL: /api/total_clicks
|
||||
METHOD: GET
|
||||
PARAMS: short url
|
||||
RETURN: dictionary with the total short url visits.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
args = total_clicks_parser.parse_args()
|
||||
url = args['url'].split('/')[-1]
|
||||
|
||||
try:
|
||||
url = Url.query.filter_by(short_url=url).first()
|
||||
|
||||
return dict(
|
||||
total=url.visits,
|
||||
short_url=url.short_url,
|
||||
original_url=url.original_url,
|
||||
success=True
|
||||
), 200
|
||||
|
||||
except AttributeError:
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not find the URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
|
||||
class GetToken(Resource):
|
||||
"""
|
||||
Return a unique API authorization token. Used only internaly by the web app.
|
||||
URL: /api/get_token
|
||||
METHOD: GET
|
||||
RETURN: unique API authorization token
|
||||
AuthToken: Required
|
||||
"""
|
||||
decorators = [auth.login_required]
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
token = AuthToken()
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
return str(token)
|
||||
12
app/server/api/api_auth.py
Normal file
12
app/server/api/api_auth.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from flask_httpauth import HTTPTokenAuth
|
||||
|
||||
from app.server.db.models import AuthToken
|
||||
|
||||
auth = HTTPTokenAuth(scheme='Bearer')
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
"""Used to verify user's Token"""
|
||||
return AuthToken.query.filter_by(auth_token=token).first()
|
||||
|
||||
0
app/server/db/__init__.py
Normal file
0
app/server/db/__init__.py
Normal file
4
app/server/db/extensions.py
Normal file
4
app/server/db/extensions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
87
app/server/db/models.py
Normal file
87
app/server/db/models.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# ------- standard library imports -------
|
||||
import random
|
||||
import string
|
||||
import secrets
|
||||
from random import choices
|
||||
from datetime import datetime
|
||||
|
||||
# ------- local imports -------
|
||||
from sqlalchemy import ForeignKey
|
||||
from .extensions import db
|
||||
|
||||
|
||||
class Url(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
original_url = db.Column(db.String(512))
|
||||
short_url = db.Column(db.String(5), unique=True)
|
||||
visits = db.Column(db.Integer, default=0)
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.short_url = self.generate_short_url()
|
||||
|
||||
def generate_short_url(self):
|
||||
characters = string.digits + string.ascii_letters
|
||||
short_url = ''.join(choices(characters, k=5))
|
||||
url = self.query.filter_by(short_url=short_url).first()
|
||||
|
||||
if url:
|
||||
return self.generate_short_url()
|
||||
|
||||
return short_url
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.original_url}, {self.visits}'
|
||||
|
||||
|
||||
class AuthToken(db.Model):
|
||||
__tablename__ = 'auth_token'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
auth_token = db.Column(db.String(16))
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
email_id = db.Column(db.Integer, ForeignKey('email.id'))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.auth_token = self.generate_auth_token() if self.auth_token is None else self.auth_token
|
||||
|
||||
@staticmethod
|
||||
def generate_auth_token():
|
||||
generated_token = secrets.token_hex(16)
|
||||
return generated_token
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.auth_token}'
|
||||
|
||||
|
||||
class Email(db.Model):
|
||||
__tablename__ = 'email'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(512), unique=True)
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
is_verified = db.Column(db.Boolean, default=False)
|
||||
verification_code = db.relationship('VerificationCode', backref='email')
|
||||
auth_token = db.relationship('AuthToken', backref='email')
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.email}, {self.verification_code}, {self.auth_token}'
|
||||
|
||||
|
||||
class VerificationCode(db.Model):
|
||||
__tablename__ = 'verification_code'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email_id = db.Column(db.Integer, ForeignKey('email.id'))
|
||||
verification_code = db.Column(db.String, unique=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.verification_code = self.generate_verification_code()
|
||||
|
||||
@staticmethod
|
||||
def generate_verification_code():
|
||||
verification_code = ' '.join([str(random.randint(0, 999)).zfill(3) for _ in range(2)])
|
||||
return verification_code
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.verification_code}'
|
||||
9
app/server/routes/api_doc.py
Normal file
9
app/server/routes/api_doc.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
api_doc_blueprint = Blueprint('api_doc_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@api_doc_blueprint.route('/api_doc')
|
||||
def api_doc():
|
||||
return render_template('api_doc.html')
|
||||
9
app/server/routes/error.py
Normal file
9
app/server/routes/error.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
error_blueprint = Blueprint('error_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@error_blueprint.route('/error')
|
||||
def error():
|
||||
return render_template('error.html')
|
||||
9
app/server/routes/get_token.py
Normal file
9
app/server/routes/get_token.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
get_token_blueprint = Blueprint('get_token_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@get_token_blueprint.route('/get_token')
|
||||
def get_token():
|
||||
return render_template('get_token.html')
|
||||
9
app/server/routes/index.py
Normal file
9
app/server/routes/index.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import render_template, Blueprint
|
||||
|
||||
index_blueprint = Blueprint('index_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@index_blueprint.route("/")
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
0
app/server/routes/internal/__init__.py
Normal file
0
app/server/routes/internal/__init__.py
Normal file
13
app/server/routes/internal/favicon.py
Normal file
13
app/server/routes/internal/favicon.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# ------- standard library imports -------
|
||||
import os
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, send_from_directory
|
||||
|
||||
app_blueprint = Blueprint('app_blueprint', __name__)
|
||||
|
||||
|
||||
@app_blueprint.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app_blueprint.root_path, 'static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
24
app/server/routes/internal/redirect_to_url.py
Normal file
24
app/server/routes/internal/redirect_to_url.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, redirect, url_for
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.models import Url
|
||||
from app.server.db.extensions import db
|
||||
|
||||
redirect_to_url_blueprint = Blueprint('redirect_to_url_blueprint', __name__)
|
||||
|
||||
|
||||
@redirect_to_url_blueprint.route('/<short_url>')
|
||||
def redirect_to_url(short_url):
|
||||
"""
|
||||
This function will query the database with the short_url and will
|
||||
redirect to the original url if it exist in the database.
|
||||
"""
|
||||
url = Url.query.filter_by(short_url=short_url).first()
|
||||
|
||||
if url:
|
||||
url.visits = url.visits + 1
|
||||
db.session.commit()
|
||||
return redirect(url.original_url)
|
||||
|
||||
return redirect(url_for('page_not_found_blueprint.page_not_found'))
|
||||
50
app/server/routes/internal/send_verification_code.py
Normal file
50
app/server/routes/internal/send_verification_code.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# ------- standard library imports -------
|
||||
import re
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask_mail import Mail, Message
|
||||
from flask import Blueprint, request, redirect, url_for, session
|
||||
|
||||
# ------- local imports -------
|
||||
from app import app
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import VerificationCode, Email
|
||||
|
||||
send_otp_blueprint = Blueprint('send_otp_blueprint', __name__)
|
||||
|
||||
|
||||
@send_otp_blueprint.route('/email_validation', methods=['POST'])
|
||||
def email_validation():
|
||||
input_email = request.form['email']
|
||||
pattern = r'[^@]+@[^@]+\.[^@]+'
|
||||
is_email_valid = re.match(pattern, input_email)
|
||||
|
||||
if is_email_valid:
|
||||
email = Email.query.filter_by(email=input_email).first()
|
||||
|
||||
if email and email.is_verified:
|
||||
auth_token = Email.query.filter_by(email=input_email).first().auth_token
|
||||
return redirect(url_for('your_api_token_blueprint.your_api_token', auth_token=auth_token))
|
||||
|
||||
elif email and not email.is_verified:
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code', is_verified=False))
|
||||
|
||||
else:
|
||||
if not email:
|
||||
session['user_email'] = input_email
|
||||
|
||||
email = Email(email=input_email)
|
||||
verification_code = VerificationCode(email=email)
|
||||
db.session.add(verification_code, email)
|
||||
db.session.commit()
|
||||
|
||||
# print('user added to db')
|
||||
# print('verification_code:')
|
||||
# print(verification_code)
|
||||
# print('*' * 33)
|
||||
|
||||
mail = Mail(app.app)
|
||||
msg = Message('ShortMe Verification Code', sender='shortme.biz@gmail.com', recipients=[email.email])
|
||||
msg.body = str(f'Hi!\nThis is your verification code: {verification_code.verification_code}')
|
||||
mail.send(msg)
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code'))
|
||||
40
app/server/routes/internal/shorten_url.py
Normal file
40
app/server/routes/internal/shorten_url.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# ------- standard library imports -------
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, request, redirect, url_for
|
||||
|
||||
# ------- local imports -------
|
||||
from app import app
|
||||
|
||||
shorten_url_blueprint = Blueprint('shorten_url_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@shorten_url_blueprint.route('/shorten', methods=['POST'])
|
||||
def shorten_url():
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
original_url = request.form['original_url']
|
||||
shorten_endpoint = base_url + 'api/shorten'
|
||||
|
||||
params = {
|
||||
'url': original_url
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {app.app.secret_key}'
|
||||
}
|
||||
|
||||
response = requests.post(shorten_endpoint, headers=headers, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response = json.loads(response.text)
|
||||
short_url = response['short_url']
|
||||
original_url = response['original_url']
|
||||
|
||||
return redirect(url_for('your_short_url_blueprint.your_short_url',
|
||||
short_url=short_url,
|
||||
original_url=original_url))
|
||||
else:
|
||||
return redirect(url_for('error_blueprint.error'))
|
||||
9
app/server/routes/page_not_found.py
Normal file
9
app/server/routes/page_not_found.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
page_not_found_blueprint = Blueprint('page_not_found_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@page_not_found_blueprint.route('/page_not_found')
|
||||
def page_not_found():
|
||||
return render_template('404.html')
|
||||
24
app/server/routes/total_clicks.py
Normal file
24
app/server/routes/total_clicks.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# ------- standard library imports -------
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
total_clicks_blueprint = Blueprint('total_clicks_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@total_clicks_blueprint.route('/total_clicks')
|
||||
def total_clicks():
|
||||
short_url = request.args['short_url']
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
total_clicks_endpoint = base_url + 'api/total_clicks'
|
||||
|
||||
params = {
|
||||
'url': short_url
|
||||
}
|
||||
|
||||
response = requests.get(total_clicks_endpoint, params=params)
|
||||
total_url_clicks = json.loads(response.text)['total']
|
||||
return render_template('total-clicks.html', total_clicks=total_url_clicks)
|
||||
32
app/server/routes/verify_code.py
Normal file
32
app/server/routes/verify_code.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import VerificationCode, AuthToken, Email
|
||||
|
||||
verify_code_blueprint = Blueprint('verify_code_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@verify_code_blueprint.route('/verify', methods=['GET', 'POST'])
|
||||
def enter_verification_code():
|
||||
is_verified = request.args.get('is_verified')
|
||||
is_code_valid = request.args.get('is_code_valid')
|
||||
return render_template('verify.html', is_code_valid=is_code_valid, is_verified=is_verified)
|
||||
|
||||
|
||||
@verify_code_blueprint.route('/validate_code', methods=['POST'])
|
||||
def validate_code():
|
||||
input_code = request.form['verification']
|
||||
code = VerificationCode.query.filter_by(verification_code=input_code).first()
|
||||
|
||||
if code:
|
||||
email = Email.query.filter_by(email=session['user_email']).first()
|
||||
email.is_verified = True
|
||||
auth_token = AuthToken(email=email)
|
||||
db.session.add(auth_token)
|
||||
db.session.commit()
|
||||
return redirect(url_for('your_api_token_blueprint.your_api_token', auth_token=auth_token))
|
||||
|
||||
else:
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code', is_code_valid=False))
|
||||
15
app/server/routes/your_api_token.py
Normal file
15
app/server/routes/your_api_token.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template, request, url_for
|
||||
|
||||
your_api_token_blueprint = Blueprint('your_api_token_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@your_api_token_blueprint.route('/your_api_token')
|
||||
def your_api_token():
|
||||
auth_token = request.args.get("auth_token")
|
||||
|
||||
if auth_token:
|
||||
return render_template('your_api_token.html', auth_token=auth_token)
|
||||
|
||||
else:
|
||||
return render_template(url_for('page_not_found_blueprint.page_not_found'))
|
||||
19
app/server/routes/your_short_url.py
Normal file
19
app/server/routes/your_short_url.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
your_short_url_blueprint = Blueprint('your_short_url_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@your_short_url_blueprint.route('/your_short_url')
|
||||
def your_short_url():
|
||||
original_url = request.args['original_url']
|
||||
short_url = request.args['short_url']
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
full_short_url = f'{base_url}{short_url}'
|
||||
full_short_url = full_short_url.replace('http://www.', '')
|
||||
|
||||
return render_template('your_short_url.html',
|
||||
original_url=original_url,
|
||||
short_url=short_url,
|
||||
full_short_url=full_short_url)
|
||||
26
app/setup/settings.py
Normal file
26
app/setup/settings.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = os.environ.get('SQLALCHEMY_TRACK_MODIFICATIONS')
|
||||
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
|
||||
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
|
||||
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = True
|
||||
|
||||
"""
|
||||
:::::::: .env file content ::::::::
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:///db.sqlite3
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
||||
SECRET_KEY=randomKey
|
||||
|
||||
MAIL_SERVER=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=email@email.com
|
||||
MAIL_PASSWORD=password
|
||||
"""
|
||||
69
app/setup/setup.py
Normal file
69
app/setup/setup.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# ------- standard library imports -------
|
||||
import os
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Flask
|
||||
from flask_restful import Api
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import AuthToken
|
||||
from app.server.routes.index import index_blueprint
|
||||
from app.server.routes.internal.redirect_to_url import redirect_to_url_blueprint
|
||||
from app.server.routes.internal.favicon import app_blueprint
|
||||
from app.server.routes.internal.send_verification_code import send_otp_blueprint
|
||||
from app.server.routes.internal.shorten_url import shorten_url_blueprint
|
||||
from app.server.routes.your_short_url import your_short_url_blueprint
|
||||
from app.server.routes.total_clicks import total_clicks_blueprint
|
||||
from app.server.routes.error import error_blueprint
|
||||
from app.server.routes.page_not_found import page_not_found_blueprint
|
||||
from app.server.routes.api_doc import api_doc_blueprint
|
||||
from app.server.routes.get_token import get_token_blueprint
|
||||
from app.server.routes.your_api_token import your_api_token_blueprint
|
||||
from app.server.routes.verify_code import verify_code_blueprint
|
||||
|
||||
from app.server.api.api import Shorten, TotalClicks, GetToken
|
||||
|
||||
|
||||
def create_app(config_file):
|
||||
"""
|
||||
Creating and returning the app
|
||||
"""
|
||||
app_path = os.path.dirname(os.path.abspath(__file__))
|
||||
project_folder = os.path.expanduser(app_path)
|
||||
load_dotenv(os.path.join(project_folder, '.env'))
|
||||
|
||||
app = Flask(__name__, template_folder='../client/templates', static_folder='../client/static')
|
||||
api = Api(app)
|
||||
app.config.from_pyfile(config_file)
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
app_auth_token = app.secret_key
|
||||
auth_token = AuthToken(auth_token=app_auth_token)
|
||||
db.session.add(auth_token)
|
||||
db.session.commit()
|
||||
|
||||
api.add_resource(Shorten, '/api/shorten')
|
||||
api.add_resource(GetToken, '/api/get_token')
|
||||
api.add_resource(TotalClicks, '/api/total_clicks')
|
||||
|
||||
app.register_blueprint(index_blueprint)
|
||||
app.register_blueprint(page_not_found_blueprint)
|
||||
app.register_blueprint(redirect_to_url_blueprint)
|
||||
app.register_blueprint(your_short_url_blueprint)
|
||||
app.register_blueprint(total_clicks_blueprint)
|
||||
app.register_blueprint(error_blueprint)
|
||||
app.register_blueprint(app_blueprint)
|
||||
app.register_blueprint(api_doc_blueprint)
|
||||
app.register_blueprint(get_token_blueprint)
|
||||
app.register_blueprint(send_otp_blueprint)
|
||||
app.register_blueprint(verify_code_blueprint)
|
||||
app.register_blueprint(your_api_token_blueprint)
|
||||
app.register_blueprint(shorten_url_blueprint)
|
||||
return app
|
||||
0
app/tests/api_testing/__init__.py
Normal file
0
app/tests/api_testing/__init__.py
Normal file
18
app/tests/api_testing/api_helper.py
Normal file
18
app/tests/api_testing/api_helper.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import json
|
||||
from app.app import app as a
|
||||
|
||||
|
||||
class ApiHelper:
|
||||
HEADERS = {
|
||||
'Authorization': f'Bearer {a.secret_key}'
|
||||
}
|
||||
|
||||
def get_auth_token(self, app):
|
||||
r = app.test_client().get(
|
||||
'/api/get_token', headers=self.HEADERS
|
||||
)
|
||||
|
||||
print(r.get_data(as_text=True))
|
||||
|
||||
key = json.loads(r.get_data(as_text=True))
|
||||
return key
|
||||
26
app/tests/api_testing/settings.py
Normal file
26
app/tests/api_testing/settings.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = os.environ.get('SQLALCHEMY_TRACK_MODIFICATIONS')
|
||||
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
|
||||
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
|
||||
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = True
|
||||
|
||||
"""
|
||||
:::::::: .env file content ::::::::
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:///db.sqlite3
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
||||
SECRET_KEY=randomKey
|
||||
|
||||
MAIL_SERVER=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=email@email.com
|
||||
MAIL_PASSWORD=password
|
||||
"""
|
||||
81
app/tests/api_testing/test_api.py
Normal file
81
app/tests/api_testing/test_api.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# ------- standard library imports -------
|
||||
import json
|
||||
import unittest
|
||||
|
||||
# ------- local imports -------
|
||||
from time import sleep
|
||||
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import Url
|
||||
from app.app import create_app
|
||||
from app.tests.api_testing.api_helper import ApiHelper
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
VALID_URL = 'youtube.com'
|
||||
INVALID_URL = 'www.youtube.com/what?a=b&c=d'
|
||||
INVALID_PARAM = 'INVALID'
|
||||
|
||||
def setUp(self):
|
||||
self.helper = ApiHelper()
|
||||
self.app = create_app(config_file='settings.py')
|
||||
sleep(1)
|
||||
self.key = self.helper.get_auth_token(self.app)
|
||||
|
||||
def test_01_shorten_url_success(self):
|
||||
response = self.app.test_client().post(
|
||||
'/api/shorten',
|
||||
headers={'Authorization': f'Bearer {self.key}'},
|
||||
data={'url': self.VALID_URL}
|
||||
)
|
||||
|
||||
res_dict = json.loads(response.get_data(as_text=True))
|
||||
|
||||
short_url = res_dict['short_url']
|
||||
original_url = res_dict['original_url']
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(short_url), 5)
|
||||
self.assertEqual(original_url, 'http://youtube.com')
|
||||
|
||||
def test_02_shorten_url_fail(self):
|
||||
response = self.app.test_client().post(
|
||||
'/api/shorten',
|
||||
headers={'Authorization': f'Bearer {self.key}'},
|
||||
data={'url': self.INVALID_URL},
|
||||
)
|
||||
|
||||
res_dict = json.loads(response.get_data(as_text=True))
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(res_dict['success'], False)
|
||||
self.assertEqual(res_dict['message'], 'could not shorten this URL (page_not_found)')
|
||||
|
||||
def test_03_total_clicks(self):
|
||||
# add url to db
|
||||
response = self.app.test_client().post(
|
||||
'/api/shorten',
|
||||
headers={'Authorization': f'Bearer {self.key}'},
|
||||
data={'url': 'youtube.com'},
|
||||
)
|
||||
|
||||
with self.app.app_context():
|
||||
url = Url.query.filter_by(original_url='http://youtube.com').first()
|
||||
short_url = url.short_url
|
||||
|
||||
response = self.app.test_client().get(
|
||||
'/api/total_clicks',
|
||||
data={'url': short_url},
|
||||
)
|
||||
|
||||
res_dict = json.loads(response.get_data(as_text=True))
|
||||
self.assertEqual(res_dict['total'], 0)
|
||||
|
||||
def tearDown(self):
|
||||
db.session.remove()
|
||||
with self.app.app_context():
|
||||
db.drop_all()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
0
app/tests/front_end_testing/__init__.py
Normal file
0
app/tests/front_end_testing/__init__.py
Normal file
0
app/tests/front_end_testing/index/__init__.py
Normal file
0
app/tests/front_end_testing/index/__init__.py
Normal file
40
app/tests/front_end_testing/index/index.py
Normal file
40
app/tests/front_end_testing/index/index.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from app.tests.utilities import selenium_utility
|
||||
|
||||
|
||||
class Index(selenium_utility.SeleniumUtility):
|
||||
_heading_locator = '//p[@id="heading-p"]'
|
||||
_url_input_locator = '//input[@id="url-input"]'
|
||||
_shorten_button_locator = '//button[@type="submit"]'
|
||||
_enter_url_warning = '//div[@class="alert-box alert-warning"]'
|
||||
_try_again_button = '//button[@id="try-again-btn"]'
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
super().__init__(driver)
|
||||
self.url_input = self.get_element(self._url_input_locator)
|
||||
self.shorten_button = self.get_element(self._shorten_button_locator)
|
||||
|
||||
def get_heading_text(self):
|
||||
return self.get_element(self._heading_locator).text
|
||||
|
||||
def enter_valid_url(self):
|
||||
self.url_input = self.get_element(self._url_input_locator)
|
||||
self.url_input.click()
|
||||
self.url_input.send_keys('youtube.com')
|
||||
|
||||
def enter_invalid_url(self):
|
||||
self.url_input.click()
|
||||
self.url_input.send_keys('https://www.youtube.com/what?a=b&c=d')
|
||||
|
||||
def click_shorten_button(self):
|
||||
self.shorten_button = self.get_element(self._shorten_button_locator)
|
||||
self.shorten_button.click()
|
||||
|
||||
def check_warning_present(self):
|
||||
return self.get_element(self._enter_url_warning).is_displayed()
|
||||
|
||||
def get_current_url(self):
|
||||
return self.driver.current_url.split('/')[-1]
|
||||
|
||||
def click_try_again(self):
|
||||
self.wait_for_element(self._try_again_button).click()
|
||||
0
app/tests/front_end_testing/result/__init__.py
Normal file
0
app/tests/front_end_testing/result/__init__.py
Normal file
29
app/tests/front_end_testing/result/result.py
Normal file
29
app/tests/front_end_testing/result/result.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from app.tests.utilities import selenium_utility
|
||||
import pyperclip as pc
|
||||
|
||||
|
||||
class Result(selenium_utility.SeleniumUtility):
|
||||
_url_input_locator = '//input[@id="copy-able"]'
|
||||
_copy_button_locator = '//button[@id="copy-btn"]'
|
||||
_total_clicks_url = '//a[@id="total-clicks-link"]'
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
super().__init__(driver)
|
||||
|
||||
self.url_input = self.get_element(self._url_input_locator)
|
||||
self.copy_button = self.get_element(self._copy_button_locator)
|
||||
|
||||
def get_input_text(self):
|
||||
return self.get_element(self._url_input_locator).get_attribute('value')
|
||||
|
||||
def click_copy_button(self):
|
||||
self.copy_button.click()
|
||||
|
||||
def go_to_total_clicks(self):
|
||||
self.get_element(self._total_clicks_url).click()
|
||||
|
||||
@staticmethod
|
||||
def get_clipboard_content():
|
||||
text = pc.paste()
|
||||
return text
|
||||
78
app/tests/front_end_testing/test_front_end.py
Normal file
78
app/tests/front_end_testing/test_front_end.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# ------- standard library imports -------
|
||||
import unittest
|
||||
import multiprocessing
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from selenium import webdriver
|
||||
from flask_testing import LiveServerTestCase
|
||||
|
||||
# ------- local imports -------
|
||||
from app.app import create_app
|
||||
from app.tests.front_end_testing.index.index import Index
|
||||
from app.tests.front_end_testing.result.result import Result
|
||||
from app.tests.front_end_testing.total_clicks.total_clicks import TotalClicks
|
||||
|
||||
|
||||
class TestAppSuccess(LiveServerTestCase, unittest.TestCase):
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
def create_app(self):
|
||||
app = create_app(config_file='tests/front_end_testing/settings.py')
|
||||
app.testing = True
|
||||
app.config.update(LIVESERVER_PORT=5002)
|
||||
return app
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.chrome_browser = webdriver.Chrome()
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
"""Test the index page."""
|
||||
self.chrome_browser.get(self.get_server_url())
|
||||
index = Index(self.chrome_browser)
|
||||
# test that empty input shows error
|
||||
index.click_shorten_button()
|
||||
self.assertEqual(index.check_warning_present(), True)
|
||||
|
||||
# test that an invalid url redirecrt to error page
|
||||
index.enter_invalid_url()
|
||||
index.click_shorten_button()
|
||||
self.assertEqual(index.get_current_url(), 'error')
|
||||
# Go back to home page
|
||||
index.click_try_again()
|
||||
|
||||
# check if heading is present
|
||||
heading = index.get_heading_text()
|
||||
self.assertEqual(heading,
|
||||
'ShortMe is a free tool to shorten URLs. Create a short & memorable URL in seconds.')
|
||||
|
||||
# test that a valid URL is working
|
||||
index.enter_valid_url()
|
||||
index.click_shorten_button()
|
||||
|
||||
"""Test the result page once the long URL has been shortened"""
|
||||
result = Result(self.chrome_browser)
|
||||
# test that the short URL exist inside the input element
|
||||
short_url = result.get_input_text()
|
||||
self.assertIsNotNone(short_url)
|
||||
|
||||
# test that the copy button works
|
||||
result.click_copy_button()
|
||||
clipboard_content = result.get_clipboard_content()
|
||||
self.assertEqual(clipboard_content, short_url)
|
||||
|
||||
# go to the total_clicks page
|
||||
result.go_to_total_clicks()
|
||||
|
||||
total_clicks = TotalClicks(self.chrome_browser)
|
||||
# test that the text matches the expected
|
||||
p_text = total_clicks.get_total_paragraph_text()
|
||||
self.assertEqual(p_text, 'Your URL has been clicked 0 times so far.')
|
||||
|
||||
finally:
|
||||
self.chrome_browser.quit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
12
app/tests/front_end_testing/total_clicks/total_clicks.py
Normal file
12
app/tests/front_end_testing/total_clicks/total_clicks.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from app.tests.utilities import selenium_utility
|
||||
|
||||
|
||||
class TotalClicks(selenium_utility.SeleniumUtility):
|
||||
_url_input_locator = '//p[@id="total-p"]'
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
super().__init__(driver)
|
||||
|
||||
def get_total_paragraph_text(self):
|
||||
return self.get_element(self._url_input_locator).text
|
||||
0
app/tests/utilities/__init__.py
Normal file
0
app/tests/utilities/__init__.py
Normal file
20
app/tests/utilities/logger.py
Normal file
20
app/tests/utilities/logger.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
|
||||
def Logger(log_level=logging.INFO):
|
||||
# Gets the name of the class / method from where this method is called
|
||||
logger_name = inspect.stack()[1][3]
|
||||
logger = logging.getLogger(logger_name)
|
||||
# By default, log all messages
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
file_handler = logging.FileHandler("{0}.log".format(logger_name), mode='w')
|
||||
file_handler.setLevel(log_level)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s',
|
||||
datefmt='%m/%d/%Y %I:%M:%S %p')
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
137
app/tests/utilities/selenium_utility.py
Normal file
137
app/tests/utilities/selenium_utility.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# ------- standard library imports -------
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from selenium.common.exceptions import *
|
||||
from selenium.webdriver.common.by import By
|
||||
from time import sleep, strftime, localtime
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import Select
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
# ------- local imports -------
|
||||
from app.tests.utilities import logger
|
||||
|
||||
|
||||
class SeleniumUtility:
|
||||
"""
|
||||
This utilitie is used to make it easier to use Selenium
|
||||
"""
|
||||
|
||||
log = logger.Logger(logging.DEBUG)
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
self.actions = ActionChains(driver)
|
||||
|
||||
@staticmethod
|
||||
def _get_by_type(locator_type):
|
||||
"""Returning the By type. xpath -> By.XPATH"""
|
||||
return getattr(By, locator_type.upper())
|
||||
|
||||
def send_key_command(self, element, key):
|
||||
"""Sending key commands to a pre-found element. Keys.RETURN"""
|
||||
try:
|
||||
element.send_keys(getattr(Keys, key.upper()))
|
||||
self.log.info(f'Key: {key} sent to element: {element}')
|
||||
|
||||
except AttributeError as e:
|
||||
print(e)
|
||||
self.log.info(f'Could not send keys to {element}')
|
||||
|
||||
def take_screenshot(self, sleep_time=0):
|
||||
sleep(sleep_time)
|
||||
Path("screenshots").mkdir(exist_ok=True)
|
||||
t = localtime()
|
||||
current_time = str(strftime("%H:%M:%S", t))
|
||||
file_name = ''.join([current_time, '.png'])
|
||||
screenshot_directory = "screenshots"
|
||||
destination_file = '/'.join([screenshot_directory, file_name])
|
||||
self.driver.save_screenshot(destination_file)
|
||||
self.log.info(f'screenshot saved to {destination_file}')
|
||||
|
||||
def get_element(self, locator, locator_type='xpath'):
|
||||
"""Return found element"""
|
||||
by_type = self._get_by_type(locator_type)
|
||||
|
||||
try:
|
||||
element = self.driver.find_element(by_type, locator)
|
||||
self.log.info(f'Element found. Locator: {locator}, Loctor type: {locator_type}')
|
||||
return element
|
||||
|
||||
except NoSuchElementException as e:
|
||||
print(e)
|
||||
self.log.info(f'Element not found. Locator: {locator}, Loctor type: {locator_type}')
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.log.info(f'Error while locating {locator}. {e}')
|
||||
|
||||
def get_elements(self, locator, locator_type='xpath'):
|
||||
"""Return matching elements"""
|
||||
by_type = self._get_by_type(locator_type)
|
||||
try:
|
||||
elements = self.driver.find_elements(by_type, locator)
|
||||
return elements
|
||||
|
||||
except NoSuchElementException as e:
|
||||
print(e)
|
||||
self.log.info(f'Element not found. Locator: {locator}, Loctor type: {locator_type}')
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.log.info(f'Error while locating {locator}. {e}')
|
||||
|
||||
def scroll_to_element(self, locator, locator_type='xpath'):
|
||||
"""Scroll to matching element"""
|
||||
element = self.get_element(locator, locator_type)
|
||||
if element:
|
||||
self.actions.move_to_element(element).perform()
|
||||
|
||||
def deselct_dropdown(self, locator, locator_type='xpath'):
|
||||
"""deselect all options from that SELECT on the page"""
|
||||
select = Select(self.get_element(locator, locator_type))
|
||||
select.deselect_all()
|
||||
|
||||
def dropdown_select(self,
|
||||
locator,
|
||||
locator_type,
|
||||
by_index=False,
|
||||
by_visible_text=False,
|
||||
by_value=False):
|
||||
|
||||
select = Select(self.get_element(locator, locator_type))
|
||||
if by_index:
|
||||
select.select_by_index(by_index)
|
||||
elif by_visible_text:
|
||||
select.select_by_visible_text(by_visible_text)
|
||||
elif by_value:
|
||||
select.select_by_value(by_value)
|
||||
|
||||
def wait_for_element(self, locator,
|
||||
locator_type='xpath',
|
||||
timeout=10,
|
||||
poll_frequency=0.5):
|
||||
"""Wait to presence of an element"""
|
||||
try:
|
||||
by_type = self._get_by_type(locator_type)
|
||||
|
||||
wait = WebDriverWait(self.driver,
|
||||
timeout,
|
||||
poll_frequency,
|
||||
ignored_exceptions=[NoSuchElementException,
|
||||
ElementNotVisibleException,
|
||||
ElementNotSelectableException])
|
||||
|
||||
element = wait.until(EC.element_to_be_clickable((by_type, locator)))
|
||||
self.log.info(f'Element found. Locator: {locator}, Loctor type: {locator_type}')
|
||||
return element
|
||||
|
||||
except TimeoutException:
|
||||
self.log.info('time out exception')
|
||||
|
||||
except InvalidArgumentException:
|
||||
self.log.info(f'Element not found. Locator: {locator}, Loctor type: {locator_type}')
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: ./
|
||||
ports:
|
||||
- "5000:5000"
|
||||
40
requirements.txt
Normal file
40
requirements.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
python-dotenv==0.15.0
|
||||
aniso8601==8.1.0
|
||||
astroid==2.4.2
|
||||
blinker==1.4
|
||||
certifi==2020.11.8
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
flake8==3.8.4
|
||||
Flask==1.1.2
|
||||
Flask-HTTPAuth==4.2.0
|
||||
Flask-Mail==0.9.1
|
||||
Flask-RESTful==0.3.8
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-Testing==0.8.1
|
||||
gunicorn==20.0.4
|
||||
idna==2.10
|
||||
importlib-metadata==3.7.3
|
||||
isort==5.6.4
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.2
|
||||
lazy-object-proxy==1.4.3
|
||||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
pycodestyle==2.6.0
|
||||
pyflakes==2.2.0
|
||||
PyJWT==1.4.2
|
||||
pylint==2.6.0
|
||||
pyperclip==1.8.1
|
||||
python-dotenv==0.15.0
|
||||
pytz==2020.4
|
||||
requests==2.25.0
|
||||
selenium==3.141.0
|
||||
six==1.15.0
|
||||
SQLAlchemy==1.3.20
|
||||
toml==0.10.2
|
||||
typed-ast==1.4.1
|
||||
urllib3==1.26.2
|
||||
Werkzeug==1.0.1
|
||||
wrapt==1.12.1
|
||||
zipp==3.4.0
|
||||
Reference in New Issue
Block a user