├── Procfile
├── app
├── static
│ ├── img
│ │ ├── logo.png
│ │ ├── checked-box.png
│ │ ├── checked-box_resized.png
│ │ └── checked-box_resized.xcf
│ ├── js
│ │ ├── custom.js
│ │ └── bootstrap.min.js
│ ├── css
│ │ ├── bootstrap-reboot.min.css
│ │ ├── bootstrap-reboot.css
│ │ ├── bootstrap-reboot.min.css.map
│ │ └── bootstrap-grid.min.css
│ └── swagger.json
├── api
│ ├── models.py
│ ├── auth.py
│ ├── __init__.py
│ └── endpoints
│ │ ├── register.py
│ │ ├── users.py
│ │ └── todos.py
├── templates
│ ├── todolist.html
│ ├── profile.html
│ ├── todo.html
│ ├── login.html
│ ├── index.html
│ ├── register.html
│ └── base.html
├── __init__.py
├── models.py
├── forms.py
└── routes.py
├── config.py
├── .gitignore
├── Pipfile
├── manage.py
├── requirements.txt
├── README.md
└── Pipfile.lock
/Procfile:
--------------------------------------------------------------------------------
1 | web: flask db upgrade; gunicorn app:'create_app()'
--------------------------------------------------------------------------------
/app/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keva161/Docket/HEAD/app/static/img/logo.png
--------------------------------------------------------------------------------
/app/static/img/checked-box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keva161/Docket/HEAD/app/static/img/checked-box.png
--------------------------------------------------------------------------------
/app/static/img/checked-box_resized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keva161/Docket/HEAD/app/static/img/checked-box_resized.png
--------------------------------------------------------------------------------
/app/static/img/checked-box_resized.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keva161/Docket/HEAD/app/static/img/checked-box_resized.xcf
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | basedir = os.path.dirname(__file__)
3 |
4 | class Config(object):
5 | SECRET_KEY = 'NOT_SO_SECRET'
6 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
7 | SQLALCHEMY_TRACK_MODIFICATIONS = False
--------------------------------------------------------------------------------
/app/api/models.py:
--------------------------------------------------------------------------------
1 | from app.api import api
2 | from flask_restplus import fields
3 |
4 | UserModel = api.model('User', {'Username': fields.String(), 'Email Address': fields.String(), 'Password': fields.String()})
5 |
6 | TodoModel = api.model('Todo', {'Body': fields.String()})
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/.gitignore
2 | /migrations/versions/3140b9cb65bf_.py
3 | /migrations/alembic.ini
4 | /app.db
5 | /migrations/env.py
6 | /migrations/versions/f34c020379e0_.py
7 | /.idea/misc.xml
8 | /.idea/modules.xml
9 | /.idea/inspectionProfiles/profiles_settings.xml
10 | /.idea/Py-todo.iml
11 | /migrations/README
12 | /migrations/script.py.mako
13 | /.idea/vcs.xml
14 | /migrations/
15 |
--------------------------------------------------------------------------------
/app/templates/todolist.html:
--------------------------------------------------------------------------------
1 |
2 | {% for todo in todos %}
3 |
4 | {{ todo.body }}
5 | ×
6 |
7 | {% endfor %}
8 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 |
8 | [packages]
9 | flask = "*"
10 | flask-sqlalchemy = "*"
11 | flask-wtf = "*"
12 | flask-migrate = "*"
13 | flask-login = "*"
14 | itsdangerous = "*"
15 | config = "*"
16 | sqlalchemy-serializer = "*"
17 | flask-swagger-ui = "*"
18 | flask-restplus = "*"
19 | flask-restful = "*"
20 | autodoc = "*"
21 | flask-api = "*"
22 | flask-autodoc = "*"
23 | flask-restful-swagger = "*"
24 | flask-limiter = "*"
25 | flask-script = "*"
26 | apscheduler = "*"
27 | werkzeug = "~=0.16.1"
28 | gunicorn = "*"
29 |
30 | [requires]
31 | python_version = "3.8"
32 |
--------------------------------------------------------------------------------
/app/api/auth.py:
--------------------------------------------------------------------------------
1 | from flask import request
2 | from functools import wraps
3 | from app.models import User
4 |
5 |
6 | def token_required(f):
7 | @wraps(f)
8 | def decorated(*args, **kwargs):
9 | token = None
10 |
11 | if 'Token' in request.headers:
12 | token = request.headers['Token']
13 | valid_user = User.query.filter_by(api_token=token).first()
14 | if not valid_user:
15 | return {'message': 'Token is invalid or missing'}
16 | else:
17 | return {'message': 'Token is invalid or missing'}
18 |
19 | return f(*args, **kwargs)
20 |
21 | return decorated
22 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | from apscheduler.schedulers.background import BackgroundScheduler
2 | from app import create_app, db
3 | from app.models import Todo, User
4 | from flask_script import Manager
5 |
6 | app = create_app()
7 |
8 | manager = Manager(app)
9 |
10 | def clear_data():
11 | with app.app_context():
12 | db.session.query(User).delete()
13 | db.session.query(Todo).delete()
14 | db.session.commit()
15 | print("Deleted table rows!")
16 |
17 | @manager.command
18 | def run():
19 | scheduler = BackgroundScheduler()
20 | scheduler.add_job(clear_data, trigger='interval', minutes=15)
21 | scheduler.start()
22 | with app.app_context():
23 | db.create_all()
24 | app.run(debug=True)
25 |
26 | if __name__ == '__main__':
27 | clear_data()
28 | manager.run()
--------------------------------------------------------------------------------
/app/api/__init__.py:
--------------------------------------------------------------------------------
1 | from logging import StreamHandler
2 | from flask_restplus import Api
3 | from flask import Blueprint
4 | from flask_limiter import Limiter
5 | from flask_limiter.util import get_remote_address
6 |
7 | blueprint = Blueprint('api', __name__, url_prefix='/api')
8 |
9 | limiter = Limiter(key_func=get_remote_address)
10 | limiter.logger.addHandler(StreamHandler())
11 |
12 | api = Api(blueprint, doc='/documentation', version='1.0', title='Docket API',
13 | description='API for Docket. Create users and todo items through a REST API.\n'
14 | 'First of all, begin by registering a new user via the registration form in the web interface.\n'
15 | 'Or via a `POST` request to the `/Register/` end point', decorators=[limiter.limit("50/day", error_message="API request limit has been reached (50 per day)")])
16 |
17 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask_migrate import Migrate
3 | from app.models import db, login
4 | from app.routes import site
5 |
6 | def create_app():
7 | app = Flask(__name__)
8 |
9 | from config import Config
10 | app.config.from_object(Config)
11 | db.init_app(app)
12 | migrate = Migrate(app, db)
13 | login.init_app(app)
14 | login.login_view = 'login'
15 |
16 | from app.api import api, blueprint, limiter
17 | from app.api.endpoints import users, todos, register
18 | from app.api.endpoints.todos import TodosNS
19 | from app.api.endpoints.users import UserNS
20 | from app.api.endpoints.register import RegisterNS
21 |
22 | app.register_blueprint(site)
23 | app.register_blueprint(blueprint)
24 |
25 | limiter.init_app(app)
26 |
27 | api.add_namespace(TodosNS)
28 | api.add_namespace(UserNS)
29 | api.add_namespace(RegisterNS)
30 |
31 | return app
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==1.4.0
2 | aniso8601==8.0.0
3 | APScheduler==3.6.3
4 | attrs==19.3.0
5 | autodoc==0.5.0
6 | beautifulsoup4==4.8.2
7 | Click==7.0
8 | config==0.4.2
9 | decorator==4.4.1
10 | Flask==1.1.1
11 | Flask-API==2.0
12 | Flask-Autodoc==0.1.2
13 | Flask-Limiter==1.1.0
14 | Flask-Login==0.5.0
15 | Flask-Migrate==2.5.2
16 | Flask-RESTful==0.3.8
17 | flask-restful-swagger==0.20.1
18 | flask-restplus==0.13.0
19 | Flask-Script==2.0.6
20 | Flask-SQLAlchemy==2.4.1
21 | flask-swagger-ui==3.20.9
22 | Flask-WTF==0.14.3
23 | gunicorn==20.0.4
24 | itsdangerous==1.1.0
25 | Jinja2==2.11.1
26 | jsonschema==3.2.0
27 | limits==1.5
28 | Mako==1.1.1
29 | MarkupSafe==1.1.1
30 | pyrsistent==0.15.7
31 | python-dateutil==2.8.1
32 | python-editor==1.0.4
33 | pytz==2019.3
34 | six==1.14.0
35 | soupsieve==1.9.5
36 | SQLAlchemy==1.3.13
37 | SQLAlchemy-serializer==1.3.4.2
38 | tzlocal==2.0.0
39 | waitress==1.4.3
40 | WebOb==1.8.6
41 | WebTest==2.0.34
42 | Werkzeug==0.16.1
43 | WTForms==2.2.1
44 |
--------------------------------------------------------------------------------
/app/static/js/custom.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function(){
2 |
3 | $(document).on('click', '#delete', function(){
4 |
5 | let deleteItem = $(this).attr('class');
6 |
7 | console.log(deleteItem);
8 |
9 | req = $.ajax({
10 | url: '/deletetodo',
11 | type: 'POST',
12 | data: { id : deleteItem }
13 | });
14 |
15 | req.done(function(data){
16 |
17 | $('#todoList').html(data);
18 | })
19 | })
20 |
21 | $('.updateButton').on('click',function () {
22 |
23 | let todo = $('#todoInput').val();
24 |
25 | if (todo != '')
26 |
27 | req = $.ajax({
28 | url: '/newtodo',
29 | type: 'POST',
30 | data: { todo : todo }
31 | });
32 |
33 | req.done(function(data){
34 |
35 | $('#todoList').html(data);
36 | $('#todoInput').val('');
37 | })
38 | event.preventDefault();
39 | });
40 | });
--------------------------------------------------------------------------------
/app/templates/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | Docket - Profile Page
4 | {% endblock %}
5 | {% block navbar %}
6 | {% endblock %}
7 | {% block content %}
8 | {% with messages = get_flashed_messages() %}
9 | {% if messages %}
10 | {% for message in messages %}
11 |
12 | {{ message }}
13 |
14 | {% endfor %}
15 | {% endif %}
16 | {% endwith %}
17 |
18 |
19 |
20 |
21 |
22 |
23 | Username
24 | {{ User.username }}
25 |
26 |
27 | Email
28 | {{ User.email }}
29 |
30 |
31 | API Token
32 | {{ User.api_token }}
33 |
34 |
35 |
36 |
Your API token can be used by placing ?token=...in your header.
37 |
Full API documentation can be read here .
38 |
39 |
40 |
41 | {% endblock %}
--------------------------------------------------------------------------------
/app/models.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from flask_login import LoginManager
3 | from datetime import datetime
4 | from flask_login import UserMixin
5 | from werkzeug.security import generate_password_hash, check_password_hash
6 |
7 | db = SQLAlchemy()
8 | login = LoginManager()
9 |
10 | class User(UserMixin, db.Model):
11 | id = db.Column(db.Integer, primary_key=True, unique=True)
12 | api_token = db.Column(db.String(50), unique=True)
13 | username = db.Column(db.String(128), index=True, unique=True)
14 | email = db.Column(db.String(128), index=True, unique=True)
15 | password_hash = db.Column(db.String(128))
16 | todos = db.relationship('Todo', backref='owner', lazy='dynamic')
17 |
18 | def __repr__(self):
19 | return ''.format(self.username)
20 |
21 | def set_password(self, password):
22 | self.password_hash = generate_password_hash(password)
23 |
24 | def check_password(self, password):
25 | return check_password_hash(self.password_hash, password)
26 |
27 |
28 | @login.user_loader
29 | def load_user(id):
30 | return User.query.get(int(id))
31 |
32 |
33 | class Todo(db.Model):
34 | id = db.Column(db.Integer, primary_key=True)
35 | body = db.Column(db.String(140))
36 | timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
37 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
38 |
39 | def __repr__(self):
40 | return ''.format(self.body)
--------------------------------------------------------------------------------
/app/api/endpoints/register.py:
--------------------------------------------------------------------------------
1 | from flask_restplus import Resource, Namespace
2 | from app.models import db, User
3 | from app.api.models import UserModel
4 | from flask import jsonify, request, make_response
5 | import uuid
6 | #from app import limiter
7 |
8 | RegisterNS = Namespace('Register', description='Register for the application')
9 |
10 |
11 | @RegisterNS.route('/')
12 | class Register(Resource):
13 | #decorators = [limiter.limit('50 per day')]
14 |
15 | @RegisterNS.expect(UserModel)
16 | def post(self):
17 | """Creates a new user and returns an API token."""
18 | data = request.get_json()
19 |
20 | if not data:
21 | return jsonify({'message': 'Please enter some user data'})
22 | else:
23 | user = User(api_token=str(uuid.uuid4()), username=data['Username'], email=data['Email Address'])
24 | if data["Password"] != type(str):
25 | jsonify({'message' : 'Password needs to be a string'})
26 | else:
27 | user.set_password(data['Password'])
28 | try:
29 | db.session.add(user)
30 | print("Registering a new user")
31 | db.session.commit()
32 | return jsonify({'message': "New user created! Token: " + user.api_token})
33 | except Exception as e:
34 | print(e)
35 | db.session.rollback()
36 | return {'Message': 'Username or email address is invalid!'}
37 |
--------------------------------------------------------------------------------
/app/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField
3 | from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
4 | from app.models import User
5 |
6 |
7 | class LoginForm(FlaskForm):
8 | username = StringField('Username', validators=[DataRequired()], render_kw={"placeholder": "Username"})
9 | password = PasswordField('Password', validators=[DataRequired()], render_kw={"placeholder": "Password"})
10 | remember_me = BooleanField('Remember Me')
11 | submit = SubmitField('Sign in')
12 |
13 |
14 | class RegistrationForm(FlaskForm):
15 | username = StringField('Username', validators=[DataRequired()], render_kw={"placeholder": "Username"})
16 | email = StringField('Email', validators=[DataRequired(), Email()], render_kw={"placeholder": "Email"})
17 | password = PasswordField('Password', validators=[DataRequired()], render_kw={"placeholder": "Password"})
18 | password2 = PasswordField(
19 | 'Repeat Password', validators=[DataRequired(), EqualTo('password')], render_kw={"placeholder": "Repeat Password"})
20 | submit = SubmitField('Register')
21 |
22 | def validate_username(self, username):
23 | user = User.query.filter_by(username=username.data).first()
24 | if user is not None:
25 | raise ValidationError('Please enter a different username.')
26 |
27 | def validate_email(self, email):
28 | user = User.query.filter_by(email=email.data).first()
29 | if user is not None:
30 | raise ValidationError('Please use a different email address.')
31 |
32 | class TodoForm(FlaskForm):
33 | todo = StringField('New Todo', validators=[DataRequired()])
34 | submit = SubmitField('Add')
35 |
--------------------------------------------------------------------------------
/app/templates/todo.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | Docket - Welcome
4 | {% endblock %}
5 | {% block script %}
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 | {% with messages = get_flashed_messages() %}
11 | {% if messages %}
12 | {% for message in messages %}
13 |
14 | {{ message }}
15 |
16 | {% endfor %}
17 | {% endif %}
18 | {% endwith %}
19 |
20 |
21 | Hello {{ current_user.username }}
22 |
23 |
What do you need to do today?
24 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
38 | {% for todo in todos %}
39 |
40 | {{ todo.body }}
41 | ×
42 |
43 | {% endfor %}
44 |
45 |
46 |
47 |
48 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | Docket - Login
4 | {% endblock %}
5 | {% block content %}
6 |
45 |
13 |
14 | Docket
15 |
16 |
17 |
18 | Home
19 |
20 | {% if current_user.is_anonymous %}
21 |
22 | Login
23 |
24 |
25 | Register
26 |
27 | {% else %}
28 |
29 | Profile
30 |
31 |
32 | Todo
33 |
34 |
35 | Logout
36 |
37 | {% endif %}
38 |
39 |
40 |
41 |
42 |
43 | {% block content %} {% endblock %}
44 |
45 |
46 |
49 |
46 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 |
Docket - Simple Todo application for you to practise your API testing
4 | {% endblock %}
5 | {% block navbar %}
6 | {% endblock %}
7 | {% block content %}
8 | {% with messages = get_flashed_messages() %}
9 | {% if messages %}
10 | {% for message in messages %}
11 |
12 | {{ message }}
13 |
14 | {% endfor %}
15 | {% endif %}
16 | {% endwith %}
17 |
18 |
19 |
20 |
21 |
Docket is fully fledged Todo web application for you to practise and learn about API testing.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | C reate new users and todo items
30 |
31 |
32 | R ead your todo's via the web application and API protocol
33 |
34 |
35 | U pdate your user details
36 |
37 |
38 | D elete todo's and account from the database
39 |
40 |
41 |
42 |
43 |
44 |
Example Python API tests can be downloaded here
45 |
46 |
The API is limited to 50 API calls per day and the application resets itself every 15 minutes. Please contact me if you need an extension. There are intended bugs, but please reach out if you are having issues!
47 |
48 |
49 |
50 | {% endblock %}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Docket
2 |
3 | A simple TODO application written in Python that allows users to practise their test automation abilities.
4 |
5 | ## Download the code
6 |
7 | If you wish to run the code on your own machine. Simply follow the following steps.
8 |
9 | Note: Your specific steps may differ depending on your systems operating system.
10 |
11 | 1. Make sure you have [Python](http://python.org) installed and correctly setup for your operating system (you will need 3.7 or higher).
12 | 2. You will also need to install `pipenv`. This can be installed by typing `pip install pipenv` from the command line.
13 | 3. Clone this repo, or down it [here](https://github.com/keva161/Docket/archive/master.zip).
14 | 4. After unzipping the downloaded archive. Navigate to where you downloaded it and open up a terminal in the location.
15 | 5. Type: `pipenv install` to install the required dependencies.
16 | 6. Create the database required by typing `flask db init`, `flask db migrate` & `flask db upgrade`.
17 | 7. Type `python manage.py run` to start the server.
18 | 8. Navigate to `127.0.0.1:5000` to find the application running.
19 |
20 | ## Example API Tests
21 |
22 | I have written some example API tests for you to learn from. These have been written in Python and use the `pytest` framework.
23 | These can be found [here](https://github.com/keva161/DocketTests).
24 |
25 | ## Problems
26 |
27 | If you have problems with the code, or any the running of the tests. Please reach out to me on [Twitter](http://twitter.com/keva161).
28 |
29 | Have a custom solution that you would like to created automated tests for? reach out to me and lets discuss! [kevin@kevintuck.co.uk](mailto:kevin@kevintuck.co.uk)
30 |
31 | ## About the author
32 |
33 | I'm a freelance software tester based in the southwest of the UK. Please feel free to check out my blog on which I write articles about software testing. Or connect with me via email or Twitter.
34 |
35 | Website: [http://kevintuck.co.uk](http://kevintuck.co.uk)
36 |
37 | Email: [kevin@kevintuck.co.uk](mailto:kevin@kevintuck.co.uk)
38 |
39 | Twitter: [http://twitter.com/keva161](http://twitter.com/keva161)
40 |
41 |
--------------------------------------------------------------------------------
/app/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 |
Docket - Registration Page
4 | {% endblock %}
5 | {% block content %}
6 |
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/app/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}
8 | {% endblock %}
9 | {% block script %}
10 | {% endblock %}
11 |
12 |
50 |