Initial commit
This commit is contained in:
commit
3c1bda5f1f
21
.devcontainer/Dockerfile
Normal file
21
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
|||
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
|
||||
ARG VARIANT=3-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] If your requirements rarely change, uncomment this section to add them to the image.
|
||||
# COPY requirements.txt /tmp/pip-tmp/
|
||||
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
||||
# && rm -rf /tmp/pip-tmp
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
|
||||
|
53
.devcontainer/devcontainer.json
Normal file
53
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/python-3-postgres
|
||||
// Update the VARIANT arg in docker-compose.yml to pick a Python version
|
||||
{
|
||||
"name": "Python 3 & PostgreSQL",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"sqltools.connections": [{
|
||||
"name": "Container database",
|
||||
"driver": "PostgreSQL",
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 5432,
|
||||
"database": "postgres",
|
||||
"username": "postgres",
|
||||
"password": "postgres"
|
||||
}],
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.testing.pytestPath": "/usr/local/py-utils/bin/pytest"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"mtxr.sqltools",
|
||||
"mtxr.sqltools-driver-pg"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [5000, 5432],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "pip install --user -r requirements.txt",
|
||||
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
43
.devcontainer/docker-compose.yml
Normal file
43
.devcontainer/docker-compose.yml
Normal file
|
@ -0,0 +1,43 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a version of Python: 3, 3.10, 3.9, 3.8, 3.7, 3.6
|
||||
# Append -bullseye or -buster to pin to an OS version.
|
||||
# Use -bullseye variants on local arm64/Apple Silicon.
|
||||
VARIANT: "3.9"
|
||||
# Optional Node.js version to install
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
volumes:
|
||||
postgres-data: null
|
94
.gitignore
vendored
Normal file
94
.gitignore
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
# Created by https://www.gitignore.io
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
venv
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
|
||||
### Django ###
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
|
||||
.env
|
||||
db.sqlite3
|
||||
|
||||
usermedia/
|
||||
|
||||
data/
|
0
contacts/__init__.py
Normal file
0
contacts/__init__.py
Normal file
26
contacts/app.py
Normal file
26
contacts/app.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Contacts API"""
|
||||
import os
|
||||
|
||||
from . import db
|
||||
from .contacts import Contacts, Contact
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_restful import Api
|
||||
from flask_cors import CORS
|
||||
|
||||
def create_app(test_config=None):
|
||||
"""Initiate Flask app and bind routes"""
|
||||
app = Flask(__name__)
|
||||
db.init_app(app)
|
||||
api = Api(app)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
api.add_resource(Contacts, '/api/contacts')
|
||||
api.add_resource(Contact, '/api/contacts/<int:uid>')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
created_app = create_app()
|
||||
created_app.run(port=5000, debug=False)
|
225
contacts/contacts.py
Normal file
225
contacts/contacts.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
import functools
|
||||
|
||||
from flask import (
|
||||
Blueprint, flash, g, redirect, render_template, request, session, url_for
|
||||
)
|
||||
from flask import Blueprint
|
||||
from flask_restful import Resource, Api, reqparse
|
||||
|
||||
from .db import get_db
|
||||
|
||||
class Contacts(Resource):
|
||||
def get(self):
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT uid, last_name_val, first_name_val, middle_name_val, street_val, build, build_k, apartment, telephone"
|
||||
" FROM main"
|
||||
" join last_name on main.last_name = last_name_id"
|
||||
" join first_name on main.first_name = first_name_id"
|
||||
" join middle_name on main.middle_name = middle_name_id"
|
||||
" join street on main.street = street_id")
|
||||
contacts = cur.fetchall()
|
||||
def convert(data):
|
||||
return {
|
||||
'uid': data[0],
|
||||
'last_name': data[1],
|
||||
'first_name': data[2],
|
||||
'middle_name': data[3],
|
||||
'street': data[4],
|
||||
'build': data[5],
|
||||
'build_k': data[6],
|
||||
'apartment': data[7],
|
||||
'telephone': data[8]
|
||||
}
|
||||
contacts = [convert(contact) for contact in contacts]
|
||||
return contacts
|
||||
|
||||
def post(self):
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('last_name')
|
||||
parser.add_argument('first_name')
|
||||
parser.add_argument('middle_name')
|
||||
parser.add_argument('street')
|
||||
parser.add_argument('build')
|
||||
parser.add_argument('build_k')
|
||||
parser.add_argument('apartment')
|
||||
parser.add_argument('telephone', required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
cur.execute("SELECT uid FROM main ORDER BY uid DESC LIMIT 1")
|
||||
uid = cur.fetchone()
|
||||
if uid is None:
|
||||
uid = 0
|
||||
else:
|
||||
uid = uid[0] + 1
|
||||
|
||||
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
|
||||
last_name = cur.fetchone()
|
||||
if last_name is None:
|
||||
cur.execute("INSERT INTO last_name (last_name_val) VALUES (%s)", (args['last_name'],))
|
||||
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
|
||||
last_name = cur.fetchone()
|
||||
last_name = last_name[0]
|
||||
else:
|
||||
last_name = last_name[0]
|
||||
|
||||
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
|
||||
first_name = cur.fetchone()
|
||||
if first_name is None:
|
||||
cur.execute("INSERT INTO first_name (first_name_val) VALUES (%s)", (args['first_name'],))
|
||||
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
|
||||
first_name = cur.fetchone()
|
||||
first_name = first_name[0]
|
||||
else:
|
||||
first_name = first_name[0]
|
||||
|
||||
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
|
||||
middle_name = cur.fetchone()
|
||||
if middle_name is None:
|
||||
cur.execute("INSERT INTO middle_name (middle_name_val) VALUES (%s)", (args['middle_name'],))
|
||||
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
|
||||
middle_name = cur.fetchone()
|
||||
middle_name = middle_name[0]
|
||||
else:
|
||||
middle_name = middle_name[0]
|
||||
|
||||
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
|
||||
street = cur.fetchone()
|
||||
if street is None:
|
||||
cur.execute("INSERT INTO street (street_val) VALUES (%s)", (args['street'],))
|
||||
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
|
||||
street = cur.fetchone()
|
||||
street = street[0]
|
||||
else:
|
||||
street = street[0]
|
||||
|
||||
cur.execute("INSERT INTO main (uid, last_name, first_name, middle_name, street, build, build_k, apartment, telephone)"
|
||||
" VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
||||
(uid, last_name, first_name, middle_name, street, args['build'], args['build_k'], args['apartment'], args['telephone']))
|
||||
|
||||
db.commit()
|
||||
return {'uid': uid}, 201
|
||||
|
||||
class Contact(Resource):
|
||||
def get(self, uid):
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT uid, last_name_val, first_name_val, middle_name_val, street_val, build, build_k, apartment, telephone"
|
||||
" FROM main"
|
||||
" join last_name on main.last_name = last_name_id"
|
||||
" join first_name on main.first_name = first_name_id"
|
||||
" join middle_name on main.middle_name = middle_name_id"
|
||||
" join street on main.street = street_id"
|
||||
" WHERE uid = %s", (uid,))
|
||||
contact = cur.fetchone()
|
||||
if contact is None:
|
||||
return {'message': 'Contact not found'}, 404
|
||||
def convert(data):
|
||||
return {
|
||||
'uid': data[0],
|
||||
'last_name': data[1],
|
||||
'first_name': data[2],
|
||||
'middle_name': data[3],
|
||||
'street': data[4],
|
||||
'build': data[5],
|
||||
'build_k': data[6],
|
||||
'apartment': data[7],
|
||||
'telephone': data[8]
|
||||
}
|
||||
contact = convert(contact)
|
||||
return contact
|
||||
|
||||
def delete(self, uid):
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT uid FROM main WHERE uid = %s", (uid,))
|
||||
contact = cur.fetchone()
|
||||
if contact is None:
|
||||
return {'message': 'Contact not found'}, 404
|
||||
cur.execute("DELETE FROM main WHERE uid = %s", (uid,))
|
||||
db.commit()
|
||||
return {'message': 'Contact deleted'}
|
||||
|
||||
def put(self, uid):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('last_name')
|
||||
parser.add_argument('first_name')
|
||||
parser.add_argument('middle_name')
|
||||
parser.add_argument('street')
|
||||
parser.add_argument('build')
|
||||
parser.add_argument('build_k')
|
||||
parser.add_argument('apartment')
|
||||
parser.add_argument('telephone')
|
||||
args = parser.parse_args()
|
||||
|
||||
db = get_db()
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT uid FROM main WHERE uid = %s", (uid,))
|
||||
contact = cur.fetchone()
|
||||
if contact is None:
|
||||
return {'message': 'Contact not found'}, 404
|
||||
|
||||
if args['last_name'] is not None:
|
||||
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
|
||||
last_name = cur.fetchone()
|
||||
if last_name is None:
|
||||
cur.execute("INSERT INTO last_name (last_name_val) VALUES (%s)", (args['last_name'],))
|
||||
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
|
||||
last_name = cur.fetchone()
|
||||
last_name = last_name[0]
|
||||
else:
|
||||
last_name = last_name[0]
|
||||
cur.execute("UPDATE main SET last_name = %s WHERE uid = %s", (last_name, uid))
|
||||
|
||||
if args['first_name'] is not None:
|
||||
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
|
||||
first_name = cur.fetchone()
|
||||
if first_name is None:
|
||||
cur.execute("INSERT INTO first_name (first_name_val) VALUES (%s)", (args['first_name'],))
|
||||
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
|
||||
first_name = cur.fetchone()
|
||||
first_name = first_name[0]
|
||||
else:
|
||||
first_name = first_name[0]
|
||||
cur.execute("UPDATE main SET first_name = %s WHERE uid = %s", (first_name, uid))
|
||||
|
||||
if args['middle_name'] is not None:
|
||||
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
|
||||
middle_name = cur.fetchone()
|
||||
if middle_name is None:
|
||||
cur.execute("INSERT INTO middle_name (middle_name_val) VALUES (%s)", (args['middle_name'],))
|
||||
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
|
||||
middle_name = cur.fetchone()
|
||||
middle_name = middle_name[0]
|
||||
else:
|
||||
middle_name = middle_name[0]
|
||||
cur.execute("UPDATE main SET middle_name = %s WHERE uid = %s", (middle_name, uid))
|
||||
|
||||
if args['street'] is not None:
|
||||
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
|
||||
street = cur.fetchone()
|
||||
if street is None:
|
||||
cur.execute("INSERT INTO street (street_val) VALUES (%s)", (args['street'],))
|
||||
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
|
||||
street = cur.fetchone()
|
||||
street = street[0]
|
||||
else:
|
||||
street = street[0]
|
||||
cur.execute("UPDATE main SET street = %s WHERE uid = %s", (street, uid))
|
||||
|
||||
if args['build'] is not None:
|
||||
cur.execute("UPDATE main SET build = %s WHERE uid = %s", (args['build'], uid))
|
||||
|
||||
if args['build_k'] is not None:
|
||||
cur.execute("UPDATE main SET build_k = %s WHERE uid = %s", (args['build_k'], uid))
|
||||
|
||||
if args['apartment'] is not None:
|
||||
cur.execute("UPDATE main SET apartment = %s WHERE uid = %s", (args['apartment'], uid))
|
||||
|
||||
if args['telephone'] is not None:
|
||||
cur.execute("UPDATE main SET telephone = %s WHERE uid = %s", (args['telephone'], uid))
|
||||
|
||||
db.commit()
|
||||
return {'message': 'Contact updated'}
|
46
contacts/db.py
Normal file
46
contacts/db.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
import psycopg2
|
||||
|
||||
import click
|
||||
from flask import current_app, g
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
def get_db():
|
||||
"""Connect to the application's configured database. The connection
|
||||
is unique for each request and will be reused if this is called
|
||||
again.
|
||||
"""
|
||||
if 'db' not in g:
|
||||
g.db = psycopg2.connect(dbname="postgres", user="postgres", password="postgres", host="db")
|
||||
|
||||
return g.db
|
||||
|
||||
def close_db(e=None):
|
||||
"""If this request connected to the database, close the
|
||||
connection.
|
||||
"""
|
||||
db = g.pop('db', None)
|
||||
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
def init_db():
|
||||
"""Clear existing data and create new tables."""
|
||||
db = get_db()
|
||||
|
||||
with current_app.open_resource('schema.sql') as f:
|
||||
db.cursor().execute(f.read().decode('utf8'))
|
||||
db.commit()
|
||||
|
||||
@click.command('init-db')
|
||||
@with_appcontext
|
||||
def init_db_command():
|
||||
"""Clear existing data and create new tables."""
|
||||
init_db()
|
||||
click.echo('Initialized the database.')
|
||||
|
||||
def init_app(app):
|
||||
"""Register database functions with the Flask app. This is called by
|
||||
the application factory.
|
||||
"""
|
||||
app.teardown_appcontext(close_db)
|
||||
app.cli.add_command(init_db_command)
|
37
contacts/schema.sql
Normal file
37
contacts/schema.sql
Normal file
|
@ -0,0 +1,37 @@
|
|||
DROP TABLE IF EXISTS main;
|
||||
DROP TABLE IF EXISTS first_name;
|
||||
DROP TABLE IF EXISTS last_name;
|
||||
DROP TABLE IF EXISTS middle_name;
|
||||
DROP TABLE IF EXISTS street;
|
||||
CREATE TABLE first_name (
|
||||
first_name_id SERIAL PRIMARY KEY,
|
||||
first_name_val VARCHAR(255) NOT NULL
|
||||
);
|
||||
CREATE TABLE last_name (
|
||||
last_name_id SERIAL PRIMARY KEY,
|
||||
last_name_val VARCHAR(255) NOT NULL
|
||||
);
|
||||
CREATE TABLE middle_name (
|
||||
middle_name_id SERIAL PRIMARY KEY,
|
||||
middle_name_val VARCHAR(255) NOT NULL
|
||||
);
|
||||
CREATE TABLE street (
|
||||
street_id SERIAL PRIMARY KEY,
|
||||
street_val VARCHAR(255) NOT NULL
|
||||
);
|
||||
CREATE TABLE main (
|
||||
uid serial,
|
||||
last_name int,
|
||||
first_name int,
|
||||
middle_name int,
|
||||
street int,
|
||||
build VARCHAR(255),
|
||||
build_k VARCHAR(255),
|
||||
apartment VARCHAR(255),
|
||||
telephone VARCHAR(255) NOT NULL,
|
||||
CONSTRAINT fk_last_name FOREIGN KEY (last_name) REFERENCES last_name(last_name_id),
|
||||
CONSTRAINT fk_first_name FOREIGN KEY (first_name) REFERENCES first_name(first_name_id),
|
||||
CONSTRAINT fk_middle_name FOREIGN KEY (middle_name) REFERENCES middle_name(middle_name_id),
|
||||
CONSTRAINT fk_street FOREIGN KEY (street) REFERENCES street(street_id),
|
||||
PRIMARY KEY (uid)
|
||||
);
|
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
70
frontend/README.md
Normal file
70
frontend/README.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
29172
frontend/package-lock.json
generated
Normal file
29172
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
frontend/package.json
Normal file
46
frontend/package.json
Normal file
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@mui/icons-material": "^5.2.4",
|
||||
"@mui/material": "^5.2.4",
|
||||
"@mui/styles": "^5.2.3",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"mui-datatables": "^4.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-query": "^3.34.3",
|
||||
"react-scripts": "5.0.0",
|
||||
"web-vitals": "^2.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
128
frontend/src/App.jsx
Normal file
128
frontend/src/App.jsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import * as React from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { Container, Dialog, Typography, IconButton, Tooltip } from '@mui/material';
|
||||
import MUIDataTable from "mui-datatables";
|
||||
import { useAxios } from './axios';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import ContactDialog from './ContactDialog';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
function App() {
|
||||
const client = useAxios();
|
||||
const queryClient = useQueryClient()
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [openedContact, setOpenedContact] = React.useState({});
|
||||
const { data, status } = useQuery('contacts', async () => {
|
||||
const {data} = await client.get('/api/contacts');
|
||||
return data.map(({
|
||||
uid,
|
||||
first_name,
|
||||
last_name,
|
||||
middle_name,
|
||||
telephone,
|
||||
street,
|
||||
build,
|
||||
build_k,
|
||||
apartment
|
||||
}) => {
|
||||
return {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'middle_name': middle_name,
|
||||
'telephone': telephone,
|
||||
'street': street,
|
||||
'build': build,
|
||||
'build_k': build_k,
|
||||
'apartment': apartment,
|
||||
'display_name': `${first_name} ${last_name} ${middle_name}`,
|
||||
'display_address': `${street} ${build} ${build_k} ${apartment}`,
|
||||
'uid': uid
|
||||
};
|
||||
})
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
name: "uid", label: "ID", options: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "display_name", label: "Имя"
|
||||
},
|
||||
{
|
||||
name: "telephone", label: "Телефон"
|
||||
},
|
||||
{
|
||||
name: "display_address", label: "Адрес"
|
||||
},
|
||||
];
|
||||
|
||||
const commitDelete = useMutation(async (uid) => {
|
||||
await client.delete(`/api/contacts/${uid}`);
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('contacts');
|
||||
}
|
||||
});
|
||||
|
||||
const handleRowClick = (rowData) => {
|
||||
setOpenedContact(data.find(({uid}) => uid === rowData[0]));
|
||||
setOpen(true);
|
||||
console.log(data.find(({uid}) => uid === rowData[0]));
|
||||
};
|
||||
|
||||
const handleRowsDelete = (rowsDeleted) => {
|
||||
const uids = rowsDeleted.data.map(({dataIndex}) => data[dataIndex]['uid']);
|
||||
for (const uid of uids) {
|
||||
commitDelete.mutate(uid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (data === undefined || status === 'loading') {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<MUIDataTable
|
||||
title={"Контактная книга"}
|
||||
data={data}
|
||||
columns={columns}
|
||||
options={{
|
||||
filterType: "textField",
|
||||
print: false,
|
||||
rowsPerPage: 10,
|
||||
rowsPerPageOptions: [10, 20, 50, 100],
|
||||
onRowClick: handleRowClick,
|
||||
customToolbar: () => {
|
||||
return <CustomToolbar />
|
||||
},
|
||||
onRowsDelete: handleRowsDelete
|
||||
}}
|
||||
/>
|
||||
<ContactDialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
contact={openedContact}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
function CustomToolbar() {
|
||||
if (data !== undefined) return (
|
||||
<Tooltip title='Новый контакт'>
|
||||
<IconButton onClick={() => {
|
||||
setOpenedContact({});
|
||||
setOpen(true);
|
||||
}}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
return (<></>)
|
||||
}
|
||||
}
|
||||
export default App;
|
175
frontend/src/ContactDialog.jsx
Normal file
175
frontend/src/ContactDialog.jsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
import * as React from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from '@mui/material';
|
||||
import MUIDataTable from "mui-datatables";
|
||||
import { useAxios } from './axios';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
|
||||
|
||||
function ContactDialog({
|
||||
open,
|
||||
onClose,
|
||||
contact,
|
||||
}) {
|
||||
const client = useAxios();
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [uid, setUid] = React.useState(contact ? contact.uid : null);
|
||||
const [firstName, setFirstName] = React.useState(contact['first_name'] ?? '');
|
||||
const [lastName, setLastName] = React.useState(contact['last_name'] ?? '');
|
||||
const [middleName, setMiddleName] = React.useState(contact['middle_name'] ?? '');
|
||||
const [telephone, setTelephone] = React.useState(contact['telephone'] ?? '');
|
||||
const [street, setStreet] = React.useState(contact['street'] ?? '');
|
||||
const [build, setBuild] = React.useState(contact['build'] ?? '');
|
||||
const [buildK, setBuildK] = React.useState(contact['build_k'] ?? '');
|
||||
const [apartment, setApartment] = React.useState(contact['apartment'] ?? '');
|
||||
|
||||
const commitCreate = useMutation(async (payload) => client.post('/api/contacts', payload), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('contacts');
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
const commitUpdate = useMutation(async (payload) => client.put(`/api/contacts/${contact['uid']}`, payload), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('contacts');
|
||||
queryClient.invalidateQueries(['contact', contact['uid']]);
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
const handleFormSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const data = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
middle_name: middleName,
|
||||
telephone: telephone,
|
||||
street: street,
|
||||
build: build,
|
||||
build_k: buildK,
|
||||
apartment: apartment
|
||||
};
|
||||
if (contact['uid']) {
|
||||
commitUpdate.mutate(data);
|
||||
}
|
||||
else {
|
||||
commitCreate.mutate(data);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const loadData = () => {
|
||||
console.log(contact);
|
||||
setUid(contact['uid'] ?? null);
|
||||
setFirstName(contact['first_name'] ?? '');
|
||||
setLastName(contact['last_name'] ?? '');
|
||||
setMiddleName(contact['middle_name'] ?? '');
|
||||
setTelephone(contact['telephone'] ?? '');
|
||||
setStreet(contact['street'] ?? '');
|
||||
setBuild(contact['build'] ?? '');
|
||||
setBuildK(contact['build_k'] ?? '');
|
||||
setApartment(contact['apartment'] ?? '');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
TransitionProps={{
|
||||
onEnter: loadData
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Контакт</DialogTitle>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="first_name"
|
||||
label="Имя"
|
||||
value={firstName}
|
||||
onChange={(event) => setFirstName(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="last_name"
|
||||
label="Фамилия"
|
||||
value={lastName}
|
||||
onChange={(event) => setLastName(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="middle_name"
|
||||
label="Отчество"
|
||||
value={middleName}
|
||||
onChange={(event) => setMiddleName(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="telephone"
|
||||
label="Телефон"
|
||||
value={telephone}
|
||||
onChange={(event) => setTelephone(event.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
id="street"
|
||||
label="Улица"
|
||||
value={street}
|
||||
onChange={(event) => setStreet(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<TextField
|
||||
id="build"
|
||||
label="Дом"
|
||||
value={build}
|
||||
onChange={(event) => setBuild(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<TextField
|
||||
id="build_k"
|
||||
label="Корпус"
|
||||
value={buildK}
|
||||
onChange={(event) => setBuildK(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<TextField
|
||||
id="apartment"
|
||||
label="Квартира"
|
||||
value={apartment}
|
||||
onChange={(event) => setApartment(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
<Button type="submit" color="primary">Сохранить</Button>
|
||||
</DialogActions>
|
||||
|
||||
</form>
|
||||
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default ContactDialog;
|
33
frontend/src/axios.js
Normal file
33
frontend/src/axios.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import Axios, { AxiosInstance } from 'axios'
|
||||
|
||||
export const BASE_URL = 'http://localhost:5000'
|
||||
|
||||
export const AxiosContext = createContext(undefined)
|
||||
|
||||
export default function AxiosProvider ({
|
||||
children
|
||||
}) {
|
||||
const axios = useMemo(() => {
|
||||
const axios = Axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
return axios
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AxiosContext.Provider value={axios}> {children} </AxiosContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAxios () {
|
||||
const context = useContext(AxiosContext)
|
||||
if (context !== undefined) {
|
||||
return context
|
||||
}
|
||||
throw Error('useAxios must be called within axios provider!')
|
||||
}
|
27
frontend/src/index.jsx
Normal file
27
frontend/src/index.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import AxiosProvider from './axios';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools'
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const theme = createTheme({
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AxiosProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
||||
</AxiosProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
flask
|
||||
flask_restful
|
||||
flask_socketio
|
||||
pytz
|
||||
psycopg2
|
Loading…
Reference in a new issue