├── tests ├── __init__.py ├── view │ ├── __init__.py │ ├── items │ │ ├── __init__.py │ │ └── test_items.py │ ├── crudapi │ │ ├── __init__.py │ │ └── test_crud_api.py │ ├── table_shapes │ │ └── test_table_shapes.py │ ├── test_reservations.py │ ├── test_generic_views.py │ ├── floors │ │ └── test_floors.py │ └── tables │ │ └── test_tables.py ├── integration │ ├── sync │ │ ├── __init__.py │ │ ├── it_sync_location_test.py │ │ └── it_sync_tables_test.py │ ├── __init__.py │ ├── resources │ │ └── messages │ │ │ ├── message_en_US.properties │ │ │ └── message_pt_BR.properties │ ├── fixtures │ │ └── test_image.jpg │ ├── it_app_test.py │ ├── it_test_auth.py │ ├── it_other_privilages_test.py │ ├── it_administrator_privilages_test.py │ ├── it_companies_test.py │ ├── it_authorization_test.py │ ├── it_poster_test.py │ ├── it_comments_test.py │ ├── it_property_resource_test.py │ ├── it_reservations_test.py │ ├── it_roles_test.py │ ├── poster │ │ ├── poster_server_mock_test.py │ │ └── poster_server_mock.py │ ├── it_scheme_types_test.py │ ├── it_locations_test.py │ ├── it_tables_test.py │ ├── it_floors_test.py │ └── it_reservation_settings_test.py ├── test_poster.py ├── test_sms.py ├── test_auth.py └── conftest.py ├── timeless ├── items │ ├── __init__.py │ ├── forms.py │ ├── views.py │ └── models.py ├── message │ ├── __init__.py │ ├── message_resource.py │ └── property_resource.py ├── poster │ ├── __init__.py │ ├── exceptions.py │ ├── models.py │ ├── tasks.py │ └── api.py ├── roles │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── customers │ ├── __init__.py │ └── models.py ├── employees │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── reservations │ ├── __init__.py │ ├── forms.py │ └── models.py ├── templates │ ├── items │ │ ├── create.html │ │ ├── edit.html │ │ └── list.html │ ├── _formhelpers.html │ ├── auth │ │ ├── forgot_password_post.html │ │ ├── activate.html │ │ ├── forgot_password.html │ │ └── login.html │ ├── schemetypes │ │ ├── schemeconditions │ │ │ ├── create_edit.html │ │ │ └── list.html │ │ └── list.html │ ├── roles │ │ ├── create_edit.html │ │ └── list.html │ ├── reservations │ │ ├── settings │ │ │ ├── create_edit.html │ │ │ └── list.html │ │ ├── create_edit.html │ │ └── list.html │ ├── restaurants │ │ ├── table_shapes │ │ │ ├── create_edit.html │ │ │ └── list.html │ │ ├── floors │ │ │ ├── create_edit.html │ │ │ └── list.html │ │ ├── tables │ │ │ ├── create_edit.html │ │ │ └── list.html │ │ └── locations │ │ │ ├── list.html │ │ │ └── create_edit.html │ ├── views.py │ ├── base.html │ └── employees │ │ ├── create_edit.html │ │ └── list.html ├── restaurants │ ├── floors │ │ ├── __init__.py │ │ ├── forms.py │ │ └── views.py │ ├── tables │ │ ├── __init__.py │ │ ├── forms.py │ │ └── views.py │ ├── table_shapes │ │ ├── __init__.py │ │ ├── forms.py │ │ └── views.py │ ├── __init__.py │ └── locations │ │ ├── forms.py │ │ └── views.py ├── companies │ ├── __init__.py │ ├── views.py │ └── models.py ├── static │ └── style.css ├── schemetypes │ ├── __init__.py │ └── forms.py ├── db │ └── __init__.py ├── cache │ └── __init__.py ├── csrf │ └── __init__.py ├── access_control │ ├── methods.py │ ├── __init__.py │ ├── unknown_privileges.py │ ├── administrator_privileges.py │ ├── other_privileges.py │ ├── views.py │ ├── location_admin_privileges.py │ ├── director_privileges.py │ ├── manager_privileges.py │ ├── owner_privileges.py │ └── authorization.py ├── mail │ └── __init__.py ├── celery.py ├── models.py ├── uploads │ └── __init__.py ├── forms.py ├── sync │ └── synced_table.py ├── auth │ ├── auth.py │ └── views.py └── sms.py ├── migrations ├── README ├── versions │ ├── 2019-01-28T204834.py │ ├── 2019-01-28T205517.py │ ├── 2019-02-01T150645.py │ ├── 2019-01-27T152248.py │ ├── 2019-01-28T204833.py │ ├── 2019-01-22T094339.py │ ├── 2019-01-25T202746.py │ ├── 2019-01-25T120716.py │ ├── 2019-02-26T054508.py │ ├── 2019-01-28T205516.py │ ├── 2019-01-28T150634.py │ ├── 2019-01-29T144639.py │ ├── 2019-01-25T202745.py │ ├── 2019-01-25T085210.py │ ├── 2019-01-29T092044.py │ └── 2019-02-10T185948.py ├── script.py.mako ├── alembic.ini └── env.py ├── .pylintrc ├── entrypoint.sh ├── frontend ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── index.js │ ├── App.test.js │ ├── index.css │ ├── App.css │ ├── App.js │ └── logo.svg ├── .gitignore └── package.json ├── setup.cfg ├── Dockerfile ├── .gitignore ├── .codeclimate.yml ├── main.py ├── scripts ├── install │ ├── installRedis.sh │ ├── deploy │ │ ├── redis.service │ │ ├── install_python.sh │ │ ├── install_dependencies.sh │ │ ├── timeless_pg.service │ │ ├── install_redis.sh │ │ └── install_db.sh │ └── install_db.sh └── backup │ ├── cron_add.sh │ ├── pg_backup.config │ └── pg_restore.sh ├── .pdd ├── manage.py ├── .travis.yml ├── check_pylint.py ├── credentials ├── staging.id_rsa.pub.asc ├── credentials.json.asc └── staging.id_rsa.asc ├── checkstyle.py ├── .rultor.yml ├── docker-compose.yaml ├── requirements.txt ├── config.py └── deploy.sh /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/view/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/view/items/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/items/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/message/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/poster/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/roles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/view/crudapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/customers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/employees/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/reservations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/sync/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/templates/items/create.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/templates/items/edit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/restaurants/floors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/restaurants/tables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timeless/restaurants/table_shapes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /timeless/companies/__init__.py: -------------------------------------------------------------------------------- 1 | import timeless.items.models 2 | -------------------------------------------------------------------------------- /timeless/static/style.css: -------------------------------------------------------------------------------- 1 | h1 { color: #377ba8; margin: 1rem 0; } -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """ Package for integration tests """ 2 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [pre-commit-hook] 2 | command=pylint 3 | params=timeless 4 | limit=8.0 5 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sleep 5 # waiting while postgres is being started 3 | exec "$@" 4 | -------------------------------------------------------------------------------- /timeless/schemetypes/__init__.py: -------------------------------------------------------------------------------- 1 | """Python package containing DB models for SchemeTypes""" 2 | -------------------------------------------------------------------------------- /timeless/db/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | 4 | DB = SQLAlchemy() 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horoscopes/timelessis/master/frontend/public/favicon.ico -------------------------------------------------------------------------------- /tests/integration/resources/messages/message_en_US.properties: -------------------------------------------------------------------------------- 1 | #commentedkey=commentedvalue 2 | foundkey=thevalue -------------------------------------------------------------------------------- /tests/integration/resources/messages/message_pt_BR.properties: -------------------------------------------------------------------------------- 1 | #commentedkey=valorcomentado 2 | foundkey=ovalor -------------------------------------------------------------------------------- /timeless/cache/__init__.py: -------------------------------------------------------------------------------- 1 | """ CACHE module """ 2 | from flask_caching import Cache 3 | 4 | CACHE = Cache() 5 | -------------------------------------------------------------------------------- /timeless/csrf/__init__.py: -------------------------------------------------------------------------------- 1 | """ CSRF module """ 2 | from flask_wtf.csrf import CSRFProtect 3 | 4 | CSRF = CSRFProtect() 5 | -------------------------------------------------------------------------------- /tests/integration/fixtures/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horoscopes/timelessis/master/tests/integration/fixtures/test_image.jpg -------------------------------------------------------------------------------- /tests/integration/it_app_test.py: -------------------------------------------------------------------------------- 1 | def test_hello(client): 2 | response = client.get("/") 3 | assert response.data == b'Hello, World!' 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | mocked-sessions=timeless.DB.session 4 | 5 | [coverage:run] 6 | branch = True 7 | source = timeless 8 | -------------------------------------------------------------------------------- /timeless/restaurants/__init__.py: -------------------------------------------------------------------------------- 1 | import timeless.companies.models 2 | import timeless.employees.models 3 | import timeless.roles.models 4 | import timeless.items.models 5 | -------------------------------------------------------------------------------- /timeless/poster/exceptions.py: -------------------------------------------------------------------------------- 1 | """ Poster related exceptions """ 2 | 3 | 4 | class PosterAPIError(Exception): 5 | """Exception class for poster module """ 6 | pass 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | COPY . /usr/app 4 | WORKDIR /usr/app 5 | 6 | RUN pip install -r requirements.txt 7 | 8 | EXPOSE 5000 9 | ENTRYPOINT ["./entrypoint.sh"] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | __pycache__ 4 | 5 | #Flask 6 | instance/ 7 | .webassets-cache 8 | 9 | #Environment 10 | venv/ 11 | 12 | .idea/ 13 | .DS_Store 14 | 15 | .vscode/ -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | radon: 3 | enabled: true 4 | config: 5 | threshold: "B" 6 | pep8: 7 | enabled: true 8 | ratings: 9 | paths: 10 | -"timeless/**.py" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ Application Main module """ 2 | import os 3 | from timeless import create_app 4 | 5 | 6 | app = create_app( 7 | os.environ.get("TIMELESSIS_CONFIG", "config.DevelopmentConfig") 8 | ) 9 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /scripts/install/installRedis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo apt -y install make gcc libc6-dev tcl 3 | wget http://download.redis.io/redis-stable.tar.gz 4 | tar xvzf redis-stable.tar.gz 5 | cd redis-stable 6 | sudo make install 7 | src/redis-server > /dev/null & 8 | -------------------------------------------------------------------------------- /timeless/access_control/methods.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Method(Enum): 5 | """Method that a user can execute on a particular resource 6 | """ 7 | CREATE = "post" 8 | READ = "get" 9 | UPDATE = "put" 10 | DELETE = "delete" 11 | -------------------------------------------------------------------------------- /timeless/message/message_resource.py: -------------------------------------------------------------------------------- 1 | """ Message resource for internationalization """ 2 | 3 | 4 | class MessageResource: 5 | """ Interface that defines behavior """ 6 | 7 | def get(self, key): 8 | raise Exception("Message Resource does not implement methods!") 9 | -------------------------------------------------------------------------------- /timeless/items/forms.py: -------------------------------------------------------------------------------- 1 | from timeless import forms 2 | from timeless.items import models 3 | 4 | 5 | class ItemForm(forms.ModelForm): 6 | """ Base form for creating / updating Items """ 7 | 8 | class Meta: 9 | """ Meta for Item form """ 10 | model = models.Item 11 | -------------------------------------------------------------------------------- /timeless/employees/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for employees blueprint""" 2 | 3 | from timeless import forms 4 | from timeless.employees.models import Employee 5 | 6 | 7 | class EmployeeForm(forms.ModelForm): 8 | """Base form for employee""" 9 | 10 | class Meta: 11 | model = Employee 12 | -------------------------------------------------------------------------------- /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --verbose 3 | --exclude .idea/**/* 4 | --exclude venv/**/* 5 | --exclude redis-stable/**/* 6 | --exclude credentials/**/* 7 | --exclude migrations/**/* 8 | --exclude instance/**/* 9 | --exclude pylintrc 10 | --rule min-words:20 11 | --rule min-estimate:15 12 | --rule max-estimate:90 13 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /timeless/poster/models.py: -------------------------------------------------------------------------------- 1 | """File for models in poster module""" 2 | from timeless import DB 3 | 4 | 5 | class PosterSyncMixin: 6 | """Mixin with fields needed for data synchronization with Poster. 7 | """ 8 | poster_id = DB.Column(DB.Integer) 9 | synchronized_on = DB.Column(DB.DateTime) 10 | -------------------------------------------------------------------------------- /scripts/install/deploy/redis.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Redis server 3 | Documentation=man:redis-server(1) 4 | 5 | [Service] 6 | Type=forking 7 | ExecStart=redis-server 8 | SuccessExitStatus=143 9 | TimeoutStopSec=10 10 | Restart=on-failure 11 | RestartSec=5 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /timeless/restaurants/tables/forms.py: -------------------------------------------------------------------------------- 1 | from timeless import forms 2 | from timeless.restaurants import models 3 | 4 | 5 | class TableForm(forms.ModelForm): 6 | """ Base form for creating / updating Table """ 7 | 8 | class Meta: 9 | """ Meta for Table form """ 10 | model = models.Table 11 | -------------------------------------------------------------------------------- /timeless/roles/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for table shapes blueprint in order to support CRUD operations""" 2 | from timeless import forms 3 | from timeless.roles.models import Role 4 | 5 | 6 | class RoleForm(forms.ModelForm): 7 | """Base form for role""" 8 | 9 | class Meta: 10 | model = Role 11 | -------------------------------------------------------------------------------- /scripts/install/deploy/install_python.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script for installing Python 4 | 5 | sudo add-apt-repository -y ppa:jonathonf/python-3.6 6 | sudo apt-get update 7 | sudo apt-get -y install python3.6 python3.6-dev python3.6-venv python3-pip 8 | echo "Done installing Python 3.6" 9 | alias python=python3.6 10 | -------------------------------------------------------------------------------- /timeless/restaurants/locations/forms.py: -------------------------------------------------------------------------------- 1 | from timeless import forms 2 | from timeless.restaurants import models 3 | 4 | 5 | class LocationForm(forms.ModelForm): 6 | """ Base form for creating / updating Location """ 7 | 8 | class Meta: 9 | """ Meta for Location form """ 10 | model = models.Location 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager 2 | from flask_migrate import Migrate, MigrateCommand 3 | 4 | import main 5 | from timeless.db import DB 6 | 7 | 8 | MIGRATE = Migrate(main.app, DB) 9 | MANAGER = Manager(main.app) 10 | 11 | MANAGER.add_command("db", MigrateCommand) 12 | 13 | 14 | if __name__ == "__main__": 15 | MANAGER.run() 16 | -------------------------------------------------------------------------------- /timeless/templates/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
3 | {{ field.label }} 4 | {{ field(**kwargs)|safe }} 5 |
6 | {% if field.errors %} 7 | 12 | {% endif %} 13 | {% endmacro %} -------------------------------------------------------------------------------- /timeless/schemetypes/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for schema type blueprint in order to support CRUD operations""" 2 | from timeless import forms 3 | from timeless.schemetypes.models import SchemeCondition 4 | 5 | 6 | class SchemeConditionForm(forms.ModelForm): 7 | """Base form for scheme types""" 8 | 9 | class Meta: 10 | model = SchemeCondition 11 | -------------------------------------------------------------------------------- /scripts/backup/cron_add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Create a backup script and set it into cron. 4 | # 5 | 6 | if [ "$1" == "" ]; then 7 | echo "Please provide a cron expression as script parameter in the format \"* * * * *\"" 8 | else 9 | echo $1 " ./pg_backup.sh" > backup.sh 10 | chmod 755 backup.sh 11 | sudo mv backup.sh /etc/cron.d/ 12 | fi 13 | -------------------------------------------------------------------------------- /timeless/mail/__init__.py: -------------------------------------------------------------------------------- 1 | """ MAIL module """ 2 | from flask_mail import Mail 3 | 4 | 5 | """ 6 | @todo #388:30min mail sending should be disabled 7 | for development and testing as required by @vladarefiev. 8 | however should be enabled for staging and deployment. 9 | few extra words for PDD not to complain. 10 | """ 11 | 12 | MAIL = Mail() 13 | -------------------------------------------------------------------------------- /tests/view/table_shapes/test_table_shapes.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | """ Tests for the TableShape views.""" 4 | 5 | 6 | def test_list(client): 7 | """ Test list is okay """ 8 | response = client.get("/table_shapes/") 9 | assert response.status_code == HTTPStatus.OK 10 | assert b'
' in response.data 11 | 12 | -------------------------------------------------------------------------------- /timeless/templates/auth/forgot_password_post.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Forgot password{% endblock %}

5 | {% endblock %} 6 | {% block content %} 7 |

Forgot your password?

8 |

We've sent an e-mail to {{email}} with your new password.

9 |

Please use it to log in and change it.

10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /timeless/templates/schemetypes/schemeconditions/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Scheme Conditions{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {{ form }} 10 |

11 | 12 |

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/install/deploy/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | # Scripts to install all the dependencies, like Python, DB, Redis, etc 5 | srcdir="./scripts/install/deploy" 6 | 7 | chmod +x $srcdir/install_python.sh 8 | $srcdir/install_python.sh 9 | 10 | chmod +x $srcdir/install_db.sh 11 | $srcdir/install_db.sh 12 | 13 | chmod +x $srcdir/install_redis.sh 14 | $srcdir/install_redis.sh 15 | -------------------------------------------------------------------------------- /scripts/install/deploy/timeless_pg.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PostgreSQL DB Timeless 3 | Documentation=man:postgres(1) 4 | 5 | [Service] 6 | Type=forking 7 | User=postgres 8 | ExecStart=/usr/lib/postgresql/10/bin/postgres -D /etc/postgresql/10/main 9 | SuccessExitStatus=143 10 | TimeoutStopSec=10 11 | Restart=on-failure 12 | RestartSec=5 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /timeless/restaurants/floors/forms.py: -------------------------------------------------------------------------------- 1 | from timeless import forms 2 | from timeless.restaurants import models 3 | from wtforms import Form, TextField, HiddenField 4 | 5 | 6 | class FloorForm(forms.ModelForm): 7 | """ Base form for creating / updating Floor """ 8 | location_id = HiddenField("Location id") 9 | 10 | class Meta: 11 | """ Meta for Floor form """ 12 | model = models.Floor 13 | -------------------------------------------------------------------------------- /tests/view/test_reservations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for Reservations view. 3 | @todo #235:30min Continue implementation of views. Index and a 4 | view page should be created to list all reservations. In the 5 | index page there should be also a function to delete the reservation 6 | (after confirmation). In the index page it should be possible 7 | to sort and filter for every column. 8 | 9 | """ 10 | -------------------------------------------------------------------------------- /timeless/templates/roles/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Roles management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | {{ form }} 10 |

11 | 12 |

13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /timeless/access_control/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @todo #278:30min Continue securing other views, by using SecuredView. 3 | Most views are consisted of procedures, not classes, so maybe they'll 4 | have to be turned into classes. Then update the tests to test that 5 | unauthorized aren't allowed to access the view. View already implemented: 6 | employees/views.py::Create, employees/views.py::List and 7 | employees/views.py::List. 8 | """ 9 | -------------------------------------------------------------------------------- /timeless/access_control/unknown_privileges.py: -------------------------------------------------------------------------------- 1 | """ Permissions for Unknown roles 2 | @todo #182:30min Create tests for `unknown_privilege.has_privilege`. Users 3 | which does not have eny privilege should fall into this category and doesn't 4 | have access to any part of the system. 5 | """ 6 | 7 | 8 | def has_privilege(resource=None, *args, **kwargs) -> bool: 9 | """Unknown role does not have any privileges.""" 10 | return False 11 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /timeless/templates/auth/activate.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block header %} 5 |

{% block title %}Activate{% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if session.logged_in %} 10 |

{{ message }}

11 | {% else %} 12 | {% block not_logged %} 13 |

You are not logged in

14 |

You have to Log In fist.

15 | {% endblock %} 16 | {% endif %} 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tests/integration/it_test_auth.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | 4 | def test_login(client, auth): 5 | assert client.get("/auth/login").status_code == HTTPStatus.OK 6 | response = auth.login() 7 | assert response.status_code == HTTPStatus.OK 8 | 9 | 10 | def test_forgot_password(client): 11 | assert client.get("/auth/forgotpassword").status_code == HTTPStatus.OK 12 | 13 | 14 | def test_activate(client): 15 | assert client.get("/auth/activate").status_code == HTTPStatus.METHOD_NOT_ALLOWED 16 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-28T204834.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6069798906d9 4 | Revises: 79f280c6c1f4, 4d7589fae495 5 | Create Date: 2019-01-27 16:04:29.390792 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6069798906d9' 14 | down_revision = ('79f280c6c1f4', '4d7589fae495') 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-28T205517.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: bbd99136a04d 4 | Revises: 9eee25222512, 8e4ed4542586 5 | Create Date: 2019-01-28 21:54:11.052298 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'bbd99136a04d' 14 | down_revision = ('9eee25222512', '8e4ed4542586') 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /migrations/versions/2019-02-01T150645.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ce1494a8b1f3 4 | Revises: 4207a669d969, b15df456e2cd 5 | Create Date: 2019-02-01 15:53:34.832140 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ce1494a8b1f3' 14 | down_revision = ('4207a669d969', 'b15df456e2cd') 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /timeless/celery.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | 3 | 4 | def make_celery(app): 5 | celery = Celery( 6 | app.import_name, 7 | backend=app.config["RESULT_BACKEND"], 8 | broker=app.config["BROKER_URL"] 9 | ) 10 | celery.conf.update(app.config) 11 | 12 | class ContextTask(celery.Task): 13 | def __call__(self, *args, **kwargs): 14 | with app.app_context(): 15 | return self.run(*args, **kwargs) 16 | 17 | celery.Task = ContextTask 18 | return celery 19 | -------------------------------------------------------------------------------- /timeless/templates/auth/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Forgot password{% endblock %}

5 | {% endblock %} 6 | {% block primary_content %} 7 |

Forgot your password?

8 |

Please put your e-mail on the textbox below and we will send you a new one

9 | {% endblock %} 10 | {% block content %} 11 |
12 | 13 | 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.4", 7 | "react-dom": "^16.8.4", 8 | "react-scripts": "2.1.8" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /timeless/templates/reservations/settings/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Reservation Settings management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.id) }} 11 | {{ render_field(form.name) }} 12 | {{ render_field(form.value) }} 13 |

14 | 15 |

16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /timeless/access_control/administrator_privileges.py: -------------------------------------------------------------------------------- 1 | def has_privilege(method=None, resource=None, *args, **kwargs) -> bool: 2 | """Check if user with Administrator role can access a particular resource. 3 | """ 4 | return __resources.get(resource, lambda *arg: False)( 5 | method, *args, **kwargs 6 | ) 7 | 8 | 9 | def __location_access(method=None, *args, **kwargs): 10 | return True 11 | 12 | 13 | def __employee_access(method=None, *args, **kwargs): 14 | return True 15 | 16 | 17 | __resources = { 18 | "location": __location_access, 19 | "employee": __employee_access 20 | } 21 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-27T152248.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 9eee25222512 4 | Revises: 0d17ec999973 5 | Create Date: 2019-01-27 17:22:04.604430 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '9eee25222512' 11 | down_revision = '79f280c6c1f4' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | 16 | def upgrade(): 17 | # ### commands auto generated by Alembic - please adjust! ### 18 | pass 19 | # ### end Alembic commands ### 20 | 21 | 22 | def downgrade(): 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | pass 25 | # ### end Alembic commands ### 26 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-28T204833.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8e4ed4542586 4 | Revises: 6069798906d9 5 | Create Date: 2019-01-27 16:06:34.295510 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '8e4ed4542586' 11 | down_revision = '6069798906d9' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | 16 | def upgrade(): 17 | # ### commands auto generated by Alembic - please adjust! ### 18 | pass 19 | # ### end Alembic commands ### 20 | 21 | 22 | def downgrade(): 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | pass 25 | # ### end Alembic commands ### 26 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/table_shapes/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Table shapes management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.description) }} 11 | {{ render_field(form.picture) }} 12 |

13 | 14 |

15 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | services: 3 | - postgresql 4 | - redis-server 5 | python: 6 | - "3.6.7" 7 | install: 8 | - pip install -r requirements.txt 9 | - gem install pdd 10 | before_script: 11 | - psql -c "CREATE USER timeless_user WITH 12 | SUPERUSER 13 | CREATEDB 14 | CREATEROLE 15 | INHERIT 16 | LOGIN 17 | ENCRYPTED PASSWORD 'timeless_pwd';" -U postgres 18 | - psql -c 'CREATE DATABASE timelessdb_test;' -U postgres 19 | script: 20 | - pdd -f /dev/null 21 | - python checkstyle.py 22 | - python check_pylint.py 23 | - pytest --cov=./timeless 24 | - codecov --token=9110359e-20f8-46a4-872c-cd31cc35da00 -------------------------------------------------------------------------------- /timeless/templates/restaurants/floors/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Floors management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.location_id) }} 11 | {{ render_field(form.description) }} 12 | 13 | Cancel 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /timeless/access_control/other_privileges.py: -------------------------------------------------------------------------------- 1 | """ Permissions for Master / Intern / Others roles """ 2 | import flask 3 | 4 | 5 | def has_privilege(*args, resource=None, **kwargs) -> bool: 6 | """Check if user with Master / Intern / Others role can access a 7 | particular resource.""" 8 | return __resources.get(resource, lambda *arg: False)(*args, **kwargs) 9 | 10 | 11 | def __employee_access(*args, **kwargs): 12 | """Owner of this role can view and update only self""" 13 | user = flask.g.get("user") 14 | return kwargs.get("employee_id") == user.id if user else False 15 | 16 | 17 | __resources = { 18 | "employee": __employee_access 19 | } 20 | -------------------------------------------------------------------------------- /timeless/templates/reservations/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Reservation management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.start_time) }} 11 | {{ render_field(form.end_time) }} 12 | {{ render_field(form.num_of_persons) }} 13 | {{ render_field(form.comment) }} 14 | {{ render_field(form.status) }} 15 |

16 | 17 |

18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /timeless/templates/views.py: -------------------------------------------------------------------------------- 1 | 2 | def order_by(query, params): 3 | return query.order_by(*[param.replace(":", " ") for param in params]) 4 | 5 | # @todo #260:30min Define GenericFilter and subclasses per entity. 6 | # Change client code to pass instances of a GenericFilter(or subclasses) 7 | # in order to build final filter depending on specific entity, 8 | # making it possible for example to filter by id using equality 9 | # and description using like operator. For more details see 10 | # https://github.com/timelesslounge/timelessis/pull/302#discussion_r258021696 11 | 12 | 13 | def filter_by(query, params): 14 | return query.filter_by(**(dict(p.split("=", maxsplit=1) for p in params))) 15 | -------------------------------------------------------------------------------- /timeless/templates/items/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Items{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for item in items %} 9 |
10 |
11 |
12 |

{{ item['id'] }}

13 |
14 | Edit 15 | Delete 16 |
17 |
18 | {% if not loop.last %} 19 |
20 | {% endif %} 21 | {% endfor %} 22 | {% endblock %} 23 | {% endcache %} 24 | -------------------------------------------------------------------------------- /tests/test_poster.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.poster_mock import free_port, start_server 3 | from timeless.poster.api import Poster 4 | 5 | 6 | @pytest.fixture(scope='module') 7 | def poster(): 8 | port = free_port() 9 | start_server( 10 | port, 11 | locations={"data": "test_data"}, 12 | tables={"data": "test_data"}, 13 | customers={"data": "test_data"} 14 | ) 15 | return Poster(url=f"http://localhost:{port}") 16 | 17 | 18 | def test_locations(poster): 19 | assert poster.locations()["data"] == "test_data" 20 | 21 | 22 | def test_tables(poster): 23 | assert poster.tables()["data"] == "test_data" 24 | 25 | 26 | def test_customers(poster): 27 | assert poster.customers()["data"] == "test_data" 28 | -------------------------------------------------------------------------------- /check_pylint.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | from pylint.lint import Run 5 | 6 | 7 | parser = argparse.ArgumentParser(description='Run pylint') 8 | parser.add_argument( 9 | 'threshold', type=int, nargs='?', default=8, 10 | help='Pls set up threshold for quality score, MAX is 10. ' 11 | 'Script will exit with error if quality score is less then threshold.' 12 | ) 13 | 14 | 15 | def run_pylint(threshold): 16 | run = Run(['timeless'], do_exit=False) 17 | score = run.linter.stats['global_note'] 18 | 19 | if score < threshold: 20 | sys.exit(2) 21 | 22 | 23 | if __name__ == "__main__": 24 | """This script runs pylint checker with threshold value""" 25 | args = parser.parse_args() 26 | run_pylint(args.threshold) 27 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | Edit src/App.js and save to reload. 13 |

14 | 20 | Learn React 21 | 22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /scripts/install/install_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Scripts to install Postgres and init timelessis databases 4 | echo "Start Postgres server" 5 | sudo -u postgres /etc/init.d/postgresql start 6 | until sudo -u postgres psql -U "postgres" -c '\q'; do 7 | >&2 echo "Postgres is unavailable - sleeping" 8 | sleep 1 9 | done 10 | echo "Creating user: timeless_user" 11 | sudo -u postgres psql -c "CREATE USER timeless_user WITH 12 | SUPERUSER 13 | CREATEDB 14 | CREATEROLE 15 | INHERIT 16 | LOGIN 17 | ENCRYPTED PASSWORD 'timeless_pwd';" 18 | echo "Creating database: timelessdb_dev" 19 | sudo -u postgres psql -c "CREATE DATABASE timelessdb_dev;" 20 | echo "Creating database: timelessdb_test" 21 | sudo -u postgres psql -c "CREATE DATABASE timelessdb_test;" 22 | -------------------------------------------------------------------------------- /tests/integration/it_other_privilages_test.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | 4 | from tests import factories 5 | from timeless.access_control.other_privileges import has_privilege 6 | from timeless.access_control.methods import Method 7 | 8 | 9 | @pytest.mark.parametrize("method", ( 10 | Method.READ, 11 | Method.CREATE, 12 | Method.UPDATE, 13 | Method.DELETE 14 | )) 15 | def test_can_access_to_emloyee_resource(method, app): 16 | """ Check that user can access only to own employee account """ 17 | employee = factories.EmployeeFactory() 18 | flask.g.user = employee 19 | assert has_privilege( 20 | method=method, resource="employee", employee_id=employee.id) 21 | assert not has_privilege( 22 | method=method, resource="employee", employee_id=-1) 23 | -------------------------------------------------------------------------------- /timeless/access_control/views.py: -------------------------------------------------------------------------------- 1 | from flask import abort, views, request 2 | from http import HTTPStatus 3 | 4 | from timeless.access_control import authorization 5 | 6 | 7 | class SecuredView(views.MethodView): 8 | """Adds user access control to a specif view. 9 | Example: 10 | class Company(SecuredView): 11 | 12 | resource="company" 13 | 14 | def get(self, company_id): 15 | ... 16 | """ 17 | 18 | def dispatch_request(self, *args, **kwargs): 19 | if self.resource and not authorization.is_allowed( 20 | method=request.method.lower(), resource=self.resource, 21 | *args, **kwargs 22 | ): 23 | abort(HTTPStatus.FORBIDDEN) 24 | return super().dispatch_request(*args, **kwargs) 25 | -------------------------------------------------------------------------------- /timeless/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {# @todo #283:30min Change the style of login page. Lets change the#} 4 | {# style of a login page to get the UX like demonstrated in a picture below#} 5 | {# https://drive.google.com/uc?export=view&id=1oh8cLv-vHTCBmb0RDkHQiyMythC4_M6o#} 6 | {% cache 600 %} 7 | {% block header %} 8 |

{% block title %}Log In{% endblock %}

9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | {% endblock %} 20 | {% endcache %} 21 | -------------------------------------------------------------------------------- /timeless/templates/reservations/settings/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Reservations Settings{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for setting in settings %} 9 |
10 |
11 |

{{ setting['name']}} - {{ setting['value']}}

12 | Edit 13 | Delete 14 |
15 |
16 | {% if not loop.last %} 17 |
18 | {% endif %} 19 | {% endfor %} 20 | {% endblock %} 21 | {% endcache %} 22 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/tables/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Tables management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.name) }} 11 | {{ render_field(form.x) }} 12 | {{ render_field(form.y) }} 13 | {{ render_field(form.width) }} 14 | {{ render_field(form.height) }} 15 | {{ render_field(form.status) }} 16 | {{ render_field(form.multiple) }} 17 | {{ render_field(form.playstation) }} 18 | {{ render_field(form.max_capacity) }} 19 | 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /timeless/templates/roles/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Locations management - Main{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for role in roles %} 9 |
10 |
11 |
12 |

{{ role['name'] }}

13 |
14 | Edit 15 |
16 |

{{ role['works_on_shifts'] }}

17 |

{{ role['company'] }}

18 |
19 | {% if not loop.last %} 20 |
21 | {% endif %} 22 | {% endfor %} 23 | {% endblock %} 24 | {% endcache %} -------------------------------------------------------------------------------- /timeless/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock %} - Timlessis 4 | 5 | 17 |
18 |
19 | {% block header %}{% endblock %} 20 |
21 | {% for message in get_flashed_messages() %} 22 |
{{ message }}
23 | {% endfor %} 24 | {% block content %}{% endblock %} 25 |
26 | -------------------------------------------------------------------------------- /tests/view/test_generic_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from timeless.views import ListView 4 | from timeless.customers.models import Customer 5 | 6 | 7 | class TestListView(ListView): 8 | model = Customer 9 | 10 | 11 | @pytest.mark.parametrize("query_string,sql_query", ( 12 | ("/?ordering=id,-first_name", 13 | "ORDER BY customers.id ASC, customers.first_name DESC"), 14 | ("/?ordering=id,+first_name", 15 | "ORDER BY customers.id ASC"), 16 | ("/?ordering=id,foobar", 17 | "ORDER BY customers.id ASC"), 18 | )) 19 | def test_view_ordering(query_string, sql_query, app): 20 | view = TestListView() 21 | query = Customer.query 22 | with app.test_request_context(query_string): 23 | query = view.sort_query(query) 24 | assert sql_query in str(query) 25 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-22T094339.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 503a67df3f7f 4 | Revises: 5 | Create Date: 2019-01-21 16:20:49.552051 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '503a67df3f7f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('companies', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.PrimaryKeyConstraint('id') 24 | ) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_table('companies') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /timeless/reservations/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for reservation blueprint""" 2 | 3 | from timeless import forms 4 | from timeless.reservations.models import ReservationSettings 5 | from timeless.restaurants.models import Reservation 6 | 7 | 8 | class ReservationForm(forms.ModelForm): 9 | """Base form for reservation""" 10 | 11 | class Meta: 12 | model = Reservation 13 | 14 | 15 | class SettingsForm(forms.ModelForm): 16 | """ Base form for creating / updating Settings """ 17 | 18 | class Meta: 19 | """ Meta for Table form 20 | @todo #186:30min excluding greeting_by_time is temporary solution. 21 | forms.BaseModelForm can't handle JSON field. 22 | Need to research how to fix this case and fix it. 23 | """ 24 | model = ReservationSettings 25 | exclude = ["greeting_by_time"] 26 | -------------------------------------------------------------------------------- /timeless/templates/schemetypes/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Scheme Types management - Main{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for type in object_list %} 9 |
10 |
11 |
12 |

{{ type['description'] }}

13 |
14 | Edit 15 |
16 |

Default value: {{ type['default_value'] }}

17 |

Value type: {{ type['value_type'] }}

18 |
19 | {% if not loop.last %} 20 |
21 | {% endif %} 22 | {% endfor %} 23 | {% endblock %} 24 | {% endcache %} 25 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/floors/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Floors management - Main{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for floor in object_list %} 9 |
10 |
11 |
12 |

{{ floor['description'] }}

13 |
14 | Edit 15 | Destroy 16 |
17 |

{{ floor['location'] }}

18 |
19 | {% if not loop.last %} 20 |
21 | {% endif %} 22 | {% endfor %} 23 | {% endblock %} 24 | {% endcache %} 25 | -------------------------------------------------------------------------------- /tests/integration/it_administrator_privilages_test.py: -------------------------------------------------------------------------------- 1 | from timeless.access_control.administrator_privileges import has_privilege 2 | from timeless.access_control.methods import Method 3 | 4 | 5 | def test_can_access_and_change_locations(app): 6 | assert has_privilege(method=Method.READ, resource="location") 7 | assert has_privilege(method=Method.CREATE, resource="location") 8 | assert has_privilege(method=Method.UPDATE, resource="location") 9 | assert has_privilege(method=Method.DELETE, resource="location") 10 | 11 | 12 | def test_can_access_and_change_employees(app): 13 | assert has_privilege(method=Method.READ, resource="employee") 14 | assert has_privilege(method=Method.CREATE, resource="employee") 15 | assert has_privilege(method=Method.UPDATE, resource="employee") 16 | assert has_privilege(method=Method.DELETE, resource="employee") 17 | -------------------------------------------------------------------------------- /scripts/backup/pg_backup.config: -------------------------------------------------------------------------------- 1 | ############################## 2 | ## POSTGRESQL BACKUP CONFIG ## 3 | ############################## 4 | 5 | # Optional system user to run backups as. 6 | # Script will be terminated if the user does't match. 7 | # Leave it blank to skip the check. 8 | BACKUP_USER= 9 | 10 | # Optional hostname to adhere to pg_hba policies. 11 | # "localhost" if not specified. 12 | HOSTNAME= 13 | 14 | # Optional username to connect to database as. 15 | # "postgres" if not specified. 16 | USR= 17 | 18 | # The directory will be created if not exists. 19 | # Have to be writable by backup user. 20 | BACKUP_DIR=/tmp/backup 21 | 22 | # Database name 23 | DATABASE="timelessdb" 24 | 25 | # Google Drive backup settings 26 | SERVICE_ACCOUNT_CREDENTIALS=timeless-db-backup.json 27 | TIMEOUT=0 28 | BACKUP_PATH=./ 29 | FILE_ID= 30 | 31 | ###################################### 32 | -------------------------------------------------------------------------------- /timeless/message/property_resource.py: -------------------------------------------------------------------------------- 1 | """ Message resource for internationalization based on a property file """ 2 | 3 | from timeless.message.message_resource import MessageResource 4 | 5 | 6 | class PropertyResource(MessageResource): 7 | """ Resources read from a property file. 8 | @todo #269:30min Implement FileResource. FileResource should read a 9 | property file and return the value according to the keys. Locale 10 | should be used to define from which locale the file belongs. The 11 | locale is defined in the fine name, for example, 12 | messages_en_US.properties. An exception must be raised if some 13 | resource is not found on property file. Then remove the skip 14 | annotations from it_test_property_resource.py 15 | 16 | """ 17 | 18 | def get(self, key): 19 | raise Exception("PropertyResources not implement yet!") 20 | -------------------------------------------------------------------------------- /timeless/templates/schemetypes/schemeconditions/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Scheme Conditions{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for type in object_list %} 9 |
10 |
11 |
12 |

{{ type['value'] }}

13 |
14 | Edit 15 |
16 |

Priority: {{ type['priority'] }}

17 |

Start time: {{ type['start_time'] }}

18 |

End time: {{ type['end_time'] }}

19 |
20 | {% if not loop.last %} 21 |
22 | {% endif %} 23 | {% endfor %} 24 | {% endblock %} 25 | {% endcache %} 26 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-25T202746.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: be62103a5fd6 4 | Revises: 503a67df3f7f 5 | Create Date: 2019-01-22 23:53:15.034772 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'be62103a5fd6' 14 | down_revision = '503a67df3f7f' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('companies', sa.Column('name', sa.String(), nullable=True)) 22 | op.create_unique_constraint(None, 'companies', ['name']) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_constraint(None, 'companies', type_='unique') 29 | op.drop_column('companies', 'name') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /scripts/install/deploy/install_redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script for Redis availability check, installation and launch 4 | 5 | CURRENT_DIR=`pwd` 6 | 7 | which redis-cli 8 | if [ "$?" -gt "0" ]; then 9 | echo "Redis Not installed, installing" 10 | sudo apt -y install make gcc libc6-dev tcl 11 | wget http://download.redis.io/redis-stable.tar.gz 12 | tar xvzf redis-stable.tar.gz 13 | cd redis-stable 14 | sudo make install 15 | cd .. 16 | sudo rm -rf redis-stable.tar.gz redis-stable/ 17 | echo "Done installing Redis" 18 | else 19 | echo "Redis already installed" 20 | fi 21 | 22 | echo "Redis PING" 23 | redis-cli ping 24 | if [ "$?" -gt "0" ]; then 25 | echo "Redis Not running, launching" 26 | redis-server > /dev/null & 27 | echo "Redis launched" 28 | else 29 | echo "Redis already running" 30 | fi 31 | 32 | sudo cp $CURRENT_DIR/scripts/install/deploy/redis.service /lib/systemd/system/ 33 | sudo systemctl enable redis 34 | -------------------------------------------------------------------------------- /timeless/templates/employees/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% from "_formhelpers.html" import render_field %} 3 | 4 | {% block header %} 5 |

{% block title %}Employees management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {{ render_field(form.first_name) }} 11 | {{ render_field(form.last_name) }} 12 | {{ render_field(form.username) }} 13 | {{ render_field(form.phone_number) }} 14 | {{ render_field(form.birth_date) }} 15 | {{ render_field(form.registration_date) }} 16 | {{ render_field(form.account_status) }} 17 | {{ render_field(form.user_status) }} 18 | {{ render_field(form.email) }} 19 | {{ render_field(form.password) }} 20 | {{ render_field(form.pin_code) }} 21 | {{ render_field(form.comment) }} 22 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /timeless/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import wraps 3 | 4 | from timeless import DB 5 | 6 | 7 | class TimestampsMixin: 8 | """Mixin for adding created_on and updated_on attributes 9 | to any models that need to keep track of their updates. 10 | """ 11 | created_on = DB.Column(DB.DateTime, default=datetime.utcnow, 12 | nullable=False) 13 | updated_on = DB.Column(DB.DateTime, default=datetime.utcnow, 14 | onupdate=datetime.utcnow, nullable=False) 15 | 16 | 17 | def validate_required(*expected_args): 18 | """ Validate input params as mandatory """ 19 | def decorator(func): 20 | @wraps(func) 21 | def wrapper(*args, **kwargs): 22 | for arg in expected_args: 23 | if arg not in kwargs: 24 | raise KeyError("Missing required param: {}".format(arg)) 25 | return func(*args, **kwargs) 26 | return wrapper 27 | return decorator 28 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | file_template = %%(year)d-%%(month).2d-%%(day).2dT%%(hour).2d%%(minute).2d%%(second).2d 6 | timezone = UTC 7 | 8 | # set to 'true' to run the environment during 9 | # the 'revision' command, regardless of autogenerate 10 | # revision_environment = false 11 | 12 | 13 | # Logging configuration 14 | [loggers] 15 | keys = root,sqlalchemy,alembic 16 | 17 | [handlers] 18 | keys = console 19 | 20 | [formatters] 21 | keys = generic 22 | 23 | [logger_root] 24 | level = WARN 25 | handlers = console 26 | qualname = 27 | 28 | [logger_sqlalchemy] 29 | level = WARN 30 | handlers = 31 | qualname = sqlalchemy.engine 32 | 33 | [logger_alembic] 34 | level = INFO 35 | handlers = 36 | qualname = alembic 37 | 38 | [handler_console] 39 | class = StreamHandler 40 | args = (sys.stderr,) 41 | level = NOTSET 42 | formatter = generic 43 | 44 | [formatter_generic] 45 | format = %(levelname)-5.5s [%(name)s] %(message)s 46 | datefmt = %H:%M:%S 47 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-25T120716.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 79f280c6c1f4 4 | Revises: 4d7589fae495 5 | Create Date: 2019-01-25 14:06:07.312734 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '79f280c6c1f4' 14 | down_revision = '4d7589fae495' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('floors', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('location_id', sa.Integer(), nullable=True), 24 | sa.Column('description', sa.String(), nullable=True), 25 | sa.ForeignKeyConstraint(['location_id'], ['locations.id'], ), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('floors') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /tests/integration/it_companies_test.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | 4 | from tests import factories 5 | 6 | """ 7 | @todo #182:30min Inject user privileges into the test below. Test is broken 8 | because we do not set user privileges and global user privilege logic defined 9 | in #182 (get roles from user in session). We must simulate this role to user 10 | for text execution and then uncomment the test. Use factories and cerate a 11 | roles enumeration for tests with the values in authorization.py 12 | """ 13 | 14 | 15 | @pytest.mark.skip(reason="Test must set user privileges") 16 | def test_company_endpoints(client): 17 | employee = factories.EmployeeFactory( 18 | company=factories.CompanyFactory() 19 | ) 20 | 21 | with client.session_transaction() as session: 22 | session["user_id"] = employee.id 23 | 24 | url = flask.url_for('companies.api', company_id=employee.company_id) 25 | assert client.get(url).status_code == 200 26 | assert client.put(url).status_code == 200 27 | assert client.delete(url).status_code == 204 28 | -------------------------------------------------------------------------------- /tests/integration/it_authorization_test.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | 4 | from tests import factories 5 | from timeless.access_control.authorization import is_allowed 6 | from timeless.access_control.methods import Method 7 | from timeless.roles.models import RoleType 8 | 9 | 10 | @pytest.mark.parametrize('role_name,role_type', ( 11 | ("manager", RoleType.Manager), 12 | ("director", RoleType.Director), 13 | ("administrator", RoleType.Administrator), 14 | ("owner", RoleType.Owner), 15 | )) 16 | def test_can_access_employee_resource(role_name, role_type): 17 | print('role_type', role_type) 18 | my_company = factories.CompanyFactory() 19 | my_role = factories.RoleFactory( 20 | name=role_name, role_type=role_type) 21 | # set my role 22 | me = factories.EmployeeFactory(company=my_company, role=my_role) 23 | flask.g.user = me 24 | # set employee 25 | other = factories.EmployeeFactory(company=my_company) 26 | assert is_allowed( 27 | method=Method.READ, resource="employee", employee_id=other.id 28 | ) 29 | -------------------------------------------------------------------------------- /tests/integration/it_poster_test.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from timeless.poster.api import Authenticated, PosterAuthData 4 | 5 | """Integration tests for Poster""" 6 | 7 | """ 8 | @todo #113:30min Implement auth process for Poster API. 9 | client_id is required for authentication process, this id is the public key 10 | provided by the poster service to identify the applications 11 | """ 12 | 13 | @mock.patch("timeless.poster.api.requests") 14 | def test_auth(requests_mock): 15 | auth_data = PosterAuthData( 16 | application_id="test_id", 17 | application_secret="test_secret", 18 | redirect_uri="test_uri", 19 | code="test_code", 20 | ) 21 | 22 | class Response: 23 | auth_token = "861052:02391570ff9af128e93c5a771055ba88" 24 | 25 | def ok(self): 26 | return True 27 | 28 | def json(self): 29 | return {"access_token": self.auth_token} 30 | 31 | requests_mock.post.return_value = Response() 32 | 33 | auth_token = Authenticated(auth_data).auth() 34 | assert auth_token == Response.auth_token 35 | -------------------------------------------------------------------------------- /timeless/companies/views.py: -------------------------------------------------------------------------------- 1 | """Company views module.""" 2 | 3 | from timeless.access_control.views import SecuredView 4 | 5 | 6 | class Resource(SecuredView): 7 | 8 | resource = "company" 9 | 10 | """API Resource for companies /api/companies""" 11 | def get(self, company_id): 12 | """ 13 | Get method of Resource 14 | """ 15 | if company_id: 16 | return "Detail get method of CompanyViewSet", 200 17 | return "Get method of CompanyViewSet", 200 18 | 19 | def post(self): 20 | """Post method of Resource""" 21 | return "Post method of CompanyViewSet", 201 22 | 23 | def put(self, company_id): 24 | """Put method of Resource""" 25 | if company_id: 26 | return "Detail put method of CompanyViewSet", 200 27 | return "Put method of CompanyViewSet", 200 28 | 29 | def delete(self, company_id): 30 | """Delete method of Resource""" 31 | if company_id: 32 | return "Detail delete method of CompanyViewSet", 204 33 | return "Delete method of CompanyViewSet", 204 34 | -------------------------------------------------------------------------------- /migrations/versions/2019-02-26T054508.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 65535c7283b5 4 | Revises: fb887393e975 5 | Create Date: 2019-02-26 05:45:08.668445+00:00 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '65535c7283b5' 14 | down_revision = 'fb887393e975' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('employees', 22 | sa.Column('role_id', sa.Integer(), nullable=True)) 23 | op.create_foreign_key('fk_employees_roles', 24 | 'employees', 'roles', 25 | ['role_id'], ['id']) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_constraint('fk_employees_roles', 'employees', type_='foreignkey') 32 | op.drop_column('employees', 'role_id') 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /timeless/access_control/location_admin_privileges.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | from timeless.restaurants.models import Table 4 | 5 | 6 | def has_privilege(method=None, resource=None, *args, **kwargs) -> bool: 7 | """Check if user with Location Admin role can access a particular resource. 8 | """ 9 | return __resources.get(resource, lambda *arg: False)( 10 | method, *args, **kwargs 11 | ) 12 | 13 | 14 | def __table_access(method=None, *args, **kwargs): 15 | """Check if a user has access to a location's tables 16 | @todo #177:30min Continue implementing access check for reservations, 17 | reservation settings and reservation comments. Don't forget to add 18 | integration tests. See issue #22 as reference for what a Location Admin 19 | should be able to do. 20 | """ 21 | permitted, user = False, flask.g.get("user") 22 | table_id = kwargs.get("id") 23 | if user and table_id: 24 | location = Table.query.get(table_id).floor.location 25 | if location.company_id == user.company_id: 26 | permitted = True 27 | return permitted 28 | 29 | 30 | __resources = { 31 | "tables": __table_access 32 | } 33 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/table_shapes/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block header %} 3 | {% cache 600 %} 4 |

{% block title %}Table shapes management - Main{% endblock %}

5 | {% endcache %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% cache 600 %} 10 |
11 | Description:
12 | 13 |
14 | 15 |
16 | {% for table_shape in table_shapes %} 17 |
18 |
19 | Edit 20 |
21 |

{{ table_shape['description'] }}

22 | 23 |
24 | {% if not loop.last %} 25 |
26 | {% endif %} 27 | {% endfor %} 28 | {% endcache %} 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /timeless/restaurants/tables/views.py: -------------------------------------------------------------------------------- 1 | """tables views module. 2 | """ 3 | from flask import Blueprint 4 | 5 | from timeless import views 6 | from timeless.restaurants import models 7 | from timeless.restaurants.tables import forms 8 | 9 | 10 | BP = Blueprint("table", __name__, url_prefix="/tables") 11 | 12 | 13 | class TableListView(views.ListView): 14 | """ List the tables """ 15 | model = models.Table 16 | template_name = "restaurants/tables/list.html" 17 | 18 | 19 | class Create(views.CreateView): 20 | form_class = forms.TableForm 21 | success_view_name = "table.list_tables" 22 | template_name = "restaurants/tables/create_edit.html" 23 | 24 | 25 | class Edit(views.UpdateView): 26 | """View for editing a table""" 27 | model = models.Table 28 | form_class = forms.TableForm 29 | template_name = "restaurants/tables/create_edit.html" 30 | success_view_name = "table.list_tables" 31 | 32 | 33 | class Delete(views.DeleteView): 34 | """View for deleting a table""" 35 | model = models.Table 36 | success_view_name = "table.list_tables" 37 | 38 | 39 | TableListView.register(BP, "/") 40 | Create.register(BP, "/create") 41 | Edit.register(BP, "/edit/") 42 | Delete.register(BP, "/delete/") 43 | -------------------------------------------------------------------------------- /timeless/templates/reservations/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 | {% cache 600 %} 5 |

{% block title %}Reservations{% endblock %}

6 | {% endcache %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% cache 600 %} 11 | {% for reservation in object_list %} 12 |
13 |
14 | Edit 15 | Delete 16 |
17 |

ID: {{ reservation['id'] }}

18 |

Start time: {{ reservation['start_time'] }}

19 |

End time: {{ reservation['end_time'] }}

20 |

Num of persons: {{ reservation['num_of_persons'] }}

21 |

Comment: {{ reservation['comment'] }}

22 |

Status: {{ reservation['status'] }}

23 |
24 | {% if not loop.last %} 25 |
26 | {% endif %} 27 | {% endfor %} 28 | {% endcache %} 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-28T205516.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b15df456e2cd 4 | Revises: bbd99136a04d 5 | Create Date: 2019-01-28 21:54:57.088367 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b15df456e2cd' 14 | down_revision = 'bbd99136a04d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('scheme_conditions', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('value', sa.String(), nullable=False), 24 | sa.Column('priority', sa.Integer(), nullable=False), 25 | sa.Column('start_time', sa.DateTime(), nullable=False), 26 | sa.Column('end_time', sa.DateTime(), nullable=False), 27 | sa.PrimaryKeyConstraint('id'), 28 | sa.UniqueConstraint('value') 29 | ) 30 | op.add_column('locations', sa.Column('poster_id', sa.Integer(), nullable=True)) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_column('locations', 'poster_id') 37 | op.drop_table('scheme_conditions') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /timeless/access_control/director_privileges.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | from timeless.employees.models import Employee 4 | 5 | 6 | def has_privilege(method=None, resource=None, *args, **kwargs) -> bool: 7 | """Check if user with Director role can access a particular resource.""" 8 | return __resources.get(resource, lambda *arg: False)( 9 | method, *args, **kwargs 10 | ) 11 | 12 | 13 | def __employee_access(method=None, *args, **kwargs): 14 | permitted, user = False, flask.g.get("user") 15 | employee_id = kwargs.get("employee_id") 16 | if user: 17 | if employee_id: 18 | permitted = check_rights(method, employee_id, user) 19 | else: 20 | permitted = True 21 | return permitted 22 | 23 | 24 | def check_rights(method, employee_id, user): 25 | """ 26 | Check the rights a director has over an employee profile. 27 | In principle, a director can access their own profile and the 28 | profile of the employees with a lower role, who work at the same company. 29 | """ 30 | if employee_id == user.id: 31 | return True 32 | else: 33 | other = Employee.query.get(employee_id) 34 | return user.company_id == other.company_id and user.role.is_director() 35 | 36 | 37 | __resources = { 38 | "employee": __employee_access 39 | } 40 | -------------------------------------------------------------------------------- /timeless/uploads/__init__.py: -------------------------------------------------------------------------------- 1 | """File Uploads""" 2 | from flask_uploads import UploadSet, IMAGES as IMAGES_, configure_uploads 3 | 4 | 5 | IMAGES = None 6 | 7 | 8 | def images(app): 9 | """ 10 | Creates images upload set. 11 | :param app: Flask application instance 12 | :return: Images upload set created 13 | @todo #205:30min Introduce Flask-Uploads configuration parameters 14 | to app config.According to Flask-Uploads, there is a need to introduce 15 | additional configuration parameters to the application config instance. 16 | Make sure UPLOADS_DEFAULT_URL, UPLOADED_IMAGES_URL values are calculated 17 | according to the current runtime host/port information and 18 | also UPLOADS_DEFAULT_DEST and UPLOADED_IMAGES_DEST values 19 | point to correct static resources path 20 | """ 21 | # Configure the image uploading via Flask-Uploads 22 | result = UploadSet("images", IMAGES_) 23 | path = "/project/static/img/" 24 | app.config["UPLOADS_DEFAULT_DEST"] = app.instance_path + path 25 | app.config["UPLOADS_DEFAULT_URL"] = "http://localhost:5000/static/img/" 26 | 27 | app.config["UPLOADED_IMAGES_DEST"] = app.instance_path + path 28 | app.config["UPLOADED_IMAGES_URL"] = "http://localhost:5000/static/img/" 29 | 30 | configure_uploads(app, result) 31 | return result 32 | -------------------------------------------------------------------------------- /timeless/restaurants/locations/views.py: -------------------------------------------------------------------------------- 1 | """Locations views module.""" 2 | from flask import Blueprint 3 | 4 | from timeless import views 5 | from timeless.restaurants.models import Location 6 | from timeless.restaurants.locations.forms import LocationForm 7 | 8 | 9 | BP = Blueprint("location", __name__, url_prefix="/locations") 10 | 11 | 12 | class Create(views.CreateView): 13 | """ Create a new location instance""" 14 | template_name = "restaurants/locations/create_edit.html" 15 | success_view_name = "location.list" 16 | form_class = LocationForm 17 | 18 | 19 | class Edit(views.UpdateView): 20 | """Update location""" 21 | template_name = "restaurants/locations/create_edit.html" 22 | form_class = LocationForm 23 | model = Location 24 | success_view_name = "location.list" 25 | 26 | 27 | class Delete(views.DeleteView): 28 | """Delete location 29 | Deletes location using id and redirects to list page 30 | """ 31 | success_view_name = "location.list" 32 | model = Location 33 | 34 | 35 | class List(views.ListView): 36 | """List all locations""" 37 | template_name = "restaurants/locations/list.html" 38 | model = Location 39 | 40 | 41 | List.register(BP, "/") 42 | Create.register(BP, "/create") 43 | Edit.register(BP, "/edit/") 44 | Delete.register(BP, "/delete/") 45 | -------------------------------------------------------------------------------- /credentials/staging.id_rsa.pub.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP MESSAGE----- 2 | 3 | hQEMA5qETcGag5w6AQf+MeEUI3mPjO06LGQhMRLN5zk4g/Te2fIVoyyWY5F6dpt+ 4 | 7Ua2jPKOf7QDr564l1AVURVuvBrhqdM1elz5GzTMr3Pjf4GKCi1rElE7RdC6jpit 5 | DkLCMvkS+EoxzZWPLLdm2F+VJKlqw40wizngCSfeqfTKiv6BSQ9MaEzr7iKJFIzX 6 | YZw4fgTJKKXqKBAZVs3Okp72r0traBvBJkIp2gEUeS5be9Xm9dgKOVkk7sDR4Q0k 7 | XR65ymsSaKfctfliaMoztuyQKcM9SWM8/1htdnOyyTYlO7NCJgi65ynj06jeMnq8 8 | 6kWY/m4Lbw/WlcwH0Bp5Gf+AoB9SpZ6GynFB1/EfHNLpAQiMNxlX6VCZA2cYYtD5 9 | XERSOUJAo2O3oOnJyi57mfiPsDoWmZPKbgS8qrGV2hp1XJcetXC1FCMZnI/UeSwV 10 | P+4HhRadNEpngjMfGhKzop21+0pVqPwgkry9+YbGdg6oaC6TQuf7AhuaYlrx+q28 11 | +KTtEvgILHmp0Sj/b19Je78QXsWamzjdUZhRCRrBEvviU178UqVbTJnM8zWOXJxi 12 | vV6MPJKs3FALxr/l81FKd9rpppyP8cwy5EwksuEPSZx2yR9We4t0KE1sZADc27xn 13 | Npf4GhP8tjQGUlC8dNLYb9ngLLCK/cJygbLbd7KYSteqJIJznUCgBMheFS97RmtU 14 | E222HJBTYHv8qOdfZ2LaT35XoW/VMuc6gy7XVFJCESkTlu/7KjMt/8vhfp937g1h 15 | lXykl6Rue/9QqMlpKP4KPhzbYo+PB8e9cepMfVVCt4Av/iD8RzWh5/q0w+XGtt5s 16 | 5aCxzAov1YoVYFXzP/c0zzn7usyxV8eI/puiswDBQLPyvdbrkh59EKyapGs4qfAF 17 | hvFtSh6U1iCyx5mRGGJMiBvXv4pJhmmY1u1Oi2LqVb7F6Y1Z4vmIWuFPXl1W3UwF 18 | PgmXezTBpFQANqyrcw/MTj2iOR6mIEhKk+oszdAvVIWWqui1POodiUUIw0Xqt7K3 19 | SgJ4tfvYMyLtQ50xQ7zTD6paAnbea6zIY6pCKY+srBZKsXe8I12cQDfPmYdyD1Qs 20 | 1frPOLKdoeuz9Pv7qCFUGcIpX5yLYi5Gfrn6fQOgEpEc6is9Tvu0DxuEsrfF6Gfl 21 | YMJZmJcekiMLCweH 22 | =QfEq 23 | -----END PGP MESSAGE----- 24 | -------------------------------------------------------------------------------- /timeless/restaurants/table_shapes/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for table shapes blueprint in order to support CRUD operations""" 2 | from flask_wtf.file import FileField 3 | 4 | from timeless import forms 5 | from timeless.restaurants import models 6 | 7 | 8 | class TableShapeForm(forms.ModelForm): 9 | """Base form for table shape""" 10 | 11 | picture = FileField("Table Shape Image") 12 | 13 | class Meta: 14 | model = models.TableShape 15 | exclude = ("picture",) 16 | 17 | def __init__(self, *args, **kwargs): 18 | self._file = kwargs.pop("files").get("files") 19 | super(TableShapeForm, self).__init__(*args, **kwargs) 20 | 21 | def save(self, commit=True): 22 | """ 23 | Saves uploaded image 24 | :param commit: To commit flag 25 | :return: Saved instance 26 | @todo #205:30min Lets save only filename which is a static 27 | part of a url. Hostname, port and perhaps base path 28 | should be calculated dynamically for each table shape 29 | while being rendered to the template. See Configuration 30 | paragraph https://pythonhosted.org/Flask-Uploads/ 31 | """ 32 | filename = self._file.filename 33 | self._file.save(dst=filename) 34 | self.picture.data = filename 35 | super(TableShapeForm, self).save(commit=commit) 36 | -------------------------------------------------------------------------------- /credentials/credentials.json.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP MESSAGE----- 2 | 3 | hQEMA5qETcGag5w6AQf/fgVoHYupY0xLbmnFznrvnKkMjWe5RTVdJEaDyHRmxk19 4 | Ru1AVc/7UqQ1X3/LYKTn5SXLVF+dp5UT83dYC6D3Y1nfnZB66ZuBY4vrUM1NxaSQ 5 | hdGmw31hq8d4jLIOhZxnxETGOkCMGJRvdZSC6p4p7BTMRzUz5+wOJKKolyLya4wM 6 | DkfBZh2kvPNq9nXWNCYzuK9zlWIimbmxxOJ3tPOlPiQ1vxDSGl0viaRx+CfAuOIk 7 | 07xCIPdlPOjWZ7f/brxF1rbEjJs8YYg8iyW64vgmtVzf6ugQI5JxbrjLoHHMrMK6 8 | TfFbmODzrtkK61zHK/1gQrJBHMT4AWPhbFeoA7AZYdLpAWRAjeaWJpCwknPf+zLk 9 | WmeLIk/M0Cdo5x0idltfRwHKG1n2rZkAo05rt0dd9SgVJmjX6RulvSDSGEIY2nBi 10 | 8qm5H0d7pRBThJD1ro7CwxYCgkBCIuNYaTM1U/wiTyAGc4MMr+kiQMVT1QOx94BX 11 | d8AhkJq5W1bIHZTxcpjDmIDpJd8PRgDRcUJzU+z/hzwrN4O8FAjSTs4F03d9ZkTW 12 | 9Phv3EVySMvf3zyFAcW/6LNN4CVCQbclHKcwibFf0p2ZK45R5KvOwIxi889yiydW 13 | AZNT/YUCTea8kmwhTT5bZMl45kSTeIzP+9gh6+/+OH7UMDANXkksfzMDRnG0tv0Y 14 | inD+ZvBNqdAy7SxaXlQcrwYxzDEewlgMLeJfBEhpGL74oJWuDDvqaThb0jLnXijW 15 | RueKtdEFnqUu+DxfCry5voerVLR7Ks3VqWAuo89/N/A/635oKtaWiQMnvflRw9hl 16 | 42p2iLQJ7vgBeQGumU1VRhDhI6SfZmhOwI8EVOh1rJKMHg61foSasTOuhe0YMamh 17 | EerE5dubU/8VTedP1LEda/TG8Pluz3HI6KVLLUVKrdoBDGfwBg5ONY1zLtkShsiC 18 | uat51G7cNiBx9bXtL+kJ3En4jgxh4AlY7phMqtqkT+m+CVkQ0AM8kBGfyvWiBpfd 19 | yYUXR7H/eu0hfEOHVZOiKY56fv8K9D9QH1X0WZki/K9b1pwnczPGwc5Zs6DQ232i 20 | +2HMF3273bVthm55nrZCInfglz+UDcCemZmJXtP6vdgdAolMOQcmU9ZnI8n/zGcS 21 | KExzd84uO5E6V2RurPmc7JWxrrOJ02WEXrcZTJffA6o8DrqiLM7CHAUiMVM= 22 | =TheO 23 | -----END PGP MESSAGE----- 24 | -------------------------------------------------------------------------------- /timeless/forms.py: -------------------------------------------------------------------------------- 1 | """This file contains base form class which helps integrate WTF-Alchemy 2 | and Flask-WTF, since it doesn't work properly ot of the box.""" 3 | 4 | import flask_wtf 5 | import wtforms_alchemy 6 | 7 | from timeless.db import DB 8 | 9 | 10 | BaseModelForm = wtforms_alchemy.model_form_factory(flask_wtf.FlaskForm) 11 | 12 | 13 | class ModelForm(BaseModelForm): 14 | """It's made to support working WTF-Alchemy with Flask-WTF, look at 15 | https://wtforms-alchemy.readthedocs.io/en/latest/advanced.html for 16 | details.""" 17 | 18 | def __init__(self, *args, **kwargs): 19 | self.instance = kwargs.pop("instance", None) 20 | super().__init__(*args, **kwargs) 21 | 22 | @classmethod 23 | def get_session(cls): 24 | return DB.session 25 | 26 | def create(self, session): 27 | self.instance = self.Meta.model() 28 | self.populate_obj(self.instance) 29 | session.add(self.instance) 30 | 31 | def update(self, session): 32 | self.populate_obj(self.instance) 33 | session.merge(self.instance) 34 | 35 | def save(self, commit=True): 36 | session = self.get_session() 37 | 38 | if self.instance: 39 | self.update(session) 40 | else: 41 | self.create(session) 42 | 43 | if commit: 44 | session.commit() 45 | 46 | return self.instance 47 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/tables/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Tables management - Main{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for table in tables %} 9 |
10 |
11 |
12 |

{{ table['name'] }}

13 |
14 | Edit 15 |
16 |

{{ floors[table['floor_id']] }}

17 |

{{ table['x'] }}

18 |

{{ table['y'] }}

19 |

{{ table['width'] }}

20 |

{{ table['height'] }}

21 |

{{ table['status'] }}

22 |

{{ table['max_capacity'] }}

23 |

{{ table['min_capacity'] }}

24 |

{{ table['multiple'] }}

25 |

{{ table['playstation'] }}

26 |

{{ shapes[table['shape_id']] }}

27 |

{{ table['deposit_hour'] }}

28 |
29 | {% if not loop.last %} 30 |
31 | {% endif %} 32 | {% endfor %} 33 | {% endblock %} 34 | {% endcache %} 35 | -------------------------------------------------------------------------------- /checkstyle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Methods to enforce certain code style rules. 3 | """ 4 | import glob 5 | import re 6 | import sys 7 | 8 | 9 | def double_qoutes_only(): 10 | """ DoubleQuotesOnly; Forbid usage of single-quoted strings """ 11 | pattern = "['](?=[^\"]*(?:\"[^\"]*\"[^\"]*)*$)" 12 | for filename in glob.iglob("./timeless/**/*.py", recursive=True): 13 | with open(filename, "r+") as f: 14 | # read file as string containing all the lines divided by \n 15 | data = f.read() 16 | prev_line = 0 17 | prev_offset = 0 18 | match_found = False 19 | for m in re.finditer(pattern, data): 20 | match_found = True 21 | start = m.start() 22 | # count number of \n chars from 0 to start to get line number 23 | current_line = data.count("\n", 0, start) + 1 24 | # find position of single qoute in line 25 | offset = start - data.rfind("\n", 0, start) 26 | if prev_line == current_line: 27 | print(f"Found single-qoutes: {f.name}({current_line}," 28 | f"{prev_offset}-{offset})") 29 | prev_line = current_line 30 | prev_offset = offset 31 | if match_found: 32 | sys.exit(2) 33 | 34 | if __name__ == "__main__": 35 | double_qoutes_only() 36 | -------------------------------------------------------------------------------- /tests/integration/it_comments_test.py: -------------------------------------------------------------------------------- 1 | """ Integration tests for Comment """ 2 | from datetime import datetime 3 | from http import HTTPStatus 4 | 5 | import pytest 6 | 7 | from flask import url_for 8 | 9 | from timeless.reservations.models import Comment 10 | 11 | """ 12 | @todo #222:30min Correct comments its_tests. After #222 Comments had the logic 13 | changed and its tests are broken. Correct them and remove pytest.mark.skip 14 | annotation 15 | """ 16 | 17 | 18 | @pytest.mark.skip 19 | def test_comments_endpoints(client): 20 | url = url_for("/api/comments/") 21 | assert client.get(url).status_code == HTTPStatus.OK 22 | assert client.post(url).status_code == HTTPStatus.CREATED 23 | 24 | # detail resource 25 | url = url_for("/api/comments/", comment_id=3) 26 | assert client.put(url).status_code == HTTPStatus.OK 27 | 28 | assert client.delete(url).status_code == HTTPStatus.NO_CONTENT 29 | 30 | 31 | @pytest.mark.skip 32 | def test_get_single_comment(client, db_session): 33 | comment = Comment(body="My comment", date=datetime.utcnow()) 34 | db_session.add(comment) 35 | db_session.commit() 36 | url = url_for("/api/comments/", comment_id=1) 37 | assert client.get().status_code == HTTPStatus.OK 38 | 39 | 40 | @pytest.mark.xfail(raises=Exception) 41 | def test_comment_not_found(client): 42 | url = url_for("/api/comments/", comment_id=2) 43 | client.get() 44 | -------------------------------------------------------------------------------- /timeless/access_control/manager_privileges.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | from timeless.access_control.methods import Method 4 | from timeless.employees.models import Employee 5 | 6 | 7 | def has_privilege(method=None, resource=None, *args, **kwargs) -> bool: 8 | """Check if user with Manager role can access a particular resource.""" 9 | return __resources.get(resource, lambda *arg: False)( 10 | method, *args, **kwargs 11 | ) 12 | 13 | 14 | def __employee_access(method=None, *args, **kwargs): 15 | permitted, user = False, flask.g.get("user") 16 | employee_id = kwargs.get("employee_id") 17 | if user: 18 | if employee_id: 19 | permitted = check_employee(employee_id, method, user) 20 | else: 21 | permitted = True 22 | return permitted 23 | 24 | 25 | def check_employee(employee_id, method, user): 26 | if employee_id == user.id: 27 | return True 28 | 29 | employee = Employee.query.get(employee_id) 30 | 31 | if not employee or user.company_id != employee.company_id: 32 | # User cannot do anything if employee does not belong to his company 33 | return False 34 | 35 | if method == Method.READ: 36 | return True 37 | 38 | # Manager can edit not own account only if it is a master or intern 39 | return employee.role.is_master_or_intern() 40 | 41 | 42 | __resources = { 43 | "employee": __employee_access, 44 | } 45 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-28T150634.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1029025e393e 4 | Revises: 9eee25222512 5 | Create Date: 2019-01-28 17:05:56.757201 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1029025e393e' 14 | down_revision = '49f5103c70fe' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('scheme_types', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('description', sa.String(), nullable=False), 24 | sa.Column('default_value', sa.String(), nullable=False), 25 | sa.Column('value_type', sa.String(), nullable=False), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('description') 28 | ) 29 | op.add_column('scheme_conditions', sa.Column('scheme_type_id', sa.Integer(), nullable=True)) 30 | op.create_foreign_key(None, 'scheme_conditions', 'scheme_types', ['scheme_type_id'], ['id']) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_constraint(None, 'scheme_conditions', type_='foreignkey') 37 | op.drop_column('scheme_conditions', 'scheme_type_id') 38 | op.drop_table('scheme_types') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /.rultor.yml: -------------------------------------------------------------------------------- 1 | architect: 2 | - emilianodellacasa 3 | _decrypt: 4 | credentials.json: "repo/credentials/credentials.json.asc" 5 | staging.id_rsa: "repo/credentials/staging.id_rsa.asc" 6 | staging.id_rsa.pub: "repo/credentials/staging.id_rsa.pub.asc" 7 | install: |- 8 | sudo add-apt-repository -y ppa:jonathonf/python-3.6 9 | sudo apt-get update 10 | sudo apt-get -y install python3.6 python3.6-dev python3.6-venv npm nodejs 11 | python3.6 -m venv venv 12 | . venv/bin/activate 13 | pip install -r requirements.txt 14 | sudo gem install pdd 15 | sudo apt-get install sshpass 16 | chmod +x scripts/install/install_db.sh 17 | ./scripts/install/install_db.sh 18 | chmod +x scripts/install/installRedis.sh 19 | ./scripts/install/installRedis.sh 20 | cd frontend && npm install && cd .. 21 | merge: 22 | script: |- 23 | python checkstyle.py 24 | python check_pylint.py 25 | pytest -vv 26 | cd frontend && export CI=true && npm test && cd .. 27 | # @todo #533:30m Report coverage for React tests in Rultor to make changes in coverage immediately visible adding more words just to meet a not optimal pdd config, see https://github.com/timelesslounge/timelessis/issues/544 28 | pdd -f /dev/null -v 29 | deploy: 30 | script: |- 31 | 32 | pdd -f /dev/null -v 33 | deploy: 34 | script: |- 35 | chmod +x ./deploy.sh 36 | ./deploy.sh 37 | release: 38 | script: |- 39 | echo "Release not supported" 40 | exit -1 41 | -------------------------------------------------------------------------------- /timeless/reservations/models.py: -------------------------------------------------------------------------------- 1 | """File for models in reservations module""" 2 | from timeless.models import TimestampsMixin, validate_required 3 | from timeless import DB 4 | 5 | 6 | class Comment(TimestampsMixin, DB.Model): 7 | """Model for comment business entity""" 8 | __tablename__ = "comments" 9 | 10 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 11 | 12 | employee_id = DB.Column(DB.Integer, DB.ForeignKey("employees.id")) 13 | body = DB.Column(DB.String, nullable=False) 14 | date = DB.Column(DB.DateTime, nullable=False) 15 | employee = DB.Column(DB.Integer, DB.ForeignKey("employees.id")) 16 | 17 | @validate_required("body", "date") 18 | def __init__(self, **kwargs): 19 | super(Comment, self).__init__(**kwargs) 20 | 21 | def __repr__(self): 22 | return "" % self.description 23 | 24 | 25 | class ReservationSettings(TimestampsMixin, DB.Model): 26 | """Settings model for Reservations""" 27 | 28 | __tablename__ = "reservation_settings" 29 | 30 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 31 | name = DB.Column(DB.String, unique=True) 32 | default_duration = DB.Column(DB.SmallInteger) 33 | default_deposit = DB.Column(DB.SmallInteger) 34 | sms_notifications = DB.Column(DB.Boolean) 35 | threshold_sms_time = DB.Column(DB.SmallInteger) 36 | greeting_by_time = DB.Column(DB.JSON) 37 | sex = DB.Column(DB.String) 38 | -------------------------------------------------------------------------------- /timeless/companies/models.py: -------------------------------------------------------------------------------- 1 | """File for models in test_companies module""" 2 | from timeless.db import DB 3 | from timeless.models import TimestampsMixin, validate_required 4 | 5 | 6 | class Company(TimestampsMixin, DB.Model): 7 | """Model for company business entity. 8 | @todo #3:30min Create management pages for Companies to list, create, edit 9 | and delete them. In the index page it should be possible to sort and filter 10 | for every column. 11 | """ 12 | __tablename__ = "companies" 13 | 14 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 15 | name = DB.Column(DB.String, unique=True, nullable=False) 16 | code = DB.Column(DB.String, unique=True, nullable=False) 17 | address = DB.Column(DB.String) 18 | 19 | locations = DB.relationship("Location", order_by="Location.id", 20 | back_populates="company") 21 | employees = DB.relationship("Employee", order_by="Employee.id", 22 | back_populates="company") 23 | roles = DB.relationship("Role", order_by="Role.id", 24 | back_populates="company") 25 | items = DB.relationship("Item", order_by="Item.id", 26 | back_populates="company") 27 | 28 | @validate_required("name", "code") 29 | def __init__(self, **kwargs): 30 | super(Company, self).__init__(**kwargs) 31 | 32 | def __repr__(self): 33 | return "" % self.name 34 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-29T144639.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 4207a669d969 4 | Revises: 9eee25222512 5 | Create Date: 2019-01-28 23:29:28.255031 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4207a669d969' 14 | down_revision = '1029025e393e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('reservation_settings', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(), nullable=True), 24 | sa.Column('default_duration', sa.SmallInteger(), nullable=True), 25 | sa.Column('default_deposit', sa.SmallInteger(), nullable=True), 26 | sa.Column('sms_notifications', sa.Boolean(), nullable=True), 27 | sa.Column('threshold_sms_time', sa.SmallInteger(), nullable=True), 28 | sa.Column('greeting_by_time', sa.JSON(), nullable=True), 29 | sa.Column('sex', sa.String(), nullable=True), 30 | sa.Column('created_on', sa.DateTime(), nullable=False), 31 | sa.Column('updated_on', sa.DateTime(), nullable=True), 32 | sa.PrimaryKeyConstraint('id'), 33 | sa.UniqueConstraint('name') 34 | ) 35 | # ### end Alembic commands ### 36 | 37 | 38 | def downgrade(): 39 | # ### commands auto generated by Alembic - please adjust! ### 40 | op.drop_table('reservation_settings') 41 | # ### end Alembic commands ### 42 | -------------------------------------------------------------------------------- /timeless/access_control/owner_privileges.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | from timeless.access_control.methods import Method 4 | from timeless.employees.models import Employee 5 | from timeless.restaurants.models import Location 6 | 7 | 8 | def has_privilege(method=None, resource=None, *args, **kwargs) -> bool: 9 | """Check if user with Owner role can access a particular resource.""" 10 | return __resources.get( 11 | resource, lambda *arg, **kwargs: False)(method, *args, **kwargs) 12 | 13 | 14 | def __location_access(method=None, *args, **kwargs): 15 | user_company = flask.g.get("user").company_id 16 | location_company = Location.query.get(kwargs.get("id")).company_id 17 | return user_company == location_company 18 | 19 | 20 | def __employee_access(method=None, *args, **kwargs): 21 | user, employee_id = flask.g.user, kwargs.get("employee_id") 22 | if not employee_id: 23 | return True 24 | 25 | employee = Employee.query.get(employee_id) 26 | 27 | if not employee: 28 | return False 29 | 30 | return employee.company_id == user.company_id 31 | 32 | 33 | def __company_access(method=None, *args, **kwargs): 34 | user, company_id = flask.g.get("user"), kwargs.get("company_id") 35 | return user.company_id == company_id 36 | 37 | 38 | __resources = { 39 | "location": __location_access, 40 | "employee": __employee_access, 41 | "company": __company_access, 42 | "reservation_settings": __employee_access, 43 | "reservation_comment": __employee_access 44 | } 45 | -------------------------------------------------------------------------------- /timeless/items/views.py: -------------------------------------------------------------------------------- 1 | from timeless.auth import views as auth 2 | from timeless.views import ListView, CreateView 3 | from timeless.items.forms import ItemForm 4 | from timeless.items.models import Item 5 | """ Views module for Items. 6 | @todo #270:30min Continue implementation of views class using GenericViews. 7 | Use mocks from mock_items to make the tests, like tests for item list. Add 8 | the methods to ItemQuery as needed for the tests. Add 9 | authenticationannotation. Also, templates should be finished. 10 | """ 11 | from flask import ( 12 | Blueprint, redirect, render_template, url_for 13 | ) 14 | 15 | BP = Blueprint("items", __name__, url_prefix="/items") 16 | 17 | 18 | class ItemListView(ListView): 19 | """ List the Items """ 20 | model = Item 21 | template_name = "items/list.html" 22 | 23 | 24 | ItemListView.register(BP, "/") 25 | 26 | 27 | class ItemCreateView(CreateView): 28 | """ Create a new Item """ 29 | model = Item 30 | template_name = "items/create.html" 31 | success_view_name = "item.list" 32 | form_class = ItemForm 33 | decorators = (auth.login_required,) 34 | 35 | 36 | ItemCreateView.register(BP, "/create", name="create") 37 | 38 | 39 | @BP.route("/edit", methods=("GET", "POST")) 40 | def edit(): 41 | """ Edit an item by id """ 42 | return render_template("items/edit.html") 43 | 44 | 45 | @BP.route("/delete", methods=["POST"]) 46 | def delete(): 47 | """ Delete an item by id """ 48 | return redirect(url_for("items.item_list_view")) 49 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/locations/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block header %} 3 | {% cache 600 %} 4 |

{% block title %}Locations management - Main{% endblock %}

5 | {% endcache %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% cache 600 %} 10 | {% for location in object_list %} 11 |
12 |
13 |
14 |

{{ location['name'] }}

15 |
16 | Edit 17 |
18 |

{{ location['code'] }}

19 |

{{ location['country'] }}

20 |

{{ location['region'] }}

21 |

{{ location['city'] }}

22 |

{{ location['address'] }}

23 |

{{ location['longitude'] }}

24 |

{{ location['latitude'] }}

25 |

{{ location['type'] }}

26 |

{{ location['status'] }}

27 |

{{ location['comment'] }}

28 |

{{ location['company'] }}

29 |

{{ location['floors'] }}

30 |
31 | {% if not loop.last %} 32 |
33 | {% endif %} 34 | {% endfor %} 35 | {% endcache %} 36 | {% endblock %} 37 | 38 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-25T202745.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 4d7589fae495 4 | Revises: 4e1a909ab513 5 | Create Date: 2019-01-24 10:28:04.239895 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4d7589fae495' 14 | down_revision = '4e1a909ab513' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('companies', sa.Column('address', sa.String(), nullable=True)) 22 | op.add_column('companies', sa.Column('code', sa.String(), nullable=False)) 23 | op.add_column('companies', sa.Column('created_on', sa.DateTime(), nullable=False)) 24 | op.add_column('companies', sa.Column('updated_on', sa.DateTime(), nullable=True)) 25 | op.alter_column('companies', 'name', 26 | existing_type=sa.VARCHAR(), 27 | nullable=False) 28 | op.create_unique_constraint(None, 'companies', ['code']) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_constraint('companies_code_key', 'companies', type_='unique') 35 | op.alter_column('companies', 'name', 36 | existing_type=sa.VARCHAR(), 37 | nullable=True) 38 | op.drop_column('companies', 'updated_on') 39 | op.drop_column('companies', 'created_on') 40 | op.drop_column('companies', 'code') 41 | op.drop_column('companies', 'address') 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /tests/integration/it_property_resource_test.py: -------------------------------------------------------------------------------- 1 | """ Tests for PropertyResource, resource for internationalization based on a 2 | property file 3 | 4 | """ 5 | import pytest 6 | 7 | from timeless.message.property_resource import PropertyResource 8 | 9 | 10 | class TestPropertyResource(): 11 | 12 | directory = "resources/messages/" 13 | 14 | @pytest.mark.skip(reason="PropertyResource not implemented yet") 15 | def test_get_found(self): 16 | """ Test if PropertyResource can return a value that exists""" 17 | assert ( 18 | PropertyResource( 19 | directory=self.directory, 20 | locale="en_US" 21 | ).get(self, "foundkey") == "thevalue" 22 | ) 23 | 24 | @pytest.mark.skip(reason="PropertyResource not implemented yet") 25 | def test_get_not_found(self): 26 | """ Test if PropertyResource returns exception when value does not 27 | exist 28 | """ 29 | with pytest.raises(Exception, "Value not found for key"): 30 | PropertyResource( 31 | directory=self.directory, 32 | locale="en_US" 33 | ).get(self, "notfoundkey",) 34 | 35 | @pytest.mark.skip(reason="PropertyResource not implemented yet") 36 | def test_get_found(self): 37 | """ Test if PropertyResource can return a value that exists from 38 | localised file 39 | """ 40 | assert ( 41 | PropertyResource( 42 | directory=self.directory, 43 | locale="pt_BR" 44 | ).get(self, "foundkey") == "ovalor" 45 | ) 46 | -------------------------------------------------------------------------------- /timeless/employees/models.py: -------------------------------------------------------------------------------- 1 | """File for models in employees module""" 2 | 3 | from timeless.db import DB 4 | from timeless.models import TimestampsMixin 5 | 6 | 7 | class Employee(TimestampsMixin, DB.Model): 8 | """Model for employee business entity.""" 9 | __tablename__ = "employees" 10 | 11 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 12 | first_name = DB.Column(DB.String, nullable=False) 13 | last_name = DB.Column(DB.String, nullable=False) 14 | username = DB.Column(DB.String(15), unique=True, nullable=False) 15 | phone_number = DB.Column(DB.String, nullable=False) 16 | birth_date = DB.Column(DB.Date(), nullable=False) 17 | registration_date = DB.Column(DB.DateTime(), nullable=False) 18 | account_status = DB.Column(DB.String, nullable=False) 19 | user_status = DB.Column(DB.String, nullable=False) 20 | email = DB.Column(DB.String(300), nullable=False) 21 | password = DB.Column(DB.String(300), nullable=False) 22 | pin_code = DB.Column(DB.Integer, unique=True, nullable=False) 23 | comment = DB.Column(DB.String) 24 | company_id = DB.Column(DB.Integer, DB.ForeignKey("companies.id")) 25 | role_id = DB.Column(DB.Integer, DB.ForeignKey("roles.id"), nullable=True) 26 | 27 | company = DB.relationship("Company", back_populates="employees") 28 | items = DB.relationship("Item", back_populates="empolyee") 29 | history = DB.relationship("ItemHistory", back_populates="employee") 30 | role = DB.relationship("Role", back_populates="employees") 31 | 32 | def __repr__(self): 33 | return "" % self.username 34 | -------------------------------------------------------------------------------- /timeless/roles/models.py: -------------------------------------------------------------------------------- 1 | """File for models in roles module""" 2 | 3 | from timeless import DB 4 | from enum import Enum 5 | from sqlalchemy_utils.types.choice import ChoiceType 6 | 7 | 8 | class RoleType(Enum): 9 | """ Types of roles """ 10 | Director = "Director" 11 | Manager = "Manager" 12 | Master = "Master" 13 | Intern = "Intern" 14 | Administrator = "Administrator" 15 | Owner = "Owner" 16 | 17 | 18 | class Role(DB.Model): 19 | """Settings model for Role.""" 20 | 21 | __tablename__ = "roles" 22 | 23 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 24 | """ 25 | @todo #397:30m After adding role type, name may make confusion. 26 | It should be deleted and all code using it should use type instead. 27 | Also, Alter the views of the roles to match the changes. 28 | """ 29 | name = DB.Column(DB.String, unique=True) 30 | works_on_shifts = DB.Column(DB.Boolean) 31 | company_id = DB.Column(DB.Integer, DB.ForeignKey("companies.id")) 32 | role_type = DB.Column(ChoiceType(RoleType, impl=DB.String()), unique=True) 33 | 34 | company = DB.relationship("Company", back_populates="roles") 35 | employees = DB.relationship("Employee", back_populates="role") 36 | 37 | def __repr__(self): 38 | return "" % self.role_type 39 | 40 | def is_master_or_intern(self): 41 | """ check if the type is master or intern """ 42 | return self.role_type in (RoleType.Master, RoleType.Intern,) 43 | 44 | def is_director(self): 45 | """ check if type is director """ 46 | return self.role_type == RoleType.Director 47 | -------------------------------------------------------------------------------- /timeless/access_control/authorization.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | 3 | from timeless.access_control import ( 4 | administrator_privileges, manager_privileges, other_privileges, 5 | owner_privileges, director_privileges, unknown_privileges) 6 | 7 | 8 | def is_allowed(method=None, resource=None, *args, **kwargs) -> bool: 9 | """ Check if user can access particular resource for a given method. 10 | Additional information needed for authorization can be passed through 11 | args or kwargs. This method is meant to work in conjunction with 12 | SecuredView.dispatch_request so that all available information about 13 | a user view can be accessible in the authorization process. 14 | @todo #358:30min Change checking of this statement below 15 | `name = g.user.role.name` to `name = g.user.role.role_type` and fix all 16 | tests, in all tests should be provided role_type instead of name of Role 17 | model. Example below: 18 | 19 | manager_role = factories.RoleFactory( 20 | name="manager", role_type=RoleType.Manager) 21 | me = factories.EmployeeFactory(company=my_company, role=manager_role) 22 | """ 23 | 24 | if not g.user or not g.user.role: 25 | name = "unknown" 26 | else: 27 | name = g.user.role.name 28 | 29 | return __roles[name].has_privilege( 30 | method=method, resource=resource, *args, **kwargs 31 | ) 32 | 33 | 34 | __roles = { 35 | "owner": owner_privileges, 36 | "manager": manager_privileges, 37 | "director": director_privileges, 38 | "administrator": administrator_privileges, 39 | "other": other_privileges, 40 | "unknown": unknown_privileges 41 | } 42 | -------------------------------------------------------------------------------- /tests/integration/it_reservations_test.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from flask import url_for 4 | 5 | import pytest 6 | from tests import factories 7 | from timeless.restaurants.models import Reservation 8 | 9 | 10 | def test_list(client): 11 | reservation = factories.ReservationFactory() 12 | response = client.get(url_for("reservations.list")) 13 | assert response.status_code == HTTPStatus.OK 14 | html = response.data.decode("utf-8") 15 | assert reservation.comment in html 16 | assert html.count(reservation.comment) == 1 17 | 18 | 19 | def test_create(client): 20 | reservation = factories.ReservationFactory.get_dict() 21 | url = url_for("reservations.create") 22 | response = client.post(url, data=reservation) 23 | assert response.status_code == HTTPStatus.FOUND 24 | assert Reservation.query.count() == 1 25 | 26 | 27 | def test_edit(client): 28 | reservation_original = factories.ReservationFactory() 29 | reservation_edited = factories.ReservationFactory.get_dict() 30 | url = url_for("reservations.edit", id=reservation_original.id) 31 | response = client.post(url, data=reservation_edited) 32 | assert response.status_code == HTTPStatus.FOUND 33 | reservation = Reservation.query.get(reservation_original.id) 34 | assert reservation.comment == reservation_edited["comment"] 35 | 36 | 37 | def test_delete(client): 38 | reservation = factories.ReservationFactory() 39 | url = url_for("reservations.delete", id=reservation.id) 40 | response = client.post(url) 41 | assert response.location.endswith(url_for("reservations.list")) 42 | assert Reservation.query.filter_by(id=reservation.id).count() == 0 43 | -------------------------------------------------------------------------------- /timeless/templates/employees/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% cache 600 %} 3 | {% block header %} 4 |

{% block title %}Employees management - Main{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for employee in object_list %} 9 |
10 |
11 |
12 |

{{ employee['username'] }}

13 |
14 | Edit 15 |
16 |

ID: {{ employee['id'] }}

17 |

Name: {{ employee['first_name'] }} {{ employee['last_name'] }}

18 |

Company name: {{ employee['company.name'] }}

19 |

Registration date: {{ employee['registration_date'] }}

20 |

Account status: {{ employee['account_status'] }}

21 |

User status: {{ employee['user_status'] }}

22 |

Birth date: {{ employee['birth_date'] }}

23 |

Email: {{ employee['email'] }}

24 |

Phone number: {{ employee['phone_number'] }}

25 |

User name: {{ employee['username'] }}

26 |

Password: {{ employee['password'] }}

27 |

Pin code: {{ employee['pin_code'] }}

28 |

Comment: {{ employee['comment'] }}

29 | 30 |
31 | {% if not loop.last %} 32 |
33 | {% endif %} 34 | {% endfor %} 35 | {% endblock %} 36 | {% endcache %} -------------------------------------------------------------------------------- /tests/test_sms.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from unittest import mock 4 | from timeless.sms import RedSMS 5 | 6 | 7 | @mock.patch("timeless.sms.datetime") 8 | @mock.patch("timeless.sms.requests") 9 | def test_red_sms_provider(requests_mock, timestamp_mock): 10 | login = "test_login" 11 | api_key = "api_key" 12 | timestamp = 1549208808.562239 13 | timestamp_mock.now.return_value = type( 14 | 'Mock date', (), {'timestamp': lambda: timestamp}) 15 | secret = hashlib.sha512(f"{timestamp}{api_key}".encode()).hexdigest() 16 | recipient = "recipient" 17 | route = "sms" 18 | sender = "sender" 19 | message = "message" 20 | 21 | sms = RedSMS( 22 | login=login, 23 | api_key=api_key, 24 | recipient=recipient, 25 | message=message, 26 | sender=sender, 27 | ) 28 | sms.send() 29 | 30 | requests_mock.post.assert_called_with( 31 | "https://cp.redsms.ru/api/message", 32 | data={ 33 | "login": login, 34 | "ts": timestamp, 35 | "secret": secret, 36 | "to": recipient, 37 | "text": message, 38 | "route": route, 39 | "from": sender, 40 | } 41 | ) 42 | 43 | 44 | @mock.patch("timeless.sms.requests") 45 | def test_retry_decorator_with_sms_sending(requests_mock): 46 | sms = RedSMS( 47 | login="test_login", 48 | api_key="api_key", 49 | recipient="recipient", 50 | message="message", 51 | sender="sender", 52 | ) 53 | requests_mock.post.return_value = type( 54 | 'Response', (), {'status_code': 500}) 55 | sms.send() 56 | assert requests_mock.post.call_count == 4 57 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-25T085210.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 4e1a909ab513 4 | Revises: be62103a5fd6 5 | Create Date: 2019-01-25 10:50:22.373521 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4e1a909ab513' 14 | down_revision = 'be62103a5fd6' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('locations', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(), nullable=False), 24 | sa.Column('code', sa.String(), nullable=False), 25 | sa.Column('company_id', sa.Integer(), nullable=True), 26 | sa.Column('country', sa.String(), nullable=False), 27 | sa.Column('region', sa.String(), nullable=False), 28 | sa.Column('city', sa.String(), nullable=False), 29 | sa.Column('address', sa.String(), nullable=False), 30 | sa.Column('longitude', sa.String(), nullable=False), 31 | sa.Column('latitude', sa.String(), nullable=False), 32 | sa.Column('type', sa.String(), nullable=False), 33 | sa.Column('status', sa.String(), nullable=False), 34 | sa.Column('comment', sa.String(), nullable=True), 35 | sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), 36 | sa.PrimaryKeyConstraint('id'), 37 | sa.UniqueConstraint('code'), 38 | sa.UniqueConstraint('name') 39 | ) 40 | # ### end Alembic commands ### 41 | 42 | 43 | def downgrade(): 44 | # ### commands auto generated by Alembic - please adjust! ### 45 | op.drop_table('locations') 46 | # ### end Alembic commands ### 47 | -------------------------------------------------------------------------------- /tests/integration/it_roles_test.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from flask import url_for 4 | 5 | from tests import factories 6 | from timeless.roles.models import Role, RoleType 7 | 8 | 9 | def test_list(client): 10 | assert client.get("/roles/").status_code == HTTPStatus.OK 11 | 12 | 13 | def test_create(client, db_session): 14 | response = client.post(url_for("role.create"), data={ 15 | "name": "John" 16 | }) 17 | assert response.location.endswith(url_for("role.list")) 18 | assert Role.query.filter_by(name="John").count() == 1 19 | 20 | 21 | def test_edit(client): 22 | role = factories.RoleFactory(role_type=RoleType.Manager) 23 | url = url_for("role.edit", id=role.id) 24 | 25 | client.post(url, data={"role_type": RoleType.Director.value}) 26 | assert Role.query.filter_by( 27 | id=role.id, role_type=RoleType.Director.value).count() == 0 28 | 29 | 30 | def test_delete_not_found(client): 31 | assert Role.query.filter_by(id=1).count() == 0 32 | assert client.post( 33 | url_for('role.delete', id=1)).status_code == HTTPStatus.NOT_FOUND 34 | 35 | 36 | def test_delete(client): 37 | name = 'role_for_deletion' 38 | assert Role.query.filter_by(name=name).count() == 0 39 | client.post(url_for("role.create"), data={ 40 | "name": name 41 | }) 42 | assert Role.query.filter_by(name=name).count() == 1 43 | created = Role.query.filter_by(name=name).first() 44 | 45 | result = client.post(url_for('role.delete', id=created.id)) 46 | assert result.status_code == HTTPStatus.FOUND 47 | assert Role.query.filter_by(id=created.id).count() == 0 48 | assert result.headers["Location"] == "http://localhost/roles/" 49 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | build: . 5 | command: bash -c 'python manage.py db upgrade && flask run --host 0.0.0.0' 6 | environment: 7 | FLASK_APP: main.py 8 | FLASK_ENV: development 9 | REDIS_HOST: redis://redis:6379 10 | SQLALCHEMY_DATABASE_URI: postgresql://timeless_user:timeless_pwd@db/timelessdb_dev 11 | ports: 12 | - 5000:5000 13 | volumes: 14 | - .:/usr/app 15 | depends_on: 16 | - db 17 | - redis 18 | db: 19 | image: postgres:10.6 20 | environment: 21 | POSTGRES_DB: timelessdb_dev 22 | POSTGRES_USER: timeless_user 23 | POSTGRES_PASSWORD: timeless_pwd 24 | volumes: 25 | - 'pgdata:/var/lib/postgresql/data' 26 | app_test: 27 | build: . 28 | command: pytest 29 | environment: 30 | FLASK_APP: main.py 31 | FLASK_ENV: testing 32 | REDIS_HOST: redis://redis:6379 33 | SQLALCHEMY_DATABASE_URI: postgresql://timeless_user:timeless_pwd@db_test/timelessdb_test 34 | ports: 35 | - 5000:5000 36 | volumes: 37 | - .:/usr/app 38 | depends_on: 39 | - db_test 40 | db_test: 41 | image: postgres:10.6 42 | environment: 43 | POSTGRES_DB: timelessdb_test 44 | POSTGRES_USER: timeless_user 45 | POSTGRES_PASSWORD: timeless_pwd 46 | redis: 47 | image: 'redis:3.2' 48 | ports: 49 | - '6379:6379' 50 | sync_worker: 51 | build: . 52 | command: bash -c 'celery -A timeless.celery worker' 53 | environment: 54 | FLASK_APP: main.py 55 | FLASK_ENV: development 56 | SQLALCHEMY_DATABASE_URI: postgresql://timeless_user:timeless_pwd@db/timelessdb_dev 57 | volumes: 58 | - .:/usr/app 59 | volumes: 60 | pgdata: null 61 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.0.6 2 | amqp==2.4.1 3 | astroid==2.0.4 4 | atomicwrites==1.2.1 5 | attrs==18.2.0 6 | bcrypt==3.1.6 7 | billiard==3.5.0.5 8 | celery==4.2.1 9 | certifi==2018.11.29 10 | cffi==1.11.5 11 | chardet==3.0.4 12 | Click==7.0 13 | codecov==2.0.15 14 | coverage==4.5.2 15 | decorator==4.3.2 16 | dodgy==0.1.9 17 | factory-boy==2.11.1 18 | Faker==1.0.2 19 | Flask==1.0.2 20 | Flask-Caching==1.4.0 21 | Flask-Migrate==2.3.1 22 | Flask-Script==2.0.6 23 | Flask-SQLAlchemy==2.3.2 24 | Flask-Uploads==0.2.1 25 | Flask-WTF==0.14.2 26 | future==0.17.1 27 | git-pylint-commit-hook==2.5.1 28 | idna==2.8 29 | infinity==1.4 30 | intervals==0.8.1 31 | isort==4.3.4 32 | itsdangerous==1.1.0 33 | Jinja2==2.10 34 | kombu==4.3.0 35 | lazy-object-proxy==1.3.1 36 | Mako==1.0.7 37 | MarkupSafe==1.1.0 38 | mccabe==0.6.1 39 | more-itertools==5.0.0 40 | passlib==1.7.1 41 | pep8-naming==0.4.1 42 | Pillow==5.4.1 43 | pluggy==0.8.1 44 | psycopg2==2.7.6.1 45 | psycopg2-binary==2.7.7 46 | py==1.7.0 47 | pycodestyle==2.4.0 48 | pycparser==2.19 49 | pydocstyle==3.0.0 50 | pyflakes==1.6.0 51 | pylint==2.1.1 52 | pylint-celery==0.3 53 | pylint-django==2.0.2 54 | pylint-plugin-utils==0.4 55 | pytest==4.1.1 56 | pytest-cov==2.6.1 57 | pytest-flask-sqlalchemy==1.0.0 58 | pytest-mock==1.10.0 59 | python-dateutil==2.7.5 60 | python-editor==1.0.3 61 | pytz==2018.9 62 | PyYAML==3.13 63 | redis==3.1.0 64 | requests==2.21.0 65 | requirements-detector==0.6 66 | setoptconf==0.2.0 67 | six==1.12.0 68 | snowballstemmer==1.2.1 69 | SQLAlchemy==1.2.16 70 | SQLAlchemy-Utils==0.33.11 71 | text-unidecode==1.2 72 | typed-ast==1.2.0 73 | urllib3==1.24.1 74 | validators==0.12.4 75 | vine==1.2.0 76 | Werkzeug==0.14.1 77 | wrapt==1.11.1 78 | WTForms==2.2.1 79 | WTForms-Alchemy==0.16.8 80 | WTForms-Components==0.10.3 81 | Flask-Mail==0.9.1 82 | blinker==1.4 -------------------------------------------------------------------------------- /timeless/sync/synced_table.py: -------------------------------------------------------------------------------- 1 | from timeless.restaurants.models import Table 2 | 3 | """ 4 | Table synced to database. 5 | @todo #342:30min Implement synchronization between Poster and Database for 6 | Tables. Data coming from Poster has priority upon data stored in our 7 | database. Synchronization must be done via celery job. See sync of 8 | Location implementation as reference. Tests for poster sync are already 9 | created in it_sync_tables_test.py 10 | """ 11 | 12 | 13 | class SyncedTable: 14 | 15 | def __init__(self, table, poster_sync, db_session): 16 | self.poster_sync = poster_sync 17 | self.db_session = db_session 18 | self.table = table 19 | 20 | def sync(self): 21 | poster_tables = self.poster_sync.tables() 22 | db_table = self.db_session.query(Table).get(self.table.id) 23 | for poster_table in poster_tables: 24 | if poster_table["id"] == db_table.id: 25 | db_table.name = poster_table["name"] 26 | db_table.floor_id = poster_table["floor_id"] 27 | db_table.x = poster_table["x"] 28 | db_table.y = poster_table["y"] 29 | db_table.width = poster_table["width"] 30 | db_table.height = poster_table["height"] 31 | db_table.status = poster_table["status"] 32 | db_table.max_capacity = poster_table["max_capacity"] 33 | db_table.multiple = poster_table["multiple"] 34 | db_table.playstation = poster_table["playstation"] 35 | db_table.shape_id = poster_table["shape_id"] 36 | db_table.min_capacity = poster_table["min_capacity"] 37 | db_table.deposit_hour = poster_table["deposit_hour"] 38 | self.db_session.commit() 39 | -------------------------------------------------------------------------------- /tests/integration/poster/poster_server_mock_test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from http.server import HTTPStatus 4 | 5 | from tests.integration.poster.poster_server_mock import PosterServerMock 6 | 7 | 8 | class TestPosterServerMock(): 9 | """ Tests for PosterServerMock. Tests the generic behaviors of 10 | PosterServerMock. 11 | """ 12 | 13 | def test_get_on_invalid_url(self): 14 | server = PosterServerMock 15 | server.start(server) 16 | server.PATTERN=r"anything" 17 | response = requests.get( 18 | url=f"http://localhost:{server.port}/otherthing" 19 | ) 20 | assert response.status_code == HTTPStatus.NOT_FOUND 21 | 22 | def test_get_on_valid_url(self): 23 | server = PosterServerMock 24 | server.start(server) 25 | server.PATTERN=r"anything" 26 | response = requests.get( 27 | url=f"http://localhost:{server.port}/anything" 28 | ) 29 | assert response.content.decode("utf-8") == '"get content"' 30 | assert response.status_code == HTTPStatus.OK 31 | 32 | def test_post_on_invalid_url(self): 33 | server = PosterServerMock 34 | server.start(server) 35 | server.PATTERN=r"anything" 36 | response = requests.post( 37 | url=f"http://localhost:{server.port}/strangerthing" 38 | ) 39 | assert response.status_code == HTTPStatus.NOT_FOUND 40 | 41 | def test_post_on_valid_url(self): 42 | server = PosterServerMock 43 | server.start(server) 44 | server.PATTERN=r"anything" 45 | response = requests.post( 46 | url=f"http://localhost:{server.port}/anything" 47 | ) 48 | assert response.content.decode("utf-8") == '"post content"' 49 | assert response.status_code == HTTPStatus.OK 50 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | """ Tests for auth/views.py methods """ 2 | import pytest 3 | 4 | from flask import g 5 | from http import HTTPStatus 6 | 7 | from tests import factories 8 | from timeless.auth import auth 9 | from timeless.employees.models import Employee 10 | 11 | 12 | def test_activate_unauthenticated_get(client): 13 | """ Tests if unauthenticated GET to activate returns 405""" 14 | response = client.get("/auth/activate") 15 | assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED 16 | 17 | 18 | def test_activate_unauthenticated(client): 19 | """ Tests if unauthenticated POST to activate returns correct screen""" 20 | response = client.post("/auth/activate") 21 | assert b"

You are not logged in

" in response.data 22 | assert response.status_code == HTTPStatus.OK 23 | 24 | 25 | def test_activate_authenticated(client): 26 | """ 27 | Tests if authenticated POST to activate returns correct screen 28 | """ 29 | employee = factories.EmployeeFactory( 30 | company=factories.CompanyFactory(), 31 | account_status=False 32 | ) 33 | with client.session_transaction() as session: 34 | session["logged_in"] = True 35 | session["user_id"] = employee.id 36 | response = client.post("/auth/activate") 37 | assert b"

Successfully activated your account.

" in response.data 38 | assert Employee.query.get(employee.id).account_status 39 | assert response.status_code == HTTPStatus.OK 40 | 41 | 42 | @pytest.mark.parametrize('email,masked_email', ( 43 | ('v@gmail.com', '*@gmail.com'), 44 | ('vr@gmail.com', 'v*@gmail.com'), 45 | ('john.black@gmail.com', 'john.*****@gmail.com'), 46 | )) 47 | def test_mask_email(email, masked_email): 48 | """ Tests if masking of email works correctly """ 49 | assert auth.mask_email(email) == masked_email 50 | -------------------------------------------------------------------------------- /timeless/templates/restaurants/locations/create_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Locations management - {% if action == 'create' %} Create {% else %} Edit {% endif %} {% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | Cancel 50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /tests/view/floors/test_floors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from http import HTTPStatus 4 | 5 | from tests import factories 6 | from timeless.restaurants.models import Floor 7 | 8 | 9 | """ Tests for floors. """ 10 | 11 | 12 | @pytest.mark.skip(reason="authentication not implemented for floors") 13 | def test_list(client): 14 | """ Test list is okay """ 15 | 16 | response = client.get("/floors/") 17 | factories.FloorFactory() 18 | factories.FloorFactory() 19 | factories.FloorFactory() 20 | factories.FloorFactory() 21 | assert response.status_code == HTTPStatus.OK 22 | assert b"

1st Floor

" in response.data 23 | assert b"

2nd Floor

" in response.data 24 | assert b"

3rd Floor

" in response.data 25 | assert b"

4th Floor

" in response.data 26 | 27 | 28 | def test_not_authenticated(client): 29 | """ Test if not authenticated user is redirected to login page """ 30 | 31 | response = client.get("/floors/") 32 | print(response.data) 33 | assert response.status_code == HTTPStatus.OK 34 | assert b"
  • Log In" in response.data 35 | assert b"Register" in response.data 36 | 37 | 38 | @pytest.mark.skip(reason="/floors/create not implemented") 39 | def test_create(client): 40 | """ Test create is okay """ 41 | assert client.get("/floors/create").status_code == HTTPStatus.OK 42 | 43 | 44 | @pytest.mark.skip(reason="/floors/edit not implemented") 45 | def test_edit(client): 46 | """ Test edit is okay """ 47 | assert client.get("/floors/edit").status_code == HTTPStatus.OK 48 | 49 | 50 | @pytest.mark.skip(reason="/floors/delete not implemented") 51 | def test_delete(client): 52 | """ Test delete is okay """ 53 | response = client.post("/floors/delete", data={"id": 1}) 54 | assert response.headers["Location"] == "http://localhost/restaurant/floors" 55 | -------------------------------------------------------------------------------- /migrations/versions/2019-01-29T092044.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 49f5103c70fe 4 | Revises: 9eee25222512 5 | Create Date: 2019-01-28 09:41:31.525707 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '49f5103c70fe' 14 | down_revision = '9eee25222512' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('employees', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('first_name', sa.String(), nullable=False), 24 | sa.Column('last_name', sa.String(), nullable=False), 25 | sa.Column('username', sa.String(length=15), nullable=False), 26 | sa.Column('phone_number', sa.String(), nullable=False), 27 | sa.Column('birth_date', sa.Date(), nullable=False), 28 | sa.Column('registration_date', sa.DateTime(), nullable=False), 29 | sa.Column('account_status', sa.String(), nullable=False), 30 | sa.Column('user_status', sa.String(), nullable=False), 31 | sa.Column('email', sa.String(length=300), nullable=False), 32 | sa.Column('password', sa.String(length=300), nullable=False), 33 | sa.Column('pin_code', sa.Integer(), nullable=False), 34 | sa.Column('comment', sa.String(), nullable=True), 35 | sa.Column('company_id', sa.Integer(), nullable=True), 36 | sa.Column('created_on', sa.DateTime(), nullable=False), 37 | sa.Column('updated_on', sa.DateTime(), nullable=True), 38 | sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ), 39 | sa.PrimaryKeyConstraint('id'), 40 | sa.UniqueConstraint('pin_code'), 41 | sa.UniqueConstraint('username') 42 | ) 43 | # ### end Alembic commands ### 44 | 45 | 46 | def downgrade(): 47 | # ### commands auto generated by Alembic - please adjust! ### 48 | op.drop_table('employees') 49 | # ### end Alembic commands ### 50 | -------------------------------------------------------------------------------- /timeless/employees/views.py: -------------------------------------------------------------------------------- 1 | """Employees views module. 2 | @todo #357:30min Continue implementing ListView to enable filtering 3 | for every column. For this, probably there will be a need to create a new 4 | generic view that all other List views will extend. This generic view should 5 | use GenericFilter implemented in #317. 6 | """ 7 | from flask import Blueprint 8 | 9 | from timeless.access_control.views import SecuredView 10 | from timeless.employees.forms import EmployeeForm 11 | from timeless import views 12 | from timeless.employees.models import Employee 13 | 14 | BP = Blueprint("employee", __name__, url_prefix="/employees") 15 | 16 | 17 | class Create(views.CreateView, SecuredView): 18 | """Create employee""" 19 | template_name = "employees/create_edit.html" 20 | form_class = EmployeeForm 21 | model = Employee 22 | success_view_name = "employee.list" 23 | resource = "employee" 24 | 25 | def get(self, **kwargs): 26 | context = self.get_context(action="create") 27 | return self.render_to_response(context) 28 | 29 | 30 | # class Edit(views.UpdateView): 31 | # """Update employee""" 32 | # template_name = "employees/create_edit.html" 33 | # form_class = EmployeeForm 34 | # model = Employee 35 | 36 | 37 | class Delete(views.DeleteView, SecuredView): 38 | """Delete employee""" 39 | model = Employee 40 | success_view_name = "employee.list" 41 | resource = "employee" 42 | 43 | 44 | class List(views.ListView, SecuredView): 45 | """List all employees""" 46 | model = Employee 47 | success_view_name = "employee.list" 48 | resource = "employee" 49 | """ 50 | @todo #348:15min Delete template_name from List after #312 will be pulled 51 | to the master branch. "template_name" is using now due to current 52 | implementation of views.DeleteView. 53 | """ 54 | template_name = "employees/list.html" 55 | 56 | 57 | List.register(BP, "/") 58 | Create.register(BP, "/create") 59 | # Edit.register(bp, "/edit/") 60 | Delete.register(BP, "//delete") 61 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ Configurations of the project. """ 2 | import os 3 | 4 | 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | 8 | class Config(object): 9 | """ All constant configurations for the project. """ 10 | DEBUG = False 11 | TESTING = False 12 | CSRF_ENABLED = True 13 | SECRET_KEY = "timele$$i$" 14 | SQLALCHEMY_TRACK_MODIFICATIONS = False 15 | # poster settings 16 | POSTER_APPLICATION_ID = "" 17 | POSTER_APPLICATION_SECRET = "" 18 | POSTER_REDIRECT_URI = "" 19 | POSTER_CODE = "" 20 | # redis and cache settings 21 | REDIS_HOST = os.environ.get("REDIS_HOST", "redis://localhost:6379") 22 | RESULT_BACKEND = REDIS_HOST 23 | BROKER_URL = REDIS_HOST 24 | CACHE_SETTINGS = { 25 | "CACHE_TYPE": "redis", 26 | "CACHE_REDIS_URL": REDIS_HOST 27 | } 28 | MAIL_DEFAULT_SENDER = "admin@timeless.com" 29 | 30 | 31 | class ProductionConfig(Config): 32 | """ Configurations for the production. """ 33 | DEBUG = False 34 | SQLALCHEMY_DATABASE_URI = os.environ.get( 35 | "SQLALCHEMY_DATABASE_URI", 36 | "postgresql://timeless_user:timeless_pwd@localhost/timelessdb") 37 | 38 | 39 | class StagingConfig(Config): 40 | """ Configurations for the staging. """ 41 | DEVELOPMENT = True 42 | DEBUG = False 43 | SQLALCHEMY_DATABASE_URI = os.environ.get( 44 | "SQLALCHEMY_DATABASE_URI", 45 | "postgresql://timeless_user:timeless_pwd@localhost/timelessdb_dev") 46 | 47 | 48 | class DevelopmentConfig(Config): 49 | """ Configurations for the Development. """ 50 | DEVELOPMENT = True 51 | DEBUG = True 52 | SQLALCHEMY_DATABASE_URI = os.environ.get( 53 | "SQLALCHEMY_DATABASE_URI", 54 | "postgresql://timeless_user:timeless_pwd@localhost/timelessdb_dev") 55 | 56 | 57 | class TestingConfig(Config): 58 | """ Configurations for the testing. """ 59 | TESTING = True 60 | WTF_CSRF_ENABLED = False 61 | CACHE_TYPE = None 62 | SQLALCHEMY_DATABASE_URI = os.environ.get( 63 | "SQLALCHEMY_DATABASE_URI", 64 | "postgresql://timeless_user:timeless_pwd@localhost/timelessdb_test") 65 | -------------------------------------------------------------------------------- /tests/integration/poster/poster_server_mock.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | import requests 4 | import json 5 | 6 | from http.server import BaseHTTPRequestHandler, HTTPServer 7 | from threading import Thread 8 | 9 | 10 | class PosterServerMock(BaseHTTPRequestHandler): 11 | """Generic Mock for Poster interactions""" 12 | 13 | PATTERN = None 14 | port = None 15 | 16 | def start(self): 17 | self.port = self.free_port(self) 18 | server = HTTPServer(("localhost", self.port), self) 19 | thread = Thread(target=server.serve_forever) 20 | thread.setDaemon(True) 21 | thread.start() 22 | 23 | def free_port(self): 24 | """Returns free port 25 | 26 | :return: Port 27 | """ 28 | sock = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) 29 | sock.bind(("localhost", 0)) 30 | address, port = sock.getsockname() 31 | sock.close() 32 | return port 33 | 34 | def do_GET(self): 35 | if re.search(self.PATTERN, self.path): 36 | self.send_response(requests.codes.ok) 37 | self.send_header("Content-Type", "application/json; charset=utf-8") 38 | self.end_headers() 39 | self.wfile.write(self.get_content().encode("utf-8")) 40 | else: 41 | self.send_response(requests.codes.not_found) 42 | self.send_header("Content-Type", "application/json; charset=utf-8") 43 | self.end_headers() 44 | 45 | def do_POST(self): 46 | if re.search(self.PATTERN, self.path): 47 | self.send_response(requests.codes.ok) 48 | self.send_header("Content-Type", "application/json; charset=utf-8") 49 | self.end_headers() 50 | self.wfile.write(self.post_content().encode("utf-8")) 51 | else: 52 | self.send_response(requests.codes.not_found) 53 | self.send_header("Content-Type", "application/json; charset=utf-8") 54 | self.end_headers() 55 | 56 | def get_content(self): 57 | return json.dumps("get content") 58 | 59 | def post_content(self): 60 | return json.dumps("post content") 61 | 62 | -------------------------------------------------------------------------------- /tests/integration/it_scheme_types_test.py: -------------------------------------------------------------------------------- 1 | """ Integration tests for Scheme Types """ 2 | from datetime import datetime 3 | from http import HTTPStatus 4 | 5 | from flask import url_for 6 | 7 | from tests import factories 8 | from timeless.schemetypes.models import SchemeType, SchemeCondition 9 | 10 | 11 | def test_list(client, db_session): 12 | """ Test getting list of Scheme types """ 13 | db_session.add(SchemeType(description="Test scheme type", 14 | default_value="1", value_type="Integer")) 15 | db_session.commit() 16 | types = client.get(url_for("scheme_type.list")) 17 | assert types.status_code == HTTPStatus.OK 18 | assert b"Test scheme type" in types.data 19 | 20 | 21 | def test_scheme_condition_list(client, db_session): 22 | """ Test getting list of Scheme conditions for Scheme type """ 23 | scheme_type = SchemeType(description="Scheme type", default_value="2", 24 | value_type="String") 25 | db_session.add(scheme_type) 26 | db_session.commit() 27 | db_session.add(SchemeCondition(scheme_type_id=scheme_type.id, 28 | value="Test condition", priority=1, 29 | start_time=datetime.utcnow(), 30 | end_time=datetime.utcnow())) 31 | db_session.commit() 32 | conditions = client.get(url_for("scheme_type.scheme_condition_list", 33 | scheme_type_id=scheme_type.id)) 34 | assert conditions.status_code == HTTPStatus.OK 35 | assert b"Test condition" in conditions.data 36 | 37 | 38 | def test_scheme_condition_create(client, db_session): 39 | """ Test that CreateView works correctly and creates an instance """ 40 | scheme_type = factories.SchemeTypeFactory() 41 | client.post( 42 | url_for( 43 | "scheme_type.scheme_condition_create", 44 | scheme_type_id=scheme_type.id), 45 | data={ 46 | "priority": 123, 47 | "value": "Value", 48 | "end_time": datetime.today().strftime('%Y-%m-%d %H:%M:%S'), 49 | }) 50 | assert SchemeCondition.query.filter_by(value="Value").count() == 1 51 | -------------------------------------------------------------------------------- /tests/integration/it_locations_test.py: -------------------------------------------------------------------------------- 1 | """Integration tests for Locations""" 2 | import pytest 3 | from http import HTTPStatus 4 | from flask import url_for 5 | 6 | from tests import factories 7 | from timeless.restaurants.models import Location 8 | 9 | 10 | def test_list(client): 11 | """ List all locations """ 12 | location = factories.LocationFactory() 13 | response = client.get(url_for("location.list")) 14 | assert response.status_code == HTTPStatus.OK 15 | html = response.data.decode("utf-8") 16 | assert html.count(location.name) == 1 17 | assert html.count(location.comment) == 1 18 | 19 | 20 | def test_create(client): 21 | location_data = { 22 | "name": "Name", 23 | "code": "Code", 24 | "country": "Country", 25 | "region": "Region", 26 | "city": "City", 27 | "address": "Address", 28 | "longitude": "0.0000000", 29 | "latitude": "0.0000000", 30 | "type": "Type", 31 | "status": "Active", 32 | "comment": "No comments", 33 | } 34 | response = client.post( 35 | url_for("location.create"), data=location_data, follow_redirects=True) 36 | assert Location.query.count() == 1 37 | assert location_data["name"].encode() in response.data 38 | 39 | 40 | def test_edit(client): 41 | location_original = factories.LocationFactory() 42 | location_edited = factories.LocationFactory.get_edit_fields_dict() 43 | edit_url = url_for("location.edit", id=location_original.id) 44 | response = client.post(edit_url, data=location_edited) 45 | assert response.status_code == HTTPStatus.FOUND 46 | assert response.location.endswith(url_for('location.list')) 47 | assert Location.query.count() == 1 48 | location = Location.query.get(location_original.id) 49 | for attr in location_edited.keys(): 50 | assert getattr(location, attr) == location_edited[attr] 51 | 52 | 53 | def test_delete(client): 54 | location = factories.LocationFactory() 55 | url = url_for("location.delete", id=location.id) 56 | response = client.post(url) 57 | assert response.status_code == HTTPStatus.FOUND 58 | assert response.location.endswith(url_for('location.list')) 59 | assert Location.query.count() == 0 60 | -------------------------------------------------------------------------------- /timeless/customers/models.py: -------------------------------------------------------------------------------- 1 | """File for models in customer module""" 2 | from datetime import datetime 3 | from timeless import DB 4 | from timeless.poster.models import PosterSyncMixin 5 | from timeless.models import validate_required 6 | 7 | 8 | class Customer(PosterSyncMixin, DB.Model): 9 | """Model for customer business entity. 10 | 11 | """ 12 | __tablename__ = "customers" 13 | 14 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 15 | first_name = DB.Column(DB.String, nullable=False) 16 | last_name = DB.Column(DB.String, nullable=False) 17 | phone_number = DB.Column(DB.String, nullable=False) 18 | created_on = DB.Column(DB.DateTime, default=datetime.utcnow, nullable=False) 19 | updated_on = DB.Column(DB.DateTime, onupdate=datetime.utcnow) 20 | 21 | def __repr__(self): 22 | return "" % (self.first_name, self.last_name) 23 | 24 | @classmethod 25 | def merge_with_poster(cls, customer: "Customer", poster_customer: dict): 26 | """ 27 | Method should return Customer object with merged data from table entity 28 | and poster customer dict 29 | """ 30 | return Customer( 31 | id=customer.id, 32 | first_name=poster_customer["firstname"], 33 | last_name=poster_customer["lastname"], 34 | phone_number=poster_customer["phone_number"], 35 | created_on=poster_customer["date_activate"], 36 | updated_on=datetime.utcnow(), 37 | poster_id=customer.poster_id, 38 | synchronized_on=datetime.utcnow() 39 | ) 40 | 41 | @classmethod 42 | def create_by_poster(cls, poster_customer: dict): 43 | """ 44 | Method should return Customer object with given data from 45 | poster_customer dict 46 | """ 47 | return Customer( 48 | first_name=poster_customer["firstname"], 49 | last_name=poster_customer["lastname"], 50 | phone_number=poster_customer["phone_number"], 51 | created_on=poster_customer["date_activate"], 52 | updated_on=datetime.utcnow(), 53 | poster_id=poster_customer["client_id"], 54 | synchronized_on=datetime.utcnow() 55 | ) 56 | -------------------------------------------------------------------------------- /timeless/auth/auth.py: -------------------------------------------------------------------------------- 1 | """ Authentication methods are implemented here. """ 2 | import random 3 | import string 4 | 5 | 6 | from flask import session 7 | from passlib.handlers.bcrypt import bcrypt_sha256 8 | 9 | from timeless import DB 10 | from timeless.employees.models import Employee 11 | from timeless.mail import MAIL 12 | from flask_mail import Message 13 | 14 | 15 | PASS_LENGTH = 8 16 | 17 | 18 | def mask_email(email: str) -> str: 19 | """ It masks provided email with asterisks """ 20 | username, host = email.split("@") 21 | if len(username) == 1: 22 | local_part = "*" 23 | else: 24 | half_len = len(username) // 2 25 | local_part = username[:half_len] + half_len * "*" 26 | return f"{local_part}@{host}" 27 | 28 | 29 | def login(username="", password=""): 30 | """Login user 31 | 32 | """ 33 | user = Employee.query.filter_by(username=username).first() 34 | error = None 35 | if user is None or not verify(password, user.password): 36 | error = "login.failed" 37 | if error is None: 38 | session.clear() 39 | session["user_id"] = user.id 40 | return error 41 | 42 | 43 | def forgot_password(email=""): 44 | """ Handle the forgot password routine. """ 45 | user = Employee.query.filter_by(email=email).first() 46 | if not user: 47 | return "failed" 48 | 49 | password = "".join(random.choice( 50 | string.ascii_uppercase + string.digits 51 | ) for _ in range(PASS_LENGTH)) 52 | user.password = bcrypt_sha256.hash(password) 53 | DB.session.commit() 54 | MAIL.send( 55 | Message( 56 | f"Hello! your new password is {password}, please change it!", 57 | recipients=[email] 58 | ) 59 | ) 60 | session.clear() 61 | 62 | 63 | def verify(password, hash): 64 | """ 65 | Verifies password against hash 66 | :param password: Password to check 67 | :param hash: Hash to check password against 68 | :return: True in case of successful password verification 69 | """ 70 | return bcrypt_sha256.verify(password, hash) 71 | 72 | 73 | def hash(password): 74 | """ 75 | Hash password 76 | :param password: Password to hash 77 | :return: Hashed password 78 | """ 79 | return bcrypt_sha256.hash(password) 80 | -------------------------------------------------------------------------------- /timeless/auth/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auth views module. 3 | """ 4 | from functools import wraps 5 | from flask import ( 6 | Blueprint, flash, g, redirect, render_template, request, session, url_for 7 | ) 8 | from timeless.auth import auth 9 | from timeless.employees.models import Employee 10 | from timeless import DB 11 | 12 | BP = Blueprint("auth", __name__, url_prefix="/auth") 13 | 14 | 15 | @BP.before_app_request 16 | def load_logged_in_user(): 17 | user_id = session.get("user_id") 18 | if not user_id: 19 | g.user = None 20 | else: 21 | g.user = Employee.query.get(user_id) 22 | 23 | 24 | def login_required(view): 25 | @wraps(view) 26 | def wrapped_view(**kwargs): 27 | if not g.user: 28 | return redirect(url_for("auth.login")) 29 | return view(**kwargs) 30 | 31 | return wrapped_view 32 | 33 | 34 | @BP.route("/login", methods=("GET", "POST")) 35 | def login(): 36 | if request.method == "POST": 37 | error = auth.login( 38 | username=request.form["username"], 39 | password=request.form["password"]) 40 | if error is not None: 41 | return redirect(url_for("auth.login")) 42 | 43 | return render_template("auth/login.html") 44 | 45 | 46 | @BP.route("/logout") 47 | def logout(): 48 | session.clear() 49 | return redirect(url_for("main")) 50 | 51 | 52 | @BP.route("/forgotpassword", methods=("GET", "POST")) 53 | def forgot_password(): 54 | if request.method == "POST": 55 | email = request.form["email"] 56 | error = auth.forgot_password(email=email) 57 | 58 | if error: 59 | template = render_template( 60 | "auth/forgot_password.html", error=error) 61 | else: 62 | template = render_template( 63 | "auth/forgot_password_post.html", email=auth.mask_email(email)) 64 | 65 | return template 66 | 67 | return render_template("auth/forgot_password.html") 68 | 69 | 70 | @BP.route("/activate", methods=["POST"]) 71 | def activate(): 72 | """ 73 | Activate the user's account by setting account status to true. 74 | """ 75 | if not session.get("user_id"): 76 | return render_template("auth/activate.html") 77 | 78 | g.user.account_status = True 79 | DB.session.commit() 80 | return render_template( 81 | "auth/activate.html", 82 | message="Successfully activated your account." 83 | ) 84 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | 6 | from tests import factories 7 | from timeless import create_app 8 | from timeless.cache import CACHE 9 | from timeless.db import DB 10 | 11 | 12 | @pytest.fixture(scope='session') 13 | def app(): 14 | db_fd, db_path = tempfile.mkstemp() 15 | app = create_app("config.TestingConfig") 16 | app_context = app.test_request_context() 17 | app_context.push() 18 | DB.create_all() 19 | yield app 20 | DB.session.remove() 21 | DB.drop_all() 22 | os.close(db_fd) 23 | os.unlink(db_path) 24 | 25 | 26 | @pytest.fixture(autouse=True) 27 | def corrected_factories(db_session): 28 | """ It patches factories with correct db session """ 29 | for factory in ( 30 | factories.TableShapeFactory, 31 | factories.EmployeeFactory, 32 | factories.CompanyFactory, 33 | factories.RoleFactory, 34 | factories.ReservationFactory, 35 | factories.SchemeTypeFactory, 36 | factories.LocationFactory, 37 | ): 38 | factory._meta.sqlalchemy_session = db_session 39 | 40 | 41 | @pytest.fixture 42 | def client(app): 43 | return app.test_client() 44 | 45 | 46 | @pytest.fixture 47 | def runner(app): 48 | return app.test_cli_runner() 49 | 50 | 51 | class AuthActions(): 52 | def __init__(self, client): 53 | self._client = client 54 | 55 | def login(self, username="test", password="test"): 56 | employee = factories.EmployeeFactory( 57 | company=factories.CompanyFactory(), 58 | ) 59 | with self._client.session_transaction() as session: 60 | session["user_id"] = employee.id 61 | session["logged_in"] = True 62 | return True 63 | 64 | def logout(self): 65 | with self._client.session_transaction() as session: 66 | session["user_id"] = None 67 | session["logged_in"] = False 68 | return self._client.get("/auth/logout") 69 | 70 | 71 | @pytest.fixture 72 | def auth(client): 73 | return AuthActions(client) 74 | 75 | 76 | @pytest.fixture(scope='session') 77 | def _db(app): 78 | """ 79 | Provide the transactional fixtures with access to the database via a 80 | Flask-SQLAlchemy database connection. 81 | """ 82 | return DB 83 | 84 | 85 | @pytest.fixture(autouse=True) 86 | def clear_cache(app): 87 | """ Clean the cache for every test """ 88 | with app.app_context(): 89 | CACHE.clear() 90 | -------------------------------------------------------------------------------- /timeless/roles/views.py: -------------------------------------------------------------------------------- 1 | """roles views module. 2 | @todo #255:30min Continue implementing edit() method, 3 | using SQLAlchemy and Location model. In the index page it 4 | should be possible to sort and filter for every column. Location management 5 | page should be accessed by the Location page. Update html templates when 6 | methods are implemented. Create more tests for edit() route. 7 | Remember not to use DB layer directly. Please refer to 8 | timeless/companies/views.py as an example on how routes 9 | should be implemented. 10 | """ 11 | from http import HTTPStatus 12 | 13 | from flask import ( 14 | Blueprint, flash, redirect, render_template, request, url_for, 15 | abort) 16 | 17 | from timeless import views 18 | from timeless.roles.forms import RoleForm 19 | from timeless.roles.models import Role 20 | 21 | 22 | BP = Blueprint("role", __name__, url_prefix="/roles") 23 | 24 | 25 | class List(views.ListView): 26 | """ List the tables """ 27 | model = Role 28 | template_name = "roles/list.html" 29 | 30 | 31 | List.register(BP, "/") 32 | 33 | 34 | @BP.route("/create", methods=("GET", "POST")) 35 | def create(): 36 | """ Create new table shape""" 37 | form = RoleForm(request.form) 38 | if request.method == "POST" and form.validate(): 39 | form.save() 40 | return redirect(url_for("role.list")) 41 | return render_template( 42 | "roles/create_edit.html", form=form) 43 | 44 | 45 | @BP.route("/edit/", methods=("GET", "POST")) 46 | def edit(id): 47 | """ 48 | Role edit route 49 | :param id: Role id 50 | :return: Current role edit view 51 | """ 52 | if request.method == "POST": 53 | table = Role.query.get(id) 54 | if not table: 55 | return abort(HTTPStatus.NOT_FOUND) 56 | flash("Edit not yet implemented") 57 | action = "edit" 58 | companies = [ 59 | {"id": 1, "name": "Foo Inc.", "selected": False}, 60 | {"id": 3, "name": "Foomatic Co.", "selected": True}, 61 | ] 62 | return render_template( 63 | "roles/create_edit.html", action=action, 64 | companies=companies 65 | ) 66 | 67 | 68 | @BP.route("/delete/", methods=["POST"]) 69 | def delete(id): 70 | """ 71 | Role delete route 72 | :param id: Role id 73 | :return: List roles view 74 | """ 75 | roles = Role.query.get(id) 76 | if not roles: 77 | return abort(HTTPStatus.NOT_FOUND) 78 | Role.query.filter_by(id=id).delete() 79 | return redirect(url_for("role.list")) 80 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /timeless/sms.py: -------------------------------------------------------------------------------- 1 | """Module for sms sending""" 2 | import functools 3 | import hashlib 4 | import time 5 | from datetime import datetime 6 | from http import HTTPStatus 7 | 8 | import requests 9 | 10 | 11 | class SMS: 12 | """Interface for SMS""" 13 | 14 | def send(self): 15 | """Abstract method send""" 16 | raise NotImplementedError 17 | 18 | 19 | class RetrySendSMS: 20 | """Decorator class for resending requests""" 21 | def __init__(self, retry_count=3, timeout=1): 22 | self.retry_count = retry_count 23 | self.timeout = timeout 24 | self.counter = 0 25 | self.unavailable_status_codes = [ 26 | HTTPStatus.NOT_FOUND, 27 | HTTPStatus.INTERNAL_SERVER_ERROR 28 | ] 29 | 30 | def __call__(self, send): 31 | @functools.wraps(send) 32 | def decorated(*args, **kwargs): 33 | response = send(*args, **kwargs) 34 | response_status_code = response.status_code 35 | while (response_status_code in self.unavailable_status_codes and 36 | self.counter < self.retry_count): 37 | time.sleep(self.timeout) 38 | response = send(*args, **kwargs) 39 | response_status_code = response.status_code 40 | self.counter += 1 41 | return response 42 | return decorated 43 | 44 | 45 | class RedSMS(SMS): 46 | """Class for sms sending 47 | API docs - https://redsms.ru/api-doc/ 48 | """ 49 | api_url = "https://cp.redsms.ru/api/message" 50 | 51 | def __init__( 52 | self, login, api_key, recipient, message, sender): 53 | self.login = login 54 | self.api_key = api_key 55 | self.recipient = recipient 56 | self.message = message 57 | self.sender = sender 58 | 59 | def make_base_payload(self): 60 | """Make base payload with basic data for provider 61 | """ 62 | timestamp = datetime.now().timestamp() 63 | return { 64 | "login": self.login, 65 | "ts": timestamp, 66 | "secret": hashlib.sha512( 67 | f"{timestamp}{self.api_key}".encode() 68 | ).hexdigest(), 69 | "route": "sms", 70 | } 71 | 72 | @RetrySendSMS() 73 | def send(self): 74 | """ 75 | Sends sms via provider 76 | """ 77 | base_payload = self.make_base_payload() 78 | return requests.post(self.api_url, data={ 79 | "to": self.recipient, 80 | "text": self.message, 81 | "from": self.sender, 82 | **base_payload 83 | }) 84 | -------------------------------------------------------------------------------- /tests/integration/it_tables_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | @todo #234:30min Timeless ModelForm is not working properly, the 3 | Table form in test_create cannot be saved. The problem is that it 4 | does not pass form data parameters to model constructor properly. 5 | Parameters are passed through *args but ModelForm works uses **kwargs 6 | to instantiate model. Once this is fixed, remove skip annotations 7 | from tests. 8 | """ 9 | from http import HTTPStatus 10 | 11 | import pytest 12 | from flask import url_for 13 | 14 | from timeless.restaurants.models import Table 15 | 16 | 17 | def test_list(client): 18 | assert client.get("/tables/").status_code == HTTPStatus.OK 19 | 20 | 21 | @pytest.mark.skip() 22 | def test_create(client): 23 | name = "test table" 24 | response = client.post(url_for("table.create"), data={ 25 | "name": name, 26 | "x": 1, 27 | "y": 2, 28 | "width": 100, 29 | "height": 75, 30 | "status": 1, 31 | "max_capacity": 4, 32 | "multiple": False, 33 | "playstation": True 34 | }) 35 | assert response.location.endswith(url_for('table.list_tables')) 36 | assert Table.query.count() == 1 37 | assert Table.query.get(1).name == name 38 | 39 | 40 | @pytest.mark.skip() 41 | def test_edit(client, db_session): 42 | table = Table( 43 | name="first name", 44 | x=1, 45 | y=2, 46 | width=100, 47 | height=75, 48 | status=1, 49 | max_capacity=4, 50 | multiple=False, 51 | playstation=True 52 | ) 53 | db_session.add(table) 54 | db_session.commit() 55 | name = "updated name" 56 | response = client.post(url_for("table.edit", id=1), data={ 57 | "name": name, 58 | "x": 1, 59 | "y": 2, 60 | "width": 100, 61 | "height": 75, 62 | "status": 1, 63 | "max_capacity": 4, 64 | "multiple": False, 65 | "playstation": True 66 | }) 67 | assert response.location.endswith(url_for('table.list_tables')) 68 | assert Table.query.count() == 1 69 | assert Table.query.get(1).name == name 70 | 71 | 72 | @pytest.mark.skip(reason="Must be replaced with /table/delete") 73 | def test_delete(client, db_session): 74 | table = Table( 75 | name="test name", 76 | x=1, 77 | y=2, 78 | width=100, 79 | height=75, 80 | status=1, 81 | max_capacity=4, 82 | multiple=False, 83 | playstation=True 84 | ) 85 | db_session.add(table) 86 | db_session.commit() 87 | response = client.post(url_for("table.delete", id=table.id)) 88 | assert response.location.endswith(url_for('table.list_tables')) 89 | assert Table.query.count() == 0 90 | -------------------------------------------------------------------------------- /credentials/staging.id_rsa.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP MESSAGE----- 2 | 3 | hQEMA5qETcGag5w6AQf/W75DYnE55dAlVTRDeBwEJ3vgdY8p+0Q4eBxdHHr55G7h 4 | cKMIfKi4fWOzLoilXcS/wDM6dWWgfCVG9FrzNylIOG/PMPD7mfag5Vwsrn0oQA/0 5 | 4hgPlLXTxl5hmXulAuojL3j01tyr02XdX8ZfuKBDSXHseWENh/3Q2D3kzBplqeCR 6 | EGAyeR3JuDu595TA2KchAaaAfvaMbwvec763XtV135/KeNQWdIz0JnUUfVui+vl1 7 | F+XEJsT1WLOBe9TuaOXHd6vtjw+H/1LYTY+LfAe2vT/1MWUkZbGvaXdwckZmi4wa 8 | BEf72i0E57qFCKQAasdo0jTz2G5exPUq0hdW+XL5+tLqAXbVv6yWpYAXpR+XUeuF 9 | y1tt68D1AdEkVQh41qqQosnnll6Gj+I8QtvDZyt5taYwrdyaWAYa3NqsvQngdZ8h 10 | 9+4uHD9OW/aZDnfHI39qWeEQSCd9oBqhQ+AgYoABU5o2rDHIgFmc2YQQmZe7m4C1 11 | 3/ll3RpoGmcAIKu4wflHrH9Qt2tbM2We4Opm8nd23LVhmlbgg+4nGWj5u9xk/3lV 12 | v8EwShYZ2jdbehkf8IY6/9iELDULsrsBujF93F/vgNCncG9fU+HYO3VXOF0a4QS2 13 | 0dywTVTZAb6o9PtUrABBYOkP67XwFbgk+iA88lcwPZ6T8e4Aw9VEHbkeUZr/n7cz 14 | SWfB+zvscIQBEPpgA/cS2p/VW0YgVSAPuWb7h+DYukixJqOqRFGWqAHnBqiVyhpC 15 | zeaJlPXXb3OI9IsvnSN8lO2rj/h4C4OvcZqvCmvqVoTcWmjvov7m8Vv5vCb0F5DY 16 | iy5LLplsTJJjes5Cn0nTHTBWnWj2l8uD6T7EBUbUU0+wAmQ/fOziPIviHZDRsAV8 17 | d2dJLsQ9zDWzSTnGYw+vtfsH9kdkFqKC0xJxVqkXyR5NiNx+ebHOgTlxvNG24sKZ 18 | 5rg05H7J5v801sXUB9JjCL5ufeYcjFK0nn2gekFcHPOE93WYXiRHKDtyALWS5kER 19 | oW4PZjwIaVCEmMkioko17Gez7JJntWxCEniuoJoL40VUwvKS4oWSnFZ///DoPPV5 20 | EEeoQGR+R6D80sBuE3GTMjTwHSbECWjKs9PDukAjPye3FqpXptNHN0jW+yVH91kr 21 | xAZuZY003t08wC/KogoOjbH3j/vi3b4WE55oPxehjUEceMjrKGwtMbrVBrce401v 22 | LOkDngi26w3pCoU9pA3dx2stvLF/gY+MTehpD0UiUq6AaRjbXvTP9VPoxAdF2D1h 23 | gFcUg9oBFEH/OD8fo8+i5+IEsqmuqq9naXizf2eOuCrGcQsOYuj/95dgX15pHrLu 24 | su/yH5QZcG99H18kWB397TzrV38lll0ZrQ0aAjta9LtF9C3az16C1ruuWVKQ5Veb 25 | /Bi0wc/kVv4qViH54j2e9mYSGR4B2ck2Kxy8DC0lxw2d4W0eavFtxRHuV54zo1p0 26 | ALZWccQQh9vfV6OZCePJReoMjKxp7AH5pjhuwVRqB8kYTev7bUiTDHv/umvdgTXr 27 | re8uEH/E0ESQoqmPgo7GOB6vOGVbEbQPOIPJ/zY9XwlyxlGZawidlc+AKTp9DQri 28 | O1HZy4Enwi/gRMRK8rhiJmv05PTjIC5S3FkzpZSNZpiZC17Eiy3Y4JwqVvQDoZPV 29 | 8oAy6hoKgGm/qEfJ75qsMDGoZexCWgKZK47ODnkGKNWsd99HCSa0oUL07QaLH++6 30 | pul2VcUBX//mHIXQtZJXXfcIDySgmlEym/l/MyghT7fIFOG66AwhTrUujXQt/xcq 31 | u28Oeh9GpAvKn2+cAKYdFk/jcrwAo5XQRKMYp8pw4hx7ZS9t5BZfvtNL2qFWl6ra 32 | 7SZY3mclbi6wz8bVNMfGIcpqbzwShQOhTQdAGn/3/Cw4VquwJ18ObgU6ffV8FcSO 33 | 93e6+4MAT44xYN9KKbwmqqZ0SQmeCUYR3PTLo9h3987mm/+qm1CKTovGFRrkHyIO 34 | 0wqsDab6SQx8QNN8ZOUBx+i97frT3fM/FT5Y4IFvMU40j16/XQu7Qy4RIGurYA65 35 | xtYjrkwo2f697/MTqly2Gie0urja2g6J4KQU5UNWUmVPg/ImKbfASPfP9sv55V1e 36 | 04CRtS4/Dy5wMbulpax7mCS+mBE+I9LqbDMlc02eDEyjLhifCTS3jqN3w2jAMN9B 37 | ez4RxvT0j+5UrG7N0+HuYPYkUS8WyxS9S8yfi3vW1HD9O7pcC5xr0wkvDFYFuJc4 38 | 2KZAPBgn7P/Ck01w3QB2iEcIRYyjinI288t3nJj14ECx3cz3xNcOLydWBWuuLpJE 39 | AEkhybzM8earabyoDSZCdebIewQmzpq/hirAhjeuC7qDNbWpWe+5j1mfKxuqg2aw 40 | Dz5nfgfzWpn7CO+FX1RWgIkhMuMkfQoWFY4NyyyqHX569DjshXFb4RC9/kOHfmMn 41 | AGtwataIYj2hzFH3BBZkMwyg631tgdz0Mzwu5JmQ8h8hUoPUX9K1mfsJTw== 42 | =Pauf 43 | -----END PGP MESSAGE----- 44 | -------------------------------------------------------------------------------- /tests/view/tables/test_tables.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from http import HTTPStatus 4 | from flask import g, url_for 5 | 6 | from timeless.roles.models import RoleType 7 | from tests import factories 8 | 9 | 10 | """ Tests for the Table views. 11 | @todo #430:30min Fix TableListView problem of returning empty list. 12 | TableListView is returning an empty list when it should return valid values. 13 | Test below is valid, it calls TableListViews with an authenticated user but it 14 | isn't retrieving the tables. Fix the TableListView class and then uncomment the 15 | test below. 16 | """ 17 | 18 | 19 | @pytest.mark.skip(reason="TableListView is not behaving correctly") 20 | def test_list(client): 21 | """ Test list is okay """ 22 | role = factories.RoleFactory(name=RoleType.Intern.name) 23 | company = factories.CompanyFactory() 24 | employee = factories.EmployeeFactory( 25 | company=company, role_id=role.id 26 | ) 27 | location = factories.LocationFactory(company=company) 28 | floor = factories.FloorFactory(location=location) 29 | with client.session_transaction() as session: 30 | session["user_id"] = employee.id 31 | g.user = employee 32 | factories.TableFactory(floor_id=floor.id, name="Table 01") 33 | factories.TableFactory(floor_id=floor.id, name="Table 02") 34 | factories.TableFactory(floor_id=floor.id, name="Table 03") 35 | response = client.get(url_for("/tables/")) 36 | assert response.status_code == HTTPStatus.OK 37 | assert b"

    Table 01

    " in response.data 38 | assert b"

    Table 02

    " in response.data 39 | assert b"

    Table 03

    " in response.data 40 | assert b"

    Table " in response.data.count == 3 41 | 42 | 43 | def test_required_authentication(client): 44 | """ Test list is okay """ 45 | response = client.get("/tables/") 46 | assert response.status_code == HTTPStatus.OK 47 | assert b"
  • Register" in response.data 48 | assert b"
  • Log In" in response.data 49 | 50 | 51 | @pytest.mark.skip(reason="/tables/create not implemented") 52 | def test_create(client): 53 | """ Test create is okay """ 54 | assert client.get("/tables/create").status_code == HTTPStatus.OK 55 | 56 | 57 | @pytest.mark.skip(reason="/tables/edit not implemented") 58 | def test_edit(client): 59 | """ Test edit is okay """ 60 | assert client.get("/tables/edit").status_code == HTTPStatus.OK 61 | 62 | 63 | @pytest.mark.skip(reason="/tables/delete not implemented") 64 | def test_delete(client): 65 | """ Test delete is okay """ 66 | response = client.post("/tables/delete", data={"id": 1}) 67 | assert response.headers["Location"] == "http://localhost//tables" 68 | -------------------------------------------------------------------------------- /tests/integration/sync/it_sync_location_test.py: -------------------------------------------------------------------------------- 1 | import unittest.mock 2 | 3 | import pytest 4 | 5 | from timeless.companies.models import Company 6 | from timeless.poster.api import Poster, Authenticated 7 | from timeless.poster.tasks import sync_locations 8 | from timeless.restaurants.models import Location 9 | 10 | 11 | """Integration tests for Location Sync with database 12 | 13 | @todo #232:30min Location poster mock refactor. To simplify future 14 | poster mock creation PosterServerMock were refactored as a 15 | generic server for postermocks (PosterServerMock in 16 | tests/integration/poster/poster_integration_mock.py). Implement a mock 17 | location server based on PosterServerMock implementation. 18 | @todo #232:30min Refactor it_sync_location_test.py to use factory-created 19 | mocks. Create Location in factories so we can use it instead fixed 20 | mocks. The refactor the tests below so they pass again 21 | """ 22 | 23 | 24 | @pytest.mark.skip( 25 | "Skipped until ticket #453 is fixed") 26 | @unittest.mock.patch.object(Authenticated, 'auth') 27 | @unittest.mock.patch.object(Poster, 'locations') 28 | def test_sync_location(locations_mock, auth_mock, db_session): 29 | company = Company( 30 | id=50, 31 | name="Company of Heroes", 32 | code="Cpny", 33 | address="Somewhere in the bermuda triangle" 34 | ) 35 | db_session.add(company) 36 | db_session.commit() 37 | 38 | auth_mock.return_value = 'token' 39 | locations_mock.return_value = { 40 | "response": [{ 41 | "id": 100, 42 | "name": "Coco Bongo", 43 | "code": "C", 44 | "company_id": company.id, 45 | "country": "United States", 46 | "region": "East Coast", 47 | "city": "Edge City", 48 | "address": "Blvd. Kukulcan Km 9.5 #30, Plaza Forum", 49 | "longitude": 21.1326063, 50 | "latitude": -86.7473191, 51 | "type": "L", 52 | "status": "open", 53 | "comment": "Nightclub from a famous movie" 54 | }] 55 | } 56 | 57 | sync_locations() 58 | 59 | row = Location.query.filter_by(id=100).one() 60 | assert row.id == 100 61 | assert row.name == "Coco Bongo" 62 | assert row.code == "C" 63 | assert row.company_id == 50 64 | assert row.country == "United States" 65 | assert row.region == "East Coast" 66 | assert row.city == "Edge City" 67 | assert row.address == "Blvd. Kukulcan Km 9.5 #30, Plaza Forum" 68 | assert row.longitude == 21.1326063 69 | assert row.latitude == -86.7473191 70 | assert row.type == "L" 71 | assert row.status == "open" 72 | assert row.comment == "Nightclub from a famous movie" 73 | -------------------------------------------------------------------------------- /timeless/restaurants/table_shapes/views.py: -------------------------------------------------------------------------------- 1 | """TableShape views module.""" 2 | import pytest 3 | 4 | from http import HTTPStatus 5 | 6 | from flask import ( 7 | Blueprint, redirect, abort, render_template, request, url_for 8 | ) 9 | 10 | from timeless import views 11 | from timeless.db import DB 12 | from timeless.restaurants import models 13 | from timeless.restaurants.table_shapes import forms 14 | from timeless.templates.views import order_by, filter_by 15 | from timeless.uploads import IMAGES 16 | 17 | 18 | BP = Blueprint("table_shape", __name__, url_prefix="/table_shapes") 19 | 20 | 21 | @pytest.mark.skip(reason="Waiting for TableShape Filtering Implementation") 22 | class List(views.ListView): 23 | """ List the TableShape """ 24 | model = models.TableShape 25 | template_name = "restaurants/table_shapes/list.html", 26 | 27 | 28 | """List.register(BP, "/")""" 29 | 30 | 31 | @BP.route("/") 32 | def list(): 33 | """List all table shapes 34 | @todo #316:30min Improve filtering of table shapes from the UI. 35 | Now to filter tables from the UI, GET params should look like this: 36 | filter_by=description=mytable&filter_by=id=1 37 | It is ambiguous to make such request from a HTML from. So either 38 | alter the way filter fields are parsed or write some JS logic. 39 | """ 40 | order_fields = request.args.getlist("order_by") 41 | filter_fields = request.args.getlist("filter_by") 42 | query = models.TableShape.query 43 | if order_fields: 44 | query = order_by(query, order_fields) 45 | if filter_fields: 46 | query = filter_by(query, filter_fields) 47 | return render_template( 48 | "restaurants/table_shapes/list.html", 49 | table_shapes=query.all()) 50 | 51 | 52 | class Create(views.CreateView): 53 | """ 54 | Create view for TableShapes 55 | """ 56 | form_class = forms.TableShapeForm 57 | template_name = "restaurants/table_shapes/create_edit.html" 58 | success_view_name = "table_shape.list" 59 | 60 | 61 | class Edit(views.UpdateView): 62 | model = models.TableShape 63 | form_class = forms.TableShapeForm 64 | template_name = "restaurants/table_shapes/create_edit.html" 65 | success_view_name = "table_shape.list" 66 | 67 | 68 | class Delete(views.DeleteView): 69 | """ 70 | @todo #312:30min Refactor all deleting views to use `views.DeleteView` 71 | and the method they were registered to bluprints. See `.register` method 72 | in `GenericView`, use it. Also uncomment all tests related to these views. 73 | """ 74 | model = models.TableShape 75 | success_view_name = "table_shape.list" 76 | 77 | 78 | Create.register(BP, "/create") 79 | Edit.register(BP, "/edit/") 80 | Delete.register(BP, "/delete/") 81 | -------------------------------------------------------------------------------- /timeless/items/models.py: -------------------------------------------------------------------------------- 1 | """File for models in items module""" 2 | from datetime import datetime 3 | 4 | from timeless import DB 5 | from timeless.models import validate_required 6 | 7 | 8 | class Item(DB.Model): 9 | """Model for item entity 10 | """ 11 | __tablename__ = "items" 12 | 13 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 14 | name = DB.Column(DB.String, nullable=False) 15 | stock_date = DB.Column(DB.DateTime, nullable=False) 16 | comment = DB.Column(DB.String, nullable=True) 17 | company_id = DB.Column(DB.Integer, DB.ForeignKey("companies.id")) 18 | created_on = DB.Column(DB.DateTime, default=datetime.utcnow, nullable=False) 19 | updated_on = DB.Column(DB.DateTime, onupdate=datetime.utcnow) 20 | company = DB.relationship("Company", back_populates="items") 21 | employee_id = DB.Column(DB.Integer, DB.ForeignKey("employees.id")) 22 | empolyee = DB.relationship("Employee", back_populates="items") 23 | history = DB.relationship("ItemHistory", back_populates="item") 24 | 25 | @validate_required("name", "stock_date", "comment", "created_on") 26 | def __init__(self, **kwargs): 27 | super(Item, self).__init__(**kwargs) 28 | 29 | def assign(self, employee): 30 | """ Assigning the item to an employee """ 31 | self.employee_id = employee.id 32 | for hist_item in self.history: 33 | if not hist_item.end_time: 34 | hist_item.end_time = datetime.utcnow 35 | break 36 | self.history.append( 37 | ItemHistory( 38 | employee_id=self.employee_id, 39 | item_id=self.id, 40 | start_time=datetime.utcnow 41 | ) 42 | ) 43 | 44 | def item_history(self): 45 | """ Returns item history 46 | 47 | """ 48 | return self.history 49 | 50 | def __repr__(self): 51 | """Return object information - String""" 52 | return "" % self.name 53 | 54 | 55 | class ItemHistory(DB.Model): 56 | """Model for item assigning history 57 | """ 58 | __tablename__ = "itemsHistory" 59 | 60 | id = DB.Column(DB.Integer, primary_key=True, autoincrement=True) 61 | start_time = DB.Column(DB.DateTime, default=datetime.utcnow, nullable=False) 62 | end_time = DB.Column(DB.DateTime) 63 | employee_id = DB.Column(DB.Integer, DB.ForeignKey("employees.id")) 64 | employee = DB.relationship("Employee", back_populates="history") 65 | item_id = DB.Column(DB.Integer, DB.ForeignKey("items.id")) 66 | item = DB.relationship("Item", back_populates="history") 67 | 68 | @validate_required("start_time") 69 | def __init__(self, **kwargs): 70 | super(ItemHistory, self).__init__(**kwargs) 71 | 72 | def __repr__(self): 73 | """Return object information - String""" 74 | return "" % self.id 75 | -------------------------------------------------------------------------------- /scripts/backup/pg_restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ########################### 4 | ####### LOAD CONFIG ####### 5 | ########################### 6 | while [ $# -gt 0 ]; do 7 | case $1 in 8 | -c) 9 | if [ -r "$2" ]; then 10 | source "$2" 11 | shift 2 12 | else 13 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Unreadable config file \"$2\"" 1>&2 14 | exit 1 15 | fi 16 | ;; 17 | -d) 18 | if [ -d "$2" ]; then 19 | RESTORE_DIR="$2" 20 | shift 2 21 | else 22 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Unreadable directory \"$2\"" 1>&2 23 | exit 1 24 | fi 25 | ;; 26 | *) 27 | echo " [ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Unknown Option \"$1\"" 1>&2 28 | exit 2 29 | ;; 30 | esac 31 | done 32 | 33 | if [ $# = 0 ]; then 34 | SCRIPTPATH=$(cd ${0%/*} && pwd -P) 35 | source $SCRIPTPATH/pg_backup.config 36 | fi; 37 | 38 | ########################### 39 | #### PRE-RESTORE CHECKS #### 40 | ########################### 41 | 42 | # Make sure we're running as the required backup user 43 | if [ "$BACKUP_USER" != "" -a "$(id -un)" != "$BACKUP_USER" ]; then 44 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] This script must be run as $BACKUP_USER. Exiting." 1>&2 45 | exit 1; 46 | fi; 47 | 48 | if [ -z $RESTORE_DIR ]; then 49 | echo -e "\nRestore directory is empty, retrieving from Google Drive backup" 50 | if ! gdrive download --timeout "$TIMEOUT" --path "$BACKUP_PATH" "$FILE_ID"; then 51 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Download from Google Drive backup of $DATABASE failed!" 1>&2 52 | else 53 | echo -e "\nDownload of database backups complete!" 54 | fi 55 | fi 56 | 57 | if [ $(find $RESTORE_DIR -type f | wc -l) -ne 1 ]; then 58 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Backup directory contains more than one file." 1>&2 59 | exit 1; 60 | fi; 61 | 62 | 63 | ########################### 64 | ### INITIALISE DEFAULTS ### 65 | ########################### 66 | 67 | if [ ! $HOSTNAME ]; then 68 | HOSTNAME="localhost" 69 | fi; 70 | 71 | if [ ! $USR ]; then 72 | USR="postgres" 73 | fi; 74 | 75 | 76 | ########################### 77 | ###### FULL BACKUP ####### 78 | ########################### 79 | 80 | RESTORE_FILE=$(find $RESTORE_DIR -type f) 81 | 82 | echo -e "\n\nPerforming restore from $RESTORE_FILE" 83 | echo -e "--------------------------------------------\n" 84 | 85 | echo "Restore $DATABASE" 86 | if ! zcat $RESTORE_FILE | psql -h "$HOSTNAME" -U "$USR" "$DATABASE"; then 87 | echo "[ERROR]["$(date +\%Y-\%m-\%d\ %H:%M:%S:%3N)"] Failed to restore backup from $RESTORE_FILE to database $DATABASE" 1>&2 88 | fi 89 | 90 | echo -e "\nDatabase restore complete!" 91 | -------------------------------------------------------------------------------- /tests/integration/it_floors_test.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | import pytest 4 | from flask import g, url_for 5 | 6 | from tests import factories 7 | from timeless.restaurants.models import Floor 8 | 9 | 10 | def test_list(client, db_session): 11 | db_session.add(Floor(location_id=None, description="Test floor")) 12 | db_session.commit() 13 | floors = client.get("/floors/") 14 | assert floors.status_code == HTTPStatus.OK 15 | assert b"Test floor" in floors.data 16 | 17 | 18 | @pytest.mark.skip(reason="Order is not yet implemented") 19 | def test_ordered_list(client): 20 | factories.FloorFactory(description="B") 21 | factories.FloorFactory(description="A") 22 | response = client.get(url_for("floor.list", order_by=["description:asc"])) 23 | html = response.data.decode('utf-8') 24 | assert html.count( 25 | "\n\n\n
    \n
    \n
    \n

    A

    " 26 | ) == 1 27 | assert response.status_code != HTTPStatus.OK 28 | 29 | 30 | @pytest.mark.skip(reason="Order is not yet implemented") 31 | def test_filtered_list(client): 32 | response = client.get(url_for('floor.list', filter_by=["description=B"])) 33 | html = response.data.decode('utf-8') 34 | assert html.count('

    A

    ') == 0 35 | assert html.count('

    B

    ') == 1 36 | assert response.status_code == HTTPStatus.OK 37 | 38 | 39 | @pytest.mark.parametrize("path", ( 40 | "/floors/edit/1", 41 | "/floors/delete/1", 42 | )) 43 | def test_login_required(client, path): 44 | response = client.post(path) 45 | assert response.headers["Location"] == url_for("auth.login", 46 | _external=True) 47 | 48 | 49 | def test_create(client, auth): 50 | location = factories.LocationFactory() 51 | auth.login() 52 | floor_data = { 53 | "description": "Test floor", 54 | "location_id": location.id 55 | } 56 | response = client.post(url_for('floor.create'), data=floor_data) 57 | assert response.headers["Location"] == "http://localhost/floors/" 58 | assert response.status_code == HTTPStatus.FOUND 59 | assert Floor.query.count() == 1 60 | floor = Floor.query.first() 61 | assert floor.description == floor_data["description"] 62 | assert floor.location_id == floor_data["location_id"] 63 | 64 | 65 | @pytest.mark.skip(reason="auth.login() is not yet implemented") 66 | def test_edit(client, auth): 67 | auth.login() 68 | assert client.get("/floors/edit/1").status_code == HTTPStatus.OK 69 | 70 | 71 | @pytest.mark.skip(reason="auth.login() is not yet implemented") 72 | def test_delete(client, auth): 73 | auth.login() 74 | floor = factories.FloorFactory() 75 | response = client.post(url_for("floor.delete", id=floor.id)) 76 | assert response.headers["Location"] == "http://localhost/floors/" 77 | assert response.status_code == HTTPStatus.FOUND 78 | assert Floor.query.count() == 0 79 | -------------------------------------------------------------------------------- /tests/integration/it_reservation_settings_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from http import HTTPStatus 3 | 4 | import pytest 5 | from flask import url_for 6 | 7 | from timeless.reservations import models 8 | 9 | """ 10 | @todo #356:30min Enable the ignored tests below when the functionality 11 | will be implemented. There will be tuning needed because there 12 | are no endpoint names in the views. 13 | """ 14 | 15 | 16 | @pytest.mark.skip 17 | def test_list(client): 18 | data = reservation_data("nicer comment here") 19 | client.post(url_for("reservations.settings_create_view"), data=data) 20 | response = client.get(url_for("reservations.settings_list")) 21 | assert response.status_code == HTTPStatus.OK 22 | html = response.data.decode("utf-8") 23 | assert html.count(str(data["num_of_persons"])) == 1 24 | assert html.count(data["comment"]) == 1 25 | 26 | 27 | def test_create(client): 28 | data = reservation_data("nice comment here") 29 | response = client.post(url_for("reservations.settings_create_view"), data=data) 30 | assert response.status_code == HTTPStatus.FOUND 31 | assert response.location.endswith(url_for("reservations.settings_list")) 32 | 33 | 34 | @pytest.mark.skip 35 | def test_edit(client): 36 | data = reservation_data("nicer comment here") 37 | new_data = reservation_data("different comment here") 38 | client.post(url_for("reservations.settings_create_view"), data=data) 39 | identifier = models.ReservationSettings.query.first().id 40 | client.post( 41 | url_for("reservations.settings_create_view", id=identifier), data=new_data 42 | ) 43 | response = client.get(url_for("reservations.settings_list")) 44 | assert response.status_code == HTTPStatus.OK 45 | html = response.data.decode("utf-8") 46 | assert html.count(str(data["num_of_persons"])) == 1 47 | assert html.count(data["comment"]) == 0 48 | assert html.count(new_data["comment"]) == 1 49 | 50 | 51 | @pytest.mark.skip 52 | def test_delete(client): 53 | data = reservation_data("my very unique comment") 54 | client.post(url_for("reservations.settings_create_view"), data=data) 55 | identifier = models.ReservationSettings.query.first().id 56 | client.post( 57 | url_for("reservations.settings_delete", setting_id=identifier) 58 | ) 59 | response = client.get(url_for("reservations.settings_list")) 60 | assert response.status_code == HTTPStatus.OK 61 | html = response.data.decode("utf-8") 62 | assert html.count(str(data["num_of_persons"])) == 0 63 | assert html.count(data["comment"]) == 0 64 | 65 | 66 | def reservation_data(comment): 67 | data = { 68 | "start_time": datetime.datetime.now(), 69 | "end_time": datetime.datetime.now() + datetime.timedelta(hours=1), 70 | "customer_id": 1, 71 | "num_of_persons": 1, 72 | "comment": comment, 73 | "status": "on", 74 | "multiple": True, 75 | "tables": 1 76 | } 77 | return data 78 | -------------------------------------------------------------------------------- /migrations/versions/2019-02-10T185948.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: fb887393e975 4 | Revises: 7f38c5d3030d 5 | Create Date: 2019-02-10 18:59:48.800057+00:00 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'fb887393e975' 14 | down_revision = '7f38c5d3030d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('customers', 22 | sa.Column('poster_id', sa.Integer(), nullable=True), 23 | sa.Column('synchronized_on', sa.DateTime(), nullable=True), 24 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 25 | sa.Column('first_name', sa.String(), nullable=False), 26 | sa.Column('last_name', sa.String(), nullable=False), 27 | sa.Column('phone_number', sa.String(), nullable=False), 28 | sa.Column('created_on', sa.DateTime(), nullable=False), 29 | sa.Column('updated_on', sa.DateTime(), nullable=True), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | op.create_table('reservations', 33 | sa.Column('created_on', sa.DateTime(), nullable=False), 34 | sa.Column('updated_on', sa.DateTime(), nullable=False), 35 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 36 | sa.Column('start_time', sa.DateTime(), nullable=False), 37 | sa.Column('end_time', sa.DateTime(), nullable=False), 38 | sa.Column('duration', sa.Time(), nullable=False), 39 | sa.Column('customer_id', sa.Integer(), nullable=True), 40 | sa.Column('num_of_persons', sa.DateTime(), nullable=False), 41 | sa.Column('comment', sa.String(), nullable=False), 42 | sa.Column('status', sa.Enum('unconfirmed', 'confirmed', 'started', 'finished', 'canceled', 'late', 'not_contacting', name='reservationstatus'), nullable=False), 43 | sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ), 44 | sa.PrimaryKeyConstraint('id') 45 | ) 46 | op.create_table('table_reservations', 47 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 48 | sa.Column('reservation_id', sa.Integer(), nullable=True), 49 | sa.Column('table_id', sa.Integer(), nullable=True), 50 | sa.ForeignKeyConstraint(['reservation_id'], ['reservations.id'], ), 51 | sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ), 52 | sa.PrimaryKeyConstraint('id') 53 | ) 54 | op.add_column('tables', sa.Column('poster_id', sa.Integer(), nullable=True)) 55 | op.add_column('tables', sa.Column('synchronized_on', sa.DateTime(), nullable=True)) 56 | # ### end Alembic commands ### 57 | 58 | 59 | def downgrade(): 60 | # ### commands auto generated by Alembic - please adjust! ### 61 | op.drop_column('tables', 'synchronized_on') 62 | op.drop_column('tables', 'poster_id') 63 | op.drop_table('table_reservations') 64 | op.drop_table('reservations') 65 | op.drop_table('customers') 66 | # ### end Alembic commands ### 67 | -------------------------------------------------------------------------------- /timeless/poster/tasks.py: -------------------------------------------------------------------------------- 1 | """Celery tasks for poster module""" 2 | import os 3 | 4 | from flask import current_app 5 | 6 | from celery import shared_task 7 | 8 | from timeless import DB 9 | from timeless.customers.models import Customer 10 | from timeless.poster.api import Authenticated, PosterAuthData, Poster 11 | from timeless.restaurants.models import Table, Location 12 | 13 | 14 | def __poster_api(): 15 | auth_data = PosterAuthData( 16 | application_id=current_app.config.get("poster_application_id"), 17 | application_secret=current_app.config.get("poster_application_secret"), 18 | redirect_uri=current_app.config.get("poster_redirect_uri"), 19 | code=current_app.config.get("poster_code"), 20 | ) 21 | auth_token = Authenticated(auth_data=auth_data).auth() 22 | poster = Poster(auth_token=auth_token) 23 | return poster 24 | 25 | 26 | @shared_task 27 | def sync_tables(): 28 | """ 29 | Periodic task for fetching and saving tables from Poster 30 | @todo #187:30min Set up scheduler for celery, 31 | docs - http://docs.celeryproject.org/en/ 32 | latest/userguide/periodic-tasks.html#id5 33 | Also should make small refactoring: celery.py should situated in 34 | timelessis/celery.py not in timelessis/sync/celery.py 35 | """ 36 | for poster_table in __poster_api().tables(): 37 | table = DB.session(Table).query.filter_by( 38 | name=poster_table["name"], floor_id=poster_table["floor_id"] 39 | ).first() 40 | merge_data( 41 | model=Table, poster_data=poster_table, timelessis_data=table 42 | ) 43 | 44 | 45 | @shared_task 46 | def sync_customers(): 47 | """ 48 | Periodic task for fetching and saving tables from Poster 49 | Docs - https://dev.joinposter.com/docs/api#clients-getclients 50 | """ 51 | for poster_customer in __poster_api().customers().get("response", []): 52 | customer = Customer.query.filter_by( 53 | poster_id=poster_customer["client_id"] 54 | ).first() 55 | merge_data( 56 | model=Customer, 57 | poster_data=poster_customer, 58 | timelessis_data=customer 59 | ) 60 | 61 | 62 | @shared_task 63 | def sync_locations(): 64 | """ 65 | Periodic task for fetching and saving location from Poster 66 | """ 67 | for poster_location in __poster_api().locations(): 68 | location = DB.session(Location).query.filter_by( 69 | name=poster_location["name"], 70 | code=poster_location["code"] 71 | ).first() 72 | merge_data( 73 | model=Location, 74 | poster_data=poster_location, 75 | timelessis_data=location 76 | ) 77 | 78 | 79 | def merge_data(model, poster_data, timelessis_data): 80 | if timelessis_data: 81 | DB.session.add(model.merge_with_poster(timelessis_data, poster_data)) 82 | else: 83 | DB.session.merge(model.create_by_poster(poster_data)) 84 | DB.session.commit() 85 | -------------------------------------------------------------------------------- /timeless/poster/api.py: -------------------------------------------------------------------------------- 1 | """Poster API""" 2 | 3 | from urllib.parse import urljoin 4 | import attr 5 | import requests 6 | 7 | 8 | from timeless.poster import exceptions 9 | 10 | 11 | class Poster: 12 | """Poster application API. 13 | 14 | """ 15 | 16 | GET = "GET" 17 | POST = "POST" 18 | client_id = 0 19 | token = "" 20 | 21 | def __init__(self, auth_token=None, **kwargs): 22 | self.url = kwargs.get("url", "https://joinposter.com/api") 23 | self.account = kwargs.get("client_id", 0) 24 | self.auth_token = auth_token 25 | 26 | def locations(self): 27 | """Fetches location data 28 | 29 | :return: 30 | Location data 31 | """ 32 | return self.send(method=self.GET, action="clients.getLocations").json() 33 | 34 | def tables(self): 35 | """Fetches data about tables 36 | 37 | :return: 38 | Data about tables 39 | """ 40 | return self.send(method=self.GET, action="clients.getTables").json() 41 | 42 | def customers(self): 43 | """Fetches data about customers 44 | 45 | :return: 46 | Data about customers 47 | """ 48 | return self.send(method=self.GET, action="clients.getClients").json() 49 | 50 | def send(self, **kwargs): 51 | """Sends http request for specific poster action 52 | 53 | :return: response 54 | """ 55 | response = requests.request( 56 | kwargs.get("method"), 57 | urljoin(self.url, kwargs.get("action", "")), 58 | params=kwargs 59 | ) 60 | response.raise_for_status() 61 | return response 62 | 63 | 64 | @attr.s 65 | class PosterAuthData: 66 | """ Poster auth data class """ 67 | application_id = attr.ib() 68 | application_secret = attr.ib() 69 | redirect_uri = attr.ib() 70 | code = attr.ib() 71 | 72 | 73 | class Authenticated: 74 | """ Poster Auth class """ 75 | auth_url = "https://joinposter.com/api/v2/auth/access_token" 76 | 77 | def __init__(self, auth_data: PosterAuthData): 78 | self.auth_data = auth_data 79 | 80 | def auth(self): 81 | """ 82 | Authentication into poster API 83 | https://dev.joinposter.com/en/docs/api#authorization-in-api and use 84 | """ 85 | auth_data = { 86 | "application_id": self.auth_data.application_id, 87 | "application_secret": self.auth_data.application_secret, 88 | "grant_type": "authorization_code", 89 | "redirect_uri": self.auth_data.redirect_uri, 90 | "code": self.auth_data.code, 91 | } 92 | 93 | response = requests.post(self.auth_url, data=auth_data) 94 | 95 | if not response.ok: 96 | raise exceptions.PosterAPIError("Problem accessing poster api") 97 | 98 | token = response.json().get("access_token") 99 | 100 | if not token: 101 | raise exceptions.PosterAPIError("Token not found") 102 | 103 | return token 104 | -------------------------------------------------------------------------------- /tests/view/items/test_items.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | import flask 4 | import pytest 5 | 6 | from timeless.items.models import Item 7 | from tests import factories 8 | 9 | 10 | """ Tests for the items. 11 | @todo #417:30min Fix ItemListView problem of returning empty list. 12 | ItemListView is returning an empty list when it should return valid values. 13 | Test below is valid, it calls ItemListView with an authenticated user but it 14 | isn't retrieving the items. Fix the ItemListView class and then uncomment the 15 | test below. 16 | @todo #311:30min CreateView is not working as intended and is not saving Items. 17 | Find and fix the problem and then uncomment the def test_create(client) 18 | method. 19 | """ 20 | 21 | 22 | @pytest.mark.skip(reason="Correct the ItemListView bug") 23 | def test_list(client): 24 | """ Test list is okay """ 25 | employee = factories.EmployeeFactory() 26 | factories.ItemFactory(name="1") 27 | factories.ItemFactory(name="2") 28 | factories.ItemFactory(name="3") 29 | flask.g.user = employee 30 | with client.session_transaction() as session: 31 | session["user_id"] = employee.id 32 | response = client.get("/items/") 33 | assert b"

    1

    " in response.data 34 | assert b"

    2

    " in response.data 35 | assert b"

    3

    " in response.data 36 | assert response.status_code == HTTPStatus.OK 37 | 38 | 39 | @pytest.mark.skip(reason="Correct CreateView problem") 40 | def test_create(client): 41 | """ Test create is okay """ 42 | company = factories.CompanyFactory() 43 | employee = factories.EmployeeFactory(company=company) 44 | item_name = "Yellow Fedora" 45 | item_comment = "A yellow fedora that belonged to a hero from a movie" 46 | item = { 47 | "name": item_name, 48 | "comment": item_comment, 49 | "company_id": company.id, 50 | "employee_id": employee.id, 51 | } 52 | create_response = client.post("/items/create", data=item) 53 | database_item = Item.query.filter_by(name="Yellow Fedora").first() 54 | assert create_response.status_code == HTTPStatus.OK 55 | assert database_item is not None 56 | assert database_item.name == item_name 57 | assert database_item.comment == item_comment 58 | assert database_item.company_id == company.id 59 | assert database_item.employee_id == employee.id 60 | 61 | 62 | @pytest.mark.parametrize("path", ( 63 | "items.create", 64 | )) 65 | def test_login_required(client, path): 66 | response = client.post(flask.url_for(path)) 67 | assert response.status_code == HTTPStatus.FOUND 68 | assert response.headers["Location"].endswith("auth/login") 69 | 70 | 71 | def test_edit(client): 72 | """ Test edit is okay """ 73 | assert client.get("/items/edit").status_code == HTTPStatus.OK 74 | 75 | 76 | def test_delete(client): 77 | """ Test delete is okay """ 78 | response = client.post("/items/delete", data={"id": 1}) 79 | assert response.headers["Location"] == "http://localhost/items/" 80 | -------------------------------------------------------------------------------- /timeless/restaurants/floors/views.py: -------------------------------------------------------------------------------- 1 | """Floors views module. 2 | @todo #421:30min Continue implementing list floors view. Floors index page 3 | must allow sorting and filtering of floors for every column and it has to be 4 | accessed by the Location page. Then remove skip annotation from 5 | FloorsListView tests for ordering and filtering. Authentication must be 6 | faked in order to test work. 7 | @todo #95:30min Continue on developing create floor view. After creation we 8 | should render Detail view showing the newly inserted floor information and 9 | a message with the result of the insertion of this data to the repository. 10 | The tests must cover if the screen is being showed correctly and if the 11 | errors are being displayed if any. 12 | @todo #95:30min Implement edit / update floor views. Edit / update floor 13 | view is composed of two views: first view of edit / floor view must load the 14 | desired floor data onto the screen and must extend 15 | timeless/views.py::DetailView; second part of update view extends 16 | timeless/views.py::UpdateView, receives data from the first view and 17 | must save the data to the repository. The tests must include checking if the 18 | view screens were correctly built and if the data was saved to the repository. 19 | @todo #424:30min The last step for deletion is to ask for confirmation before 20 | actually deleting the object. When clicking on link on list floors pages a 21 | javascript modal should appear asking user for confirmation. Also make sure 22 | that when a floor is deleted, all depending entities (like Tables, for 23 | example) are deleted 24 | """ 25 | from flask import ( 26 | Blueprint, flash, redirect, render_template, request, url_for 27 | ) 28 | 29 | from timeless import views 30 | from timeless.auth import views as auth 31 | from timeless.restaurants.floors.forms import FloorForm 32 | from timeless.restaurants.models import Floor 33 | 34 | 35 | BP = Blueprint("floor", __name__, url_prefix="/floors") 36 | 37 | 38 | class List(views.ListView): 39 | """List all floors""" 40 | template_name = "restaurants/floors/list.html" 41 | model = Floor 42 | 43 | 44 | @BP.route("/edit/", methods=("GET", "POST")) 45 | @auth.login_required 46 | def edit(id): 47 | """ Edit floor with id """ 48 | if request.method == "POST": 49 | flash("Edit not yet implemented") 50 | action = "edit" 51 | return render_template( 52 | "restaurants/floors/create_edit.html", 53 | action=action 54 | ) 55 | 56 | 57 | class Delete(views.DeleteView): 58 | """ Delete floor with id """ 59 | decorators = (auth.login_required,) 60 | model = Floor 61 | success_view_name = "floor.list" 62 | 63 | 64 | class Create(views.CreateView): 65 | """ Create a new floor instance """ 66 | decorators = (auth.login_required,) 67 | template_name = "restaurants/floors/create_edit.html" 68 | success_view_name = "floor.list" 69 | form_class = FloorForm 70 | 71 | 72 | class Detail(views.DetailView): 73 | """ Detail view for Reservation Settings """ 74 | model = Floor 75 | template_name = "restaurants/floors/create_edit.html" 76 | 77 | 78 | Create.register(BP, "/create") 79 | List.register(BP, "/") 80 | Delete.register(BP, "/delete/") 81 | Detail.register(BP, "/") 82 | -------------------------------------------------------------------------------- /tests/view/crudapi/test_crud_api.py: -------------------------------------------------------------------------------- 1 | """ Tests for CrudeAPIView. """ 2 | import json 3 | from http import HTTPStatus 4 | 5 | import pytest 6 | from werkzeug.exceptions import NotFound 7 | 8 | from timeless.views import FakeAPIView, FakeModel 9 | 10 | 11 | def test_get_found_object(app): 12 | """ 13 | Tests for CrudeAPIView get method when the object exists. 14 | FakeAPIView extends CrudeAPIView. 15 | """ 16 | with app.test_request_context( 17 | "/test/crudapitest" 18 | ): 19 | apiview = FakeAPIView() 20 | result = apiview.get(FakeModel.FakeQuery.FAKE_OBJECT_ID) 21 | json_result = json.loads(result[0].get_data(as_text=True)) 22 | assert result[0].is_json is True 23 | assert json_result == {"some_id": FakeModel.FakeQuery.FAKE_OBJECT_ID, "some_attr": "attr"}, \ 24 | "Wrong result returned from CrudeAPI view" 25 | assert result[1] == HTTPStatus.OK, "Wrong response from CrudeAPI view" 26 | 27 | 28 | def test_get_not_found_object(app): 29 | """ 30 | Tests for CrudeAPIView get method when the object does not exists. 31 | FakeAPIView extends CrudeAPIView. 32 | """ 33 | with app.test_request_context( 34 | "/api/crudapi" 35 | ): 36 | apiview = FakeAPIView() 37 | with pytest.raises(NotFound, message="Fake object not found"): 38 | apiview.get(0) 39 | 40 | 41 | def test_post_object(app): 42 | """ 43 | Tests for CrudeAPIView post method. 44 | FakeAPIView extends CrudeAPIView. 45 | """ 46 | with app.test_request_context( 47 | "/test/crudapitest" 48 | ): 49 | apiview = FakeAPIView() 50 | payload = {"some_id": 6, "some_attr": "attr6"} 51 | result = apiview.post(payload) 52 | json_result = json.loads(result[0].get_data(as_text=True)) 53 | assert result[0].is_json is True 54 | assert json_result == payload, "Wrong result returned from CrudeAPI view" 55 | assert result[1] == HTTPStatus.OK, "Wrong response from CrudeAPI view" 56 | 57 | 58 | def test_put_object(app): 59 | """ 60 | Tests for CrudeAPIView post method. 61 | FakeAPIView extends CrudeAPIView. 62 | """ 63 | with app.test_request_context( 64 | "/test/crudapitest" 65 | ): 66 | apiview = FakeAPIView() 67 | payload = {"some_id": 6, "some_attr": "attr6"} 68 | result = apiview.put(payload) 69 | json_result = json.loads(result[0].get_data(as_text=True)) 70 | assert result[0].is_json is True 71 | assert json_result == payload, "Wrong result returned from CrudeAPI view" 72 | assert result[1] == HTTPStatus.OK, "Wrong response from CrudeAPI view" 73 | 74 | 75 | def test_delete_object(app): 76 | """ 77 | Tests for CrudeAPIView post method. 78 | FakeAPIView extends CrudeAPIView. 79 | """ 80 | with app.test_request_context( 81 | "/test/crudapitest" 82 | ): 83 | apiview = FakeAPIView() 84 | result = apiview.delete(object_id=FakeModel.FakeQuery.FAKE_OBJECT_ID) 85 | json_result = json.loads(result[0].get_data(as_text=True)) 86 | assert result[0].is_json is True 87 | assert json_result == {"some_id": FakeModel.FakeQuery.FAKE_OBJECT_ID, "some_attr": "attr"}, \ 88 | "Wrong result returned from CrudeAPI view" 89 | assert result[1] == HTTPStatus.OK, "Wrong response from CrudeAPI view" 90 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | 22 | config.set_main_option('sqlalchemy.url', 23 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 24 | target_metadata = current_app.extensions['migrate'].db.metadata 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure(url=url) 46 | 47 | with context.begin_transaction(): 48 | context.run_migrations() 49 | 50 | 51 | def run_migrations_online(): 52 | """Run migrations in 'online' mode. 53 | 54 | In this scenario we need to create an Engine 55 | and associate a connection with the context. 56 | 57 | """ 58 | 59 | # this callback is used to prevent an auto-migration from being generated 60 | # when there are no changes to the schema 61 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 62 | def process_revision_directives(context, revision, directives): 63 | if getattr(config.cmd_opts, 'autogenerate', False): 64 | script = directives[0] 65 | if script.upgrade_ops.is_empty(): 66 | directives[:] = [] 67 | logger.info('No changes in schema detected.') 68 | 69 | engine = engine_from_config(config.get_section(config.config_ini_section), 70 | prefix='sqlalchemy.', 71 | poolclass=pool.NullPool) 72 | 73 | connection = engine.connect() 74 | context.configure(connection=connection, 75 | target_metadata=target_metadata, 76 | process_revision_directives=process_revision_directives, 77 | **current_app.extensions['migrate'].configure_args) 78 | 79 | try: 80 | with context.begin_transaction(): 81 | context.run_migrations() 82 | except Exception as exception: 83 | logger.error(exception) 84 | raise exception 85 | finally: 86 | connection.close() 87 | 88 | if context.is_offline_mode(): 89 | run_migrations_offline() 90 | else: 91 | run_migrations_online() 92 | -------------------------------------------------------------------------------- /scripts/install/deploy/install_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CURRENT_DIR=`pwd` 4 | 5 | which psql 6 | if [ "$?" -gt "0" ]; then 7 | echo "Postgres Not installed, installing" 8 | sudo apt-get -y install wget ca-certificates 9 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 10 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' 11 | sudo apt-get update 12 | sudo apt-get -y install postgresql postgresql-contrib 13 | echo "Done installing Postgres" 14 | else 15 | echo "Postgres already installed" 16 | fi 17 | 18 | echo "Restarting Postgres" 19 | service postgresql restart 20 | 21 | sudo cp $CURRENT_DIR/scripts/install/deploy/timeless_pg.service /lib/systemd/system/ 22 | sudo systemctl start timeless_pg.service 23 | 24 | which jq 25 | if [ "$?" -gt "0" ]; then 26 | echo "Installing jq to parse credentials" 27 | sudo apt-get -y install jq 28 | echo "Done installing jq" 29 | else 30 | echo "jq already installed" 31 | fi 32 | 33 | #checks if the database exists and create if not 34 | if ! sudo -u postgres -H -- psql -lqt | cut -d \| -f 1 | grep -qw "timelessdb"; then 35 | echo "Creating database: timelessdb" 36 | sudo -u postgres psql -c "CREATE DATABASE timelessdb;" 37 | echo "Timeless database created successfully" 38 | fi 39 | 40 | PG_USER=$(cat ${credentials_src} | jq '.credentials.postgres.staging.username') 41 | PG_PWD=$(cat ${credentials_src} | jq '.credentials.postgres.staging.password') 42 | 43 | #checks if the user exists and create if not 44 | if ! sudo -u postgres -H -- psql -t -c '\du' | cut -d \| -f 1 | grep -qw "timeless_user"; then 45 | echo "Creating user: $PG_USER" 46 | sudo -u postgres psql -c "CREATE USER $PG_USER WITH 47 | SUPERUSER 48 | CREATEDB 49 | CREATEROLE 50 | INHERIT 51 | LOGIN 52 | ENCRYPTED PASSWORD '$PG_PWD';" 53 | echo "Timeless user created successfully" 54 | fi 55 | 56 | company_id=$(sudo -u postgres -H -- psql -qtA -d timelessdb -c "INSERT INTO companies (name, code, address, created_on, updated_on) values ('Timeless', 'Tm', '', current_timestamp, current_timestamp) returning id") 57 | role_id=$(sudo -u postgres -H -- psql -qtA -d timelessdb -c "INSERT INTO roles (name, works_on_shifts, company_id) values ('Administrator', False, $company_id) returning id") 58 | 59 | # read credentials from rultor 60 | first_name=$(cat ${credentials_src} | jq '.credentials.account.admin.first_name') 61 | last_name=$(cat ${credentials_src} | jq '.credentials.account.admin.last_name') 62 | username=$(cat ${credentials_src} | jq '.credentials.account.admin.username') 63 | email=$(cat ${credentials_src} | jq '.credentials.account.admin.email') 64 | password=$(cat ${credentials_src} | jq '.credentials.account.admin.password') 65 | pincode=$(cat ${credentials_src} | jq '.credentials.account.admin.pincode') 66 | 67 | 68 | sudo -u postgres -H -- psql -d timelessdb -c "INSERT INTO employee 69 | (first_name, last_name, username, phone_number, birth_date, 70 | registration_date, account_status, user_status, email, password, pin_code, 71 | comment, company_id, role_id) 72 | values 73 | ('$first_name', '$last_name', '$username', '988888', '', '', 'active', 'active', 74 | '$email', '$password', '$pincode', 'Timeless user', $company_id, $role_id 75 | ) 76 | " 77 | -------------------------------------------------------------------------------- /tests/integration/sync/it_sync_tables_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from datetime import date 4 | 5 | from tests.poster_mock import free_port, start_server 6 | from timeless.restaurants.models import Table 7 | 8 | from tests import factories 9 | 10 | """Integration tests for Table Sync with database""" 11 | 12 | def test_sync_table(db_session): 13 | 'Created info in tables for test' 14 | factories.FloorFactory.create_batch(size=10) 15 | factories.TableShapeFactory.create_batch(size=10) 16 | factories.SchemeTypeFactory.create_batch(size=10) 17 | 18 | port = free_port() 19 | poster_table_id=10 20 | poster_table_name="Table test" 21 | poster_table_floor_id=5 22 | poster_table_x=800 23 | poster_table_y=640 24 | poster_table_width=200 25 | poster_table_height=200 26 | poster_table_status="2" 27 | poster_table_max_capacity=5 28 | poster_table_min_capacity=5 29 | poster_table_multiple=False 30 | poster_table_playstation=True 31 | poster_table_shape_id=3 32 | poster_table_deposit_hour=5 33 | poster_table_created_on=date.today() 34 | poster_table_updated_on=date.today() 35 | start_server(port, 36 | tables= [ 37 | { 38 | "id": poster_table_id, 39 | "name": poster_table_name, 40 | "floor_id": poster_table_floor_id, 41 | "x": poster_table_x, 42 | "y": poster_table_y, 43 | "width": poster_table_width, 44 | "height": poster_table_height, 45 | "status": poster_table_status, 46 | "max_capacity": poster_table_max_capacity, 47 | "min_capacity": poster_table_min_capacity, 48 | "multiple": poster_table_multiple, 49 | "playstation": poster_table_playstation, 50 | "shape_id": poster_table_shape_id, 51 | "deposit_hour": poster_table_deposit_hour, 52 | "created_on": poster_table_created_on, 53 | "updated_on": poster_table_updated_on 54 | } 55 | ] 56 | ) 57 | table_in_database = Table( 58 | id=10, 59 | name="Table test", 60 | floor_id=5, 61 | x=800, 62 | y=640, 63 | width=200, 64 | height=200, 65 | status="2", 66 | max_capacity=5, 67 | min_capacity=5, 68 | multiple=False, 69 | playstation=True, 70 | shape_id=3, 71 | deposit_hour=5, 72 | created_on=date.today(), 73 | updated_on=date.today() 74 | ) 75 | db_session.add(table_in_database) 76 | db_session.commit() 77 | 78 | row = Table.query.filter_by(id=10).one() 79 | assert row.id == poster_table_id 80 | assert row.name == poster_table_name 81 | assert row.floor_id == poster_table_floor_id 82 | assert row.x == poster_table_x 83 | assert row.y == poster_table_y 84 | assert row.width == poster_table_width 85 | assert row.height == poster_table_height 86 | assert row.status == poster_table_status 87 | assert row.max_capacity == poster_table_max_capacity 88 | assert row.min_capacity == poster_table_min_capacity 89 | assert row.multiple == poster_table_multiple 90 | assert row.playstation == poster_table_playstation 91 | assert row.shape_id == poster_table_shape_id 92 | assert row.created_on == poster_table_created_on 93 | assert row.updated_on == poster_table_updated_on -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Scripts to deploy application to staging server 4 | 5 | ENVIRONMENT='staging' 6 | 7 | SERVER=$(jq -r ".credentials.server.$ENVIRONMENT.address" ../credentials.json) 8 | USER=$(jq -r ".credentials.server.$ENVIRONMENT.username" ../credentials.json) 9 | PASS=$(jq -r ".credentials.server.$ENVIRONMENT.password" ../credentials.json) 10 | PG_USER=$(jq -r ".credentials.postgres.$ENVIRONMENT.username" ../credentials.json) 11 | PG_PASS=$(jq -r ".credentials.postgres.$ENVIRONMENT.password" ../credentials.json) 12 | 13 | 14 | FIRST_NAME=$(jq -r ".credentials.account.admin.first_name" ../credentials.json) 15 | LAST_NAME=$(jq -r ".credentials.account.admin.last_name" ../credentials.json) 16 | USERNAME=$(jq -r ".credentials.account.admin.username" ../credentials.json) 17 | EMAIL=$(jq -r ".credentials.account.admin.email" ../credentials.json) 18 | PASSWORD=$(jq -r ".credentials.account.admin.password" ../credentials.json) 19 | PINCODE=$(jq -r ".credentials.account.admin.pincode" ../credentials.json) 20 | 21 | echo "-- Remove our own venv dir" 22 | rm -rf ./venv 23 | 24 | echo "-- Copy credential file" 25 | sudo cp ../credentials.json . 26 | 27 | echo "-- Remove existing dir" 28 | sshpass -p $PASS ssh -o StrictHostKeyChecking=no $USER@$SERVER -tt << EOF 29 | rm -rf /app 30 | logout 31 | EOF 32 | 33 | echo "-- Copy application code to staging server" 34 | sshpass -p $PASS scp -o StrictHostKeyChecking=no -r `pwd` $USER@$SERVER:/app 35 | 36 | # add scripts in cron (like the one created in #47) 37 | # verify the webapplication is running 38 | echo "-- Execute install script" 39 | sshpass -p $PASS ssh -o StrictHostKeyChecking=no $USER@$SERVER -tt << EOF 40 | cd /app 41 | chmod +x /app/scripts/install/deploy/install_dependencies.sh 42 | /app/scripts/install/deploy/install_dependencies.sh 43 | cd /app 44 | echo "-- REPLACE: add scripts to cron" 45 | python3.6 -m venv venv 46 | echo "-- Enabling virtual environment" 47 | . venv/bin/activate 48 | echo "-- Installing dependent libraries" 49 | pip3 install -r requirements.txt 50 | echo "-- Running database migrations" 51 | export TIMELESSIS_CONFIG="config.StagingConfig" 52 | export SQLALCHEMY_DATABASE_URI="postgresql://$PG_USER:$PG_PASS@localhost/timelessdb" 53 | python3.6 manage.py db upgrade 54 | company_id=$(sudo -u postgres -H -- psql -qtA -d timelessdb -c "INSERT INTO companies (name, code, address, created_on, updated_on) values ('Timeless', 'Tm', '', current_timestamp, current_timestamp) returning id") 55 | role_id=$(sudo -u postgres -H -- psql -qtA -d timelessdb -c "INSERT INTO roles (name, works_on_shifts, company_id) values ('Administrator', False, $company_id) returning id") 56 | sudo -u postgres -H -- psql -d timelessdb -c "INSERT INTO employee 57 | (first_name, last_name, username, phone_number, birth_date, 58 | registration_date, account_status, user_status, email, password, pin_code, 59 | comment, company_id, role_id) 60 | values 61 | ('$FIRST_NAME', '$LAST_NAME', '$USERNAME', '988888', '', '', 'active', 'active', 62 | '$EMAIL', '$PASSWORD', '$PINCODE', 'Timeless user', $company_id, $role_id 63 | ) 64 | " 65 | echo "-- Running web application server" 66 | export FLASK_APP=main.py 67 | export FLASK_ENV=development 68 | export FLASK_RUN_PORT=80 69 | export FLASK_RUN_HOST=$SERVER 70 | nohup flask run > /var/log/timeless.log 2>&1 & 71 | rm -rf /app/credentials.json 72 | echo "-- REPLACE: verify web application is running ok" 73 | logout 74 | EOF 75 | 76 | exit 0 77 | --------------------------------------------------------------------------------