Initial commit

This commit is contained in:
tomeros
2021-03-24 15:13:32 +02:00
commit 7b4b578010
60 changed files with 1847 additions and 0 deletions

11
Dockerfile Normal file
View 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
View 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.

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: gunicorn --bind 0.0.0.0:$PORT run:app

BIN
README.md Normal file

Binary file not shown.

0
app/__init__.py Normal file
View File

5
app/app.py Normal file
View File

@@ -0,0 +1,5 @@
from app.setup.setup import create_app
CONFIG_FILE = 'settings.py'
app = create_app(config_file=CONFIG_FILE)

View 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;
}

View 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);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View 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 %}

View 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>&lt;Authorization: Bearer {token}&gt;</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 %}

View 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>

View 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>&nbsp;<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>&nbsp;The
URL may have
been blocked<br></li>
<li class="bullet"><i class="fa fa-check" style="color: rgb(0,128,255);"></i>&nbsp;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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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:&nbsp;<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
View File

View File

112
app/server/api/api.py Normal file
View 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)

View 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()

View File

View File

@@ -0,0 +1,4 @@
# ------- 3rd party imports -------
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

87
app/server/db/models.py Normal file
View 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}'

View 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')

View 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')

View 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')

View 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')

View File

View 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')

View 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'))

View 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'))

View 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'))

View 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')

View 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)

View 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))

View 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'))

View 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
View 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
View 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

View File

View 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

View 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
"""

View 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()

View File

View 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()

View 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

View 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()

View 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

View File

View 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

View 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
View File

@@ -0,0 +1,7 @@
version: '3'
services:
app:
build: ./
ports:
- "5000:5000"

40
requirements.txt Normal file
View 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

5
run.py Normal file
View File

@@ -0,0 +1,5 @@
# ------- local imports -------
from app.app import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5555)