final ass
This commit is contained in:
parent
55f7fe46a1
commit
afa3cce0d3
113
z3/README.md
Normal file
113
z3/README.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Fruit app
|
||||||
|
|
||||||
|
## Overview of the APP
|
||||||
|
|
||||||
|
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
|
||||||
|
|
||||||
|
![homepage](./src/images/homepage.png?raw=true 'System Architecture')
|
||||||
|
|
||||||
|
## Technology used
|
||||||
|
|
||||||
|
- Flask (Backend Rest API)
|
||||||
|
- Angular (Frontend)
|
||||||
|
- MySQL - as persistent database
|
||||||
|
- Azure
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
## prerequist:
|
||||||
|
|
||||||
|
- mysql-client : https://dev.mysql.com/downloads/shell/
|
||||||
|
- az command line.
|
||||||
|
|
||||||
|
## Azure public Cloud :
|
||||||
|
|
||||||
|
Microsoft Azure is a cloud computing service operated by Microsoft, often referred to as Azure. it offers more than 200 cloud products and services designed to solve today’s challenges. We used this cloud to deploy our fruit-app application in the cloud.
|
||||||
|
|
||||||
|
Here is a list of used services:
|
||||||
|
|
||||||
|
- **Resource group**: Azure Resource Groups are logical collections of azure service. it is used to group a collection of services to easy manage them.
|
||||||
|
|
||||||
|
- **Azure App Service**: categorized as Platform as a Service, provides a service that allows you to build, manage, and deploy enterprise-grade scaled web apps without managing the underlying infrastructure.
|
||||||
|
|
||||||
|
- **ACR**: Azure Container registry of Docker images to store our images.
|
||||||
|
|
||||||
|
- **Mysql Database**: One of zure services to create/manage databases (like mysql or postgresql).
|
||||||
|
|
||||||
|
- **Identity Access management(IAM)**: is a web service that helps securely control access to Azure resources. We use IAM to control who is authenticated (signed in) and authorized (has permissions) to use resources.
|
||||||
|
|
||||||
|
## Folder structure:
|
||||||
|
|
||||||
|
Description of the folder structure:
|
||||||
|
|
||||||
|
- **src**: contain the application code
|
||||||
|
- **prepare-app.sh**: Script to build the docker image for the application and deploy the app to the azure cloud using commands
|
||||||
|
- **stop-app.sh** Script to stop and remove the app
|
||||||
|
- **README.md**: Documentation file
|
||||||
|
- **az-docker-compose.yml**: Docker compose for the webapp for azure configuration.
|
||||||
|
|
||||||
|
## Methode of communication:
|
||||||
|
|
||||||
|
For the communication between:
|
||||||
|
|
||||||
|
- Mysql-backend: The credentials for mysql are passed to the backend application as environement variables.
|
||||||
|
|
||||||
|
- frontend-backend: For the connection between the frontend and backend, we used the nginx base image for our frontend image. Also ConfigMap is used to store nginx configuration file to forward all the `/api` request to the backend service.
|
||||||
|
|
||||||
|
## How to prepare, run, pause and delete the application.
|
||||||
|
|
||||||
|
### Prepare the app
|
||||||
|
|
||||||
|
Now to prepare the application you have to run the command :
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
sh prepare-app.sh
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
the script which will create a resource group named 'suhailgroup' and will add all the required services for our application to run.
|
||||||
|
|
||||||
|
### To stop the app
|
||||||
|
|
||||||
|
The `stop-app.sh` script will remove the resources group and all the created services related to that group.
|
||||||
|
|
||||||
|
## Workflow:
|
||||||
|
|
||||||
|
Here is our deployment workflow:
|
||||||
|
|
||||||
|
1 - Build our application image locally.
|
||||||
|
|
||||||
|
2- Creat the resource group to group our application service in order to be able later to manage them as unit.
|
||||||
|
|
||||||
|
3- Creat a azure container registry to store our application image.
|
||||||
|
|
||||||
|
4- Tag the previous built images and push them to the created ACR registry.
|
||||||
|
|
||||||
|
5 - Creat and grand acrpull permission to a Identity service in order to use it later to pull our images from the web app service.
|
||||||
|
|
||||||
|
6- Creating Mysql database and padate the firewall rule to allow external access to database.
|
||||||
|
|
||||||
|
7 - Creat the webapp service for our application( we used the --acr-identity arguement to pass the Identity role we created prevouisly created and a docker-compose to run for the application) and also passing the environment variables for database access to that webapp containers .
|
||||||
|
|
||||||
|
## How to view the application on the web.
|
||||||
|
|
||||||
|
The application is accessible through: `https://suhail5742.azurewebsites.net/`
|
||||||
|
|
||||||
|
## Restart the service on failure:
|
||||||
|
|
||||||
|
In order to have a heigh availibiity for our application, we added the `restart` docker option ,to restart our containers if they exits due to an error:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
restart: on-failure:5
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## resources:
|
||||||
|
|
||||||
|
[Deploy docker image to azure webapp](https://www.azuredevopslabs.com/labs/vstsextend/docker/)
|
||||||
|
[Create web app in azure](https://docs.microsoft.com/en-us/azure/devops/pipelines/apps/cd/deploy-docker-webapp?view=azure-devops&tabs=java%2Cyaml)
|
||||||
|
[Docker compose options for azure](https://docs.microsoft.com/en-us/azure/app-service/configure-custom-container?pivots=container-linux#docker-compose-options)
|
||||||
|
[Azure Mysql Service](https://azure.microsoft.com/en-us/services/mysql/#overview)
|
||||||
|
[Azure web app Commands](https://docs.microsoft.com/en-us/cli/azure/webapp?view=azure-cli-latest)
|
||||||
|
[Azure identity management](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-whatis)
|
21
z3/az-docker-compose.yml
Normal file
21
z3/az-docker-compose.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: suhailregistry1.azurecr.io/fruitapp-frontend:latest
|
||||||
|
restart: on-failure:5
|
||||||
|
backend-service:
|
||||||
|
image: suhailregistry1.azurecr.io/fruitapp-backend:latest
|
||||||
|
restart: on-failure:5
|
||||||
|
env:
|
||||||
|
- name: SQLALCHEMY_DATABASE_URI
|
||||||
|
value: mysql://user:password@mysql-service/main
|
||||||
|
- name: FLASK_APP
|
||||||
|
value: app.py
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
value: main
|
||||||
|
- name: MYSQL_USER
|
||||||
|
value: user
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
value: password
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
value: root
|
2
z3/fruitapp-main/.gitignore
vendored
Normal file
2
z3/fruitapp-main/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.history
|
||||||
|
.idea
|
104
z3/fruitapp-main/Readme.md
Normal file
104
z3/fruitapp-main/Readme.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Overview of the APP
|
||||||
|
|
||||||
|
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
|
||||||
|
|
||||||
|
![homepage](images/homepage.png?raw=true "System Architecture")
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
The basic system requirements are as follows
|
||||||
|
|
||||||
|
- Any OS, preferably Linux
|
||||||
|
- Docker
|
||||||
|
- Docker compose
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
![Architecture](images/fruitapp.jpg?raw=true "System Architecture")
|
||||||
|
|
||||||
|
### Technology used
|
||||||
|
|
||||||
|
- Flask (Backend Rest API)
|
||||||
|
- Angular (Frontend)
|
||||||
|
- MySQL - as persistent database
|
||||||
|
|
||||||
|
# Docker environment
|
||||||
|
|
||||||
|
For shipping and deploying the application docker-compose is used. All the configurations are in the docker-compose.yml file.
|
||||||
|
|
||||||
|
Key points of the docker-compose.yml is given below.
|
||||||
|
|
||||||
|
## services
|
||||||
|
|
||||||
|
- **backend:** runs the Flask web API.
|
||||||
|
- **db** runs the mysql database required for the backend API
|
||||||
|
- **web** this is the front-end application
|
||||||
|
|
||||||
|
## Virtual networks
|
||||||
|
|
||||||
|
One virtual network is used
|
||||||
|
|
||||||
|
- main
|
||||||
|
|
||||||
|
## list of the containers
|
||||||
|
|
||||||
|
### fruitapp-backend
|
||||||
|
|
||||||
|
This container runs under the service name backend. The dockerfile user for this container is located at `fruitapp-backend/Dockerfile`. This container is based on python:3.9 image.
|
||||||
|
|
||||||
|
### fruitapp-frontend
|
||||||
|
|
||||||
|
The frontend container for the application. The dockerfile is located at `fruitapp-frontend/.docker/dev/Dockerfile`. Base image used to build this container is `Node:14`.
|
||||||
|
|
||||||
|
### db container
|
||||||
|
|
||||||
|
The container is built using the official `mysql:5.7.22` image pulled from dockerhub.
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
To prepare the environment for the first time run the following command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Run the app background
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash start-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of backend
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f backend
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of forntend
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of database
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stopping
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash stop-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing containers
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash remove-app.sh
|
||||||
|
```
|
||||||
|
# Viewing the app
|
||||||
|
|
||||||
|
After running the app. visit [http://localhost:4200](http://localhost:4200) to view the app
|
55
z3/fruitapp-main/docker-compose.yml
Normal file
55
z3/fruitapp-main/docker-compose.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./fruitapp-backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: fruitapp-backend
|
||||||
|
env_file:
|
||||||
|
- ./fruitapp-backend/.env
|
||||||
|
command: "bash scripts/entrypoint.sh"
|
||||||
|
ports:
|
||||||
|
- "8001:5000"
|
||||||
|
volumes:
|
||||||
|
- ./fruitapp-backend:/app
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:5.7.22
|
||||||
|
container_name: fruitapp-db
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./fruitapp-backend/.env
|
||||||
|
volumes:
|
||||||
|
- msql-data:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- "33067:3306"
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ./fruitapp-frontend
|
||||||
|
dockerfile: .docker/dev/Dockerfile
|
||||||
|
container_name: fruitapp-frontend
|
||||||
|
ports:
|
||||||
|
- 4200:4200
|
||||||
|
volumes:
|
||||||
|
- ./fruitapp-frontend:/app
|
||||||
|
command: >
|
||||||
|
bash -c "cp -rfu /cache/node_modules/. /app/node_modules/ && ng serve --host=0.0.0.0 --aot"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
msql-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
main:
|
8
z3/fruitapp-main/fruitapp-backend/.env
Normal file
8
z3/fruitapp-main/fruitapp-backend/.env
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
MYSQL_DATABASE=main
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=root
|
||||||
|
MYSQL_ROOT_PASSWORD=root
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI=mysql://root:root@db/main
|
||||||
|
|
||||||
|
FLASK_APP=app.py
|
6
z3/fruitapp-main/fruitapp-backend/.env.template
Normal file
6
z3/fruitapp-main/fruitapp-backend/.env.template
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
MYSQL_DATABASE=main
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=root
|
||||||
|
MYSQL_ROOT_PASSWORD=root
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI=mysql://root:root@db/main
|
25
z3/fruitapp-main/fruitapp-backend/.gitignore
vendored
Normal file
25
z3/fruitapp-main/fruitapp-backend/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.DS_Store
|
||||||
|
.flaskenv
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env*
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
_mailinglist
|
||||||
|
.tox/
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.idea/
|
||||||
|
docs/_build/
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
*,cover
|
8
z3/fruitapp-main/fruitapp-backend/Dockerfile
Normal file
8
z3/fruitapp-main/fruitapp-backend/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY ./scripts/entrypoint.sh /app/scripts/entrypoint.sh
|
||||||
|
COPY . /app
|
2
z3/fruitapp-main/fruitapp-backend/config.py
Normal file
2
z3/fruitapp-main/fruitapp-backend/config.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
54
z3/fruitapp-main/fruitapp-backend/main.py
Normal file
54
z3/fruitapp-main/fruitapp-backend/main.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, abort
|
||||||
|
from flask_cors import CORS
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
||||||
|
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Product(db.Model):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
image: str
|
||||||
|
likes: int
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||||
|
title = db.Column(db.String(200))
|
||||||
|
image = db.Column(db.TEXT(20000000))
|
||||||
|
likes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProductUser(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer)
|
||||||
|
product_id = db.Column(db.Integer)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/products')
|
||||||
|
def index():
|
||||||
|
return jsonify(Product.query.all())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/products/<int:id>/like', methods=['POST'])
|
||||||
|
def like(id):
|
||||||
|
# define a static user id for demo
|
||||||
|
try:
|
||||||
|
product = Product.query.get(id)
|
||||||
|
product.likes += 1
|
||||||
|
db.session.commit()
|
||||||
|
except:
|
||||||
|
abort(400, 'bad request')
|
||||||
|
|
||||||
|
return jsonify(product)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0')
|
12
z3/fruitapp-main/fruitapp-backend/manage.py
Normal file
12
z3/fruitapp-main/fruitapp-backend/manage.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
|
from flask_script import Manager
|
||||||
|
|
||||||
|
from main import app, db
|
||||||
|
|
||||||
|
migrate = Migrate(app, db, compare_type=True)
|
||||||
|
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
1
z3/fruitapp-main/fruitapp-backend/migrations/README
Normal file
1
z3/fruitapp-main/fruitapp-backend/migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
45
z3/fruitapp-main/fruitapp-backend/migrations/alembic.ini
Normal file
45
z3/fruitapp-main/fruitapp-backend/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
24
z3/fruitapp-main/fruitapp-backend/migrations/script.py.mako
Normal file
24
z3/fruitapp-main/fruitapp-backend/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
@ -0,0 +1,40 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 843c810aec1f
|
||||||
|
Revises:
|
||||||
|
Create Date: 2020-12-08 08:40:37.736465
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '843c810aec1f'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('product',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('image', sa.String(length=200), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('product_user',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('product_user')
|
||||||
|
op.drop_table('product')
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 85749c0c4589
|
||||||
|
Revises: b3ff59df2833
|
||||||
|
Create Date: 2022-04-08 07:46:57.160792
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '85749c0c4589'
|
||||||
|
down_revision = 'b3ff59df2833'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=mysql.VARCHAR(length=20000),
|
||||||
|
type_=sa.TEXT(length=20000000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=sa.TEXT(length=20000000),
|
||||||
|
type_=mysql.VARCHAR(length=20000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: b3ff59df2833
|
||||||
|
Revises: fee4d1b1d192
|
||||||
|
Create Date: 2022-04-08 07:33:52.082355
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b3ff59df2833'
|
||||||
|
down_revision = 'fee4d1b1d192'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=mysql.VARCHAR(length=200),
|
||||||
|
type_=sa.String(length=20000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=sa.String(length=20000),
|
||||||
|
type_=mysql.VARCHAR(length=200),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: fee4d1b1d192
|
||||||
|
Revises: 843c810aec1f
|
||||||
|
Create Date: 2022-04-08 07:05:04.647143
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fee4d1b1d192'
|
||||||
|
down_revision = '843c810aec1f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('product', sa.Column('likes', sa.Integer(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('product', 'likes')
|
||||||
|
# ### end Alembic commands ###
|
27
z3/fruitapp-main/fruitapp-backend/producer.py
Normal file
27
z3/fruitapp-main/fruitapp-backend/producer.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pika
|
||||||
|
|
||||||
|
RABBIT_ENDPOINT = os.environ['RABBIT_ENDPOINT']
|
||||||
|
|
||||||
|
|
||||||
|
class Producer:
|
||||||
|
"""Implement the producer logic."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.params = pika.URLParameters(RABBIT_ENDPOINT)
|
||||||
|
self.connection = pika.BlockingConnection(self.params)
|
||||||
|
|
||||||
|
def publish(self, method, body):
|
||||||
|
properties = pika.BasicProperties(method)
|
||||||
|
if not self.connection or self.connection.is_closed:
|
||||||
|
self.connection = pika.BlockingConnection(self.params)
|
||||||
|
channel = self.connection.channel()
|
||||||
|
channel.basic_publish(
|
||||||
|
exchange='',
|
||||||
|
routing_key='admin',
|
||||||
|
body=json.dumps(body),
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
print('Product published in queue: MAIN')
|
13
z3/fruitapp-main/fruitapp-backend/requirements.txt
Normal file
13
z3/fruitapp-main/fruitapp-backend/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Flask==1.1.2
|
||||||
|
werkzeug==1.0.1
|
||||||
|
Flask-SQLAlchemy==2.4.4
|
||||||
|
SQLAlchemy==1.3.20
|
||||||
|
Flask-Migrate==2.5.3
|
||||||
|
Flask-Script==2.0.6
|
||||||
|
Flask-Cors==3.0.9
|
||||||
|
requests==2.25.0
|
||||||
|
mysqlclient==2.0.1
|
||||||
|
pika==1.1.0
|
||||||
|
jinja2<3.1.0
|
||||||
|
itsdangerous==2.0.1
|
||||||
|
python-dotenv==0.20.0
|
5
z3/fruitapp-main/fruitapp-backend/scripts/entrypoint.sh
Normal file
5
z3/fruitapp-main/fruitapp-backend/scripts/entrypoint.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python manage.py db upgrade
|
||||||
|
echo "DB migration done."
|
||||||
|
python seeder.py
|
||||||
|
python main.py
|
40
z3/fruitapp-main/fruitapp-backend/seeder.py
Normal file
40
z3/fruitapp-main/fruitapp-backend/seeder.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from main import Product, db
|
||||||
|
|
||||||
|
|
||||||
|
def insert_data(data):
|
||||||
|
product = Product.query.get(data['id'])
|
||||||
|
if not product:
|
||||||
|
product = Product(id=data['id'], title=data['title'], image=data['image'])
|
||||||
|
db.session.add(product)
|
||||||
|
db.session.commit()
|
||||||
|
print('inserted ', data['title'])
|
||||||
|
else:
|
||||||
|
print('Product already exists.')
|
||||||
|
|
||||||
|
|
||||||
|
PRODUCTS = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'title': 'Apple',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/red-apple-picture-id184276818?k=20&m=184276818&s=612x612&w=0&h=QxOcueqAUVTdiJ7DVoCu-BkNCIuwliPEgtAQhgvBA_g='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'title': 'Orange',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/tiny-tangerine-picture-id89951356?k=20&m=89951356&s=170667a&w=0&h=9oW2EBTASfaBol8qYVYB8YNYcSoNe1AImG4wqdgErEs='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'title': 'Banana',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/banana-picture-id1184345169?k=20&m=1184345169&s=170667a&w=0&h=t22KqOZ9EEwyRj7i35uxY-Xf6P_gAgLejd-SeReTnPY='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'title': 'Avocado',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/half-of-fresh-ripe-avocado-isolated-on-white-background-picture-id1278032327?b=1&k=20&m=1278032327&s=170667a&w=0&h=7eMAI9S_YBiVaDN49zk5gUZEhhA1oGDIWh3Tdst6q4Y='
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in PRODUCTS:
|
||||||
|
insert_data(p)
|
||||||
|
|
17
z3/fruitapp-main/fruitapp-frontend/.browserslistrc
Normal file
17
z3/fruitapp-main/fruitapp-frontend/.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
15
z3/fruitapp-main/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
15
z3/fruitapp-main/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:14
|
||||||
|
|
||||||
|
RUN npm install @angular/cli@11.0.0 -g
|
||||||
|
|
||||||
|
# Create and define the node_modules's cache directory.
|
||||||
|
WORKDIR /cache
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY package-lock.json .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# define the application's working directory.
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
ENV PATH /app/node_modules/.bin:$PATH
|
9
z3/fruitapp-main/fruitapp-frontend/.dockerignore
Normal file
9
z3/fruitapp-main/fruitapp-frontend/.dockerignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
.history
|
16
z3/fruitapp-main/fruitapp-frontend/.editorconfig
Normal file
16
z3/fruitapp-main/fruitapp-frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
46
z3/fruitapp-main/fruitapp-frontend/.gitignore
vendored
Normal file
46
z3/fruitapp-main/fruitapp-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
27
z3/fruitapp-main/fruitapp-frontend/README.md
Normal file
27
z3/fruitapp-main/fruitapp-frontend/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# DockerizeAngular
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.2.2.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
130
z3/fruitapp-main/fruitapp-frontend/angular.json
Normal file
130
z3/fruitapp-main/fruitapp-frontend/angular.json
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"cli": {
|
||||||
|
"analytics": "d44c8c58-a3f9-4508-9e41-039ac5874480"
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"dockerize-angular": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:application": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/dockerize-angular",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"aot": true,
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [ "./node_modules/@angular/material/prebuilt-themes/purple-green.css"],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "dockerize-angular:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "dockerize-angular:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "dockerize-angular:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"tsconfig.app.json",
|
||||||
|
"tsconfig.spec.json",
|
||||||
|
"e2e/tsconfig.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "dockerize-angular:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "dockerize-angular:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "dockerize-angular"
|
||||||
|
}
|
37
z3/fruitapp-main/fruitapp-frontend/e2e/protractor.conf.js
Normal file
37
z3/fruitapp-main/fruitapp-frontend/e2e/protractor.conf.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("protractor").Config }
|
||||||
|
*/
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
browserName: 'chrome'
|
||||||
|
},
|
||||||
|
directConnect: true,
|
||||||
|
SELENIUM_PROMISE_MANAGER: false,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 30000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({
|
||||||
|
spec: {
|
||||||
|
displayStacktrace: StacktraceOption.PRETTY
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
23
z3/fruitapp-main/fruitapp-frontend/e2e/src/app.e2e-spec.ts
Normal file
23
z3/fruitapp-main/fruitapp-frontend/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { browser, logging } from 'protractor';
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('workspace-project App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display welcome message', async () => {
|
||||||
|
await page.navigateTo();
|
||||||
|
expect(await page.getTitleText()).toEqual('dockerize-angular app is running!');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Assert that there are no errors emitted from the browser
|
||||||
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
expect(logs).not.toContain(jasmine.objectContaining({
|
||||||
|
level: logging.Level.SEVERE,
|
||||||
|
} as logging.Entry));
|
||||||
|
});
|
||||||
|
});
|
11
z3/fruitapp-main/fruitapp-frontend/e2e/src/app.po.ts
Normal file
11
z3/fruitapp-main/fruitapp-frontend/e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
async navigateTo(): Promise<unknown> {
|
||||||
|
return browser.get(browser.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTitleText(): Promise<string> {
|
||||||
|
return element(by.css('app-root .content span')).getText();
|
||||||
|
}
|
||||||
|
}
|
13
z3/fruitapp-main/fruitapp-frontend/e2e/tsconfig.json
Normal file
13
z3/fruitapp-main/fruitapp-frontend/e2e/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2018",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
57
z3/fruitapp-main/fruitapp-frontend/karma.conf.js
Normal file
57
z3/fruitapp-main/fruitapp-frontend/karma.conf.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
customLaunchers: {
|
||||||
|
ChromeHeadless: {
|
||||||
|
base: 'Chrome',
|
||||||
|
flags: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--headless',
|
||||||
|
'--remote-debugging-port=9222'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
basePath: '',
|
||||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
jasmine: {
|
||||||
|
// you can add configuration options for Jasmine here
|
||||||
|
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||||
|
// for example, you can disable the random execution with `random: false`
|
||||||
|
// or set a specific seed with `seed: 4321`
|
||||||
|
},
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
jasmineHtmlReporter: {
|
||||||
|
suppressAll: true // removes the duplicated traces
|
||||||
|
},
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/dockerize-angular'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [
|
||||||
|
{ type: 'html' },
|
||||||
|
{ type: 'text-summary' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
13539
z3/fruitapp-main/fruitapp-frontend/package-lock.json
generated
Normal file
13539
z3/fruitapp-main/fruitapp-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
z3/fruitapp-main/fruitapp-frontend/package.json
Normal file
49
z3/fruitapp-main/fruitapp-frontend/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "dockerize-angular",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~11.2.3",
|
||||||
|
"@angular/cdk": "^11.2.13",
|
||||||
|
"@angular/common": "~11.2.3",
|
||||||
|
"@angular/compiler": "~11.2.3",
|
||||||
|
"@angular/core": "~11.2.3",
|
||||||
|
"@angular/forms": "~11.2.3",
|
||||||
|
"@angular/localize": "~11.2.3",
|
||||||
|
"@angular/material": "^11.2.13",
|
||||||
|
"@angular/platform-browser": "~11.2.3",
|
||||||
|
"@angular/platform-browser-dynamic": "~11.2.3",
|
||||||
|
"@angular/router": "~11.2.3",
|
||||||
|
"hello-world-npm": "^1.1.1",
|
||||||
|
"rxjs": "~6.6.0",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"zone.js": "~0.11.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~0.1102.2",
|
||||||
|
"@angular/cli": "~11.2.2",
|
||||||
|
"@angular/compiler-cli": "~11.2.3",
|
||||||
|
"@types/jasmine": "~3.6.0",
|
||||||
|
"@types/node": "^12.11.1",
|
||||||
|
"codelyzer": "^6.0.0",
|
||||||
|
"jasmine-core": "~3.6.0",
|
||||||
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
|
"karma": "~6.1.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.0.3",
|
||||||
|
"karma-jasmine": "~4.0.0",
|
||||||
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
|
"protractor": "~7.0.0",
|
||||||
|
"ts-node": "~8.3.0",
|
||||||
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "~4.1.2"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||||
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have as title 'dockerize-angular'`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('dockerize-angular');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement;
|
||||||
|
expect(compiled.querySelector('.content span').textContent).toContain('dockerize-angular app is running!');
|
||||||
|
});
|
||||||
|
});
|
10
z3/fruitapp-main/fruitapp-frontend/src/app/app.component.ts
Normal file
10
z3/fruitapp-main/fruitapp-frontend/src/app/app.component.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'dockerize-angular';
|
||||||
|
}
|
35
z3/fruitapp-main/fruitapp-frontend/src/app/app.module.ts
Normal file
35
z3/fruitapp-main/fruitapp-frontend/src/app/app.module.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { HttpErrorHandler } from './service/http-error-handler.service';
|
||||||
|
import { MessageService } from './service/message.service';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
HttpClientModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatCardModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
HttpErrorHandler,
|
||||||
|
MessageService,
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
@ -0,0 +1,21 @@
|
|||||||
|
.product-card-wrapper{
|
||||||
|
width: 1112px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card{
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-card{
|
||||||
|
background: #fff;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<div class="product-card-wrapper">
|
||||||
|
<div class="card" *ngFor="let p of products">
|
||||||
|
<mat-card class="example-card">
|
||||||
|
<mat-card-title style="margin-bottom: 15px; font-size: 18px;">{{ p.title }}</mat-card-title>
|
||||||
|
<mat-card-content style="margin-bottom: 0;">
|
||||||
|
<img src="{{ p.image }}" height="180" />
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions>
|
||||||
|
<button mat-button (click)="likeProduct(p.id)" style="display: flex; align-items: center;">
|
||||||
|
<mat-icon aria-hidden="false" aria-label="Example home icon">thumb_up</mat-icon>
|
||||||
|
<span style="margin-left: 10px;">{{ p.likes }}</span>
|
||||||
|
</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DashboardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,56 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { Product } from '../models/product';
|
||||||
|
import { DashboardService } from '../service/dashboard.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.css']
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements OnInit {
|
||||||
|
|
||||||
|
products: Product[] = [];
|
||||||
|
constructor(private dashboardService: DashboardService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
console.log('dashboard');
|
||||||
|
this.getProducts();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getProducts() {
|
||||||
|
this.dashboardService.getProducts()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
data => {
|
||||||
|
this.products = data;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
likeProduct(id: number) {
|
||||||
|
console.log('like clicked ...........');
|
||||||
|
this.dashboardService.likeProduct(id)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
data => {
|
||||||
|
this.products.forEach(product => {
|
||||||
|
if (product.id == data.id) {
|
||||||
|
product.likes = data.likes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
|||||||
|
export class Product {
|
||||||
|
id!: number;
|
||||||
|
title!: string;
|
||||||
|
image!: string;
|
||||||
|
likes!: number;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { HandleError, HttpErrorHandler } from './http-error-handler.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class DashboardService {
|
||||||
|
|
||||||
|
apiUrl = environment.apiUrl;
|
||||||
|
private handleError: HandleError;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, httpErrorHandler: HttpErrorHandler) {
|
||||||
|
this.handleError = httpErrorHandler.createHandleError('DashboardService');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProducts(): Observable<any> {
|
||||||
|
return this.http.get<any>(this.apiUrl + '/api/products')
|
||||||
|
.pipe(
|
||||||
|
catchError(this.handleError('getToDo', []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
likeProduct(id: number): Observable<any> {
|
||||||
|
return this.http.post<any>(this.apiUrl + `/api/products/${id}/like`, null)
|
||||||
|
.pipe(
|
||||||
|
catchError(this.handleError('like'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { MessageService } from './message.service';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
|
||||||
|
export type HandleError =
|
||||||
|
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
|
||||||
|
|
||||||
|
/** Handles HttpClient errors */
|
||||||
|
@Injectable()
|
||||||
|
export class HttpErrorHandler {
|
||||||
|
constructor(private messageService: MessageService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create curried handleError function that already knows the service name */
|
||||||
|
createHandleError = (serviceName = '') => <T>
|
||||||
|
(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that handles Http operation failures.
|
||||||
|
* This error handler lets the app continue to run as if no error occurred.
|
||||||
|
* @param serviceName = name of the data service that attempted the operation
|
||||||
|
* @param operation - name of the operation that failed
|
||||||
|
* @param result - optional value to return as the observable result
|
||||||
|
*/
|
||||||
|
handleError<T>(serviceName = '', operation = 'operation', result = {} as T) {
|
||||||
|
|
||||||
|
return (error: HttpErrorResponse): Observable<T> => {
|
||||||
|
// TODO: send the error to remote logging infrastructure
|
||||||
|
console.log(error); // log to console instead
|
||||||
|
const message = (error.error instanceof ErrorEvent) ?
|
||||||
|
error.error.message :
|
||||||
|
error.message;
|
||||||
|
|
||||||
|
// TODO: better job of transforming error for user consumption
|
||||||
|
this.messageService.add(message);
|
||||||
|
|
||||||
|
// Let the app keep running by returning a safe result.
|
||||||
|
return throwError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessageService {
|
||||||
|
messages: string[] = [];
|
||||||
|
messageType = '';
|
||||||
|
|
||||||
|
add(message: string) {
|
||||||
|
if (message && message !== '') {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true
|
||||||
|
};
|
@ -0,0 +1,17 @@
|
|||||||
|
// This file can be replaced during build by using the `fileReplacements` array.
|
||||||
|
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||||
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiUrl: "http://localhost:8001"
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For easier debugging in development mode, you can import the following file
|
||||||
|
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||||
|
*
|
||||||
|
* This import should be commented out in production mode because it will have a negative impact
|
||||||
|
* on performance if an error is thrown.
|
||||||
|
*/
|
||||||
|
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
BIN
z3/fruitapp-main/fruitapp-frontend/src/favicon.ico
Normal file
BIN
z3/fruitapp-main/fruitapp-frontend/src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 948 B |
16
z3/fruitapp-main/fruitapp-frontend/src/index.html
Normal file
16
z3/fruitapp-main/fruitapp-frontend/src/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Main</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="mat-typography">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
z3/fruitapp-main/fruitapp-frontend/src/main.ts
Normal file
12
z3/fruitapp-main/fruitapp-frontend/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
69
z3/fruitapp-main/fruitapp-frontend/src/polyfills.ts
Normal file
69
z3/fruitapp-main/fruitapp-frontend/src/polyfills.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/***************************************************************************************************
|
||||||
|
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
|
||||||
|
*/
|
||||||
|
import '@angular/localize/init';
|
||||||
|
/**
|
||||||
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||||
|
* You can add your own extra polyfills to this file.
|
||||||
|
*
|
||||||
|
* This file is divided into 2 sections:
|
||||||
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||||
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
|
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||||
|
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||||
|
*
|
||||||
|
* Learn more in https://angular.io/guide/browser-support
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* BROWSER POLYFILLS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IE11 requires the following for NgClass support on SVG elements
|
||||||
|
*/
|
||||||
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Animations `@angular/platform-browser/animations`
|
||||||
|
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||||
|
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||||
|
*/
|
||||||
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||||
|
* will put import in the top of bundle, so user need to create a separate file
|
||||||
|
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||||
|
* into that file, and then add the following code before importing zone.js.
|
||||||
|
* import './zone-flags';
|
||||||
|
*
|
||||||
|
* The flags allowed in zone-flags.ts are listed here.
|
||||||
|
*
|
||||||
|
* The following flags will work for all browsers.
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||||
|
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||||
|
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||||
|
*
|
||||||
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||||
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||||
|
*
|
||||||
|
* (window as any).__Zone_enable_cross_context_check = true;
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Zone JS is required by default for Angular itself.
|
||||||
|
*/
|
||||||
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* APPLICATION IMPORTS
|
||||||
|
*/
|
2
z3/fruitapp-main/fruitapp-frontend/src/styles.css
Normal file
2
z3/fruitapp-main/fruitapp-frontend/src/styles.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
|
25
z3/fruitapp-main/fruitapp-frontend/src/test.ts
Normal file
25
z3/fruitapp-main/fruitapp-frontend/src/test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||||
|
|
||||||
|
import 'zone.js/dist/zone-testing';
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting
|
||||||
|
} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
declare const require: {
|
||||||
|
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||||
|
keys(): string[];
|
||||||
|
<T>(id: string): T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting()
|
||||||
|
);
|
||||||
|
// Then we find all the tests.
|
||||||
|
const context = require.context('./', true, /\.spec\.ts$/);
|
||||||
|
// And load the modules.
|
||||||
|
context.keys().map(context);
|
15
z3/fruitapp-main/fruitapp-frontend/tsconfig.app.json
Normal file
15
z3/fruitapp-main/fruitapp-frontend/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
32
z3/fruitapp-main/fruitapp-frontend/tsconfig.json
Normal file
32
z3/fruitapp-main/fruitapp-frontend/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "es2015",
|
||||||
|
"module": "es2020",
|
||||||
|
"lib": [
|
||||||
|
"es2018",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"strictPropertyInitialization": false
|
||||||
|
|
||||||
|
}
|
18
z3/fruitapp-main/fruitapp-frontend/tsconfig.spec.json
Normal file
18
z3/fruitapp-main/fruitapp-frontend/tsconfig.spec.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/test.ts",
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
152
z3/fruitapp-main/fruitapp-frontend/tslint.json
Normal file
152
z3/fruitapp-main/fruitapp-frontend/tslint.json
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"extends": "tslint:recommended",
|
||||||
|
"rulesDirectory": [
|
||||||
|
"codelyzer"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"align": {
|
||||||
|
"options": [
|
||||||
|
"parameters",
|
||||||
|
"statements"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"array-type": false,
|
||||||
|
"arrow-return-shorthand": true,
|
||||||
|
"curly": true,
|
||||||
|
"deprecation": {
|
||||||
|
"severity": "warning"
|
||||||
|
},
|
||||||
|
"eofline": true,
|
||||||
|
"import-blacklist": [
|
||||||
|
true,
|
||||||
|
"rxjs/Rx"
|
||||||
|
],
|
||||||
|
"import-spacing": true,
|
||||||
|
"indent": {
|
||||||
|
"options": [
|
||||||
|
"spaces"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"max-classes-per-file": false,
|
||||||
|
"max-line-length": [
|
||||||
|
true,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
"member-ordering": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"order": [
|
||||||
|
"static-field",
|
||||||
|
"instance-field",
|
||||||
|
"static-method",
|
||||||
|
"instance-method"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-console": [
|
||||||
|
true,
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"time",
|
||||||
|
"timeEnd",
|
||||||
|
"trace"
|
||||||
|
],
|
||||||
|
"no-empty": false,
|
||||||
|
"no-inferrable-types": [
|
||||||
|
true,
|
||||||
|
"ignore-params"
|
||||||
|
],
|
||||||
|
"no-non-null-assertion": true,
|
||||||
|
"no-redundant-jsdoc": true,
|
||||||
|
"no-switch-case-fall-through": true,
|
||||||
|
"no-var-requires": false,
|
||||||
|
"object-literal-key-quotes": [
|
||||||
|
true,
|
||||||
|
"as-needed"
|
||||||
|
],
|
||||||
|
"quotemark": [
|
||||||
|
true,
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"semicolon": {
|
||||||
|
"options": [
|
||||||
|
"always"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"space-before-function-paren": {
|
||||||
|
"options": {
|
||||||
|
"anonymous": "never",
|
||||||
|
"asyncArrow": "always",
|
||||||
|
"constructor": "never",
|
||||||
|
"method": "never",
|
||||||
|
"named": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typedef": [
|
||||||
|
true,
|
||||||
|
"call-signature"
|
||||||
|
],
|
||||||
|
"typedef-whitespace": {
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"call-signature": "nospace",
|
||||||
|
"index-signature": "nospace",
|
||||||
|
"parameter": "nospace",
|
||||||
|
"property-declaration": "nospace",
|
||||||
|
"variable-declaration": "nospace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"call-signature": "onespace",
|
||||||
|
"index-signature": "onespace",
|
||||||
|
"parameter": "onespace",
|
||||||
|
"property-declaration": "onespace",
|
||||||
|
"variable-declaration": "onespace"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable-name": {
|
||||||
|
"options": [
|
||||||
|
"ban-keywords",
|
||||||
|
"check-format",
|
||||||
|
"allow-pascal-case"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"whitespace": {
|
||||||
|
"options": [
|
||||||
|
"check-branch",
|
||||||
|
"check-decl",
|
||||||
|
"check-operator",
|
||||||
|
"check-separator",
|
||||||
|
"check-type",
|
||||||
|
"check-typecast"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"component-class-suffix": true,
|
||||||
|
"contextual-lifecycle": true,
|
||||||
|
"directive-class-suffix": true,
|
||||||
|
"no-conflicting-lifecycle": true,
|
||||||
|
"no-host-metadata-property": true,
|
||||||
|
"no-input-rename": true,
|
||||||
|
"no-inputs-metadata-property": true,
|
||||||
|
"no-output-native": true,
|
||||||
|
"no-output-on-prefix": true,
|
||||||
|
"no-output-rename": true,
|
||||||
|
"no-outputs-metadata-property": true,
|
||||||
|
"template-banana-in-box": true,
|
||||||
|
"template-no-negated-async": true,
|
||||||
|
"use-lifecycle-interface": true,
|
||||||
|
"use-pipe-transform-interface": true,
|
||||||
|
"directive-selector": [
|
||||||
|
true,
|
||||||
|
"attribute",
|
||||||
|
"app",
|
||||||
|
"camelCase"
|
||||||
|
],
|
||||||
|
"component-selector": [
|
||||||
|
true,
|
||||||
|
"element",
|
||||||
|
"app",
|
||||||
|
"kebab-case"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
z3/fruitapp-main/images/fruitapp.jpg
Normal file
BIN
z3/fruitapp-main/images/fruitapp.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
z3/fruitapp-main/images/homepage.png
Normal file
BIN
z3/fruitapp-main/images/homepage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 209 KiB |
5
z3/fruitapp-main/prepare-app.sh
Normal file
5
z3/fruitapp-main/prepare-app.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Preparing the app.."
|
||||||
|
docker-compose build
|
||||||
|
echo "Preparation complete."
|
14
z3/fruitapp-main/remove-app.sh
Normal file
14
z3/fruitapp-main/remove-app.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Removing the app.."
|
||||||
|
echo "Removing the fruitapp-backend"
|
||||||
|
docker stop fruitapp-backend && docker rm -f fruitapp-backend
|
||||||
|
echo ""
|
||||||
|
echo "removing the fruitapp-web container"
|
||||||
|
docker stop fruitapp-frontend && docker rm -f fruitapp-frontend
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "removing the fruitapp-db container"
|
||||||
|
docker stop fruitapp-db && docker rm -f fruitapp-db
|
||||||
|
|
||||||
|
echo "App removed."
|
7
z3/fruitapp-main/start-app.sh
Normal file
7
z3/fruitapp-main/start-app.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "running the app"
|
||||||
|
docker-compose up -d
|
||||||
|
echo "Connecting to the database....Please wait."
|
||||||
|
sleep 10
|
||||||
|
echo "The app is available at http://localhost:4200"
|
5
z3/fruitapp-main/stop-app.sh
Normal file
5
z3/fruitapp-main/stop-app.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "stopping the app.."
|
||||||
|
docker-compose stop
|
||||||
|
echo "App stopped"
|
BIN
z3/images/homepage.png
Normal file
BIN
z3/images/homepage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 209 KiB |
53
z3/prepare-app.sh
Normal file
53
z3/prepare-app.sh
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
suhailregistry='suhailregistry1'
|
||||||
|
mysql_service='mysql-db-f39m'
|
||||||
|
app_name='suhail742'
|
||||||
|
echo "Preparing the app"
|
||||||
|
echo "Creating docker images"
|
||||||
|
docker build ./src/fruitapp-backend -t fruitapp-backend
|
||||||
|
docker build ./src/fruitapp-frontend/ -f ./src/fruitapp-frontend/.docker/dev/Dockerfile -t fruitapp-frontend
|
||||||
|
echo 'starting the app'
|
||||||
|
az group create --name suhailgroup --location westeurope
|
||||||
|
az acr create --resource-group suhailgroup --name $suhailregistry --sku basic
|
||||||
|
az acr login --name $suhailregistry
|
||||||
|
docker tag fruitapp-backend:latest $suhailregistry.azurecr.io/fruitapp-backend:latest
|
||||||
|
docker tag fruitapp-frontend:latest $suhailregistry.azurecr.io/fruitapp-frontend:latest
|
||||||
|
docker push $suhailregistry.azurecr.io/fruitapp-backend:latest
|
||||||
|
docker push $suhailregistry.azurecr.io/fruitapp-frontend:latest
|
||||||
|
|
||||||
|
|
||||||
|
az appservice plan create --name myserviceplan -g suhailgroup --sku S1 --is-linux
|
||||||
|
az webapp create -g suhailgroup -p myserviceplan --name $app_name \
|
||||||
|
--multicontainer-config-type compose --multicontainer-config-file az-docker-compose.yml
|
||||||
|
|
||||||
|
principalId=$(az webapp identity assign --resource-group suhailgroup --name $app_name --query principalId --output tsv)
|
||||||
|
subscriptionId=$(az account show --query id --output tsv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
az mysql server create -g suhailgroup --name $mysql_service --location eastus --admin-user adminuser --admin-password My5up3rStr0ngPaSw0rd! --sku-name B_Gen5_1 --version 5.7 --ssl-enforcement Disabled
|
||||||
|
|
||||||
|
myIp=$(curl -s api.ipify.org)
|
||||||
|
az mysql server firewall-rule create -g suhailgroup --server $mysql_service --name AllowMyIP --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0
|
||||||
|
az mysql server firewall-rule create -g suhailgroup --server $mysql_service --name myIP --start-ip-address $myIp --end-ip-address $myIp
|
||||||
|
|
||||||
|
mysql -h $mysql_service.mysql.database.azure.com -u adminuser@$mysql_service -pMy5up3rStr0ngPaSw0rd! -e "create database main;"
|
||||||
|
|
||||||
|
az role assignment create --assignee $principalId \
|
||||||
|
--scope /subscriptions/$subscriptionId/resourceGroups/suhailgroup/providers/Microsoft.ContainerRegistry/registries/$suhailregistry \
|
||||||
|
--role acrpull
|
||||||
|
|
||||||
|
az webapp config appsettings set --name $app_name -g suhailgroup --settings SQLALCHEMY_DATABASE_URI='mysql://adminuser@$mysql_service:My5up3rStr0ngPaSw0rd!@$mysql_service.mysql.database.azure.com/main' \
|
||||||
|
MYSQL_USER='adminuser@$mysql_service' MYSQL_PASSWORD='My5up3rStr0ngPaSw0rd!' MYSQL_ROOT_PASSWORD='My5up3rStr0ngPaSw0rd!'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
az resource update --ids /subscriptions/$subscriptionId/resourceGroups/suhailgroup/providers/Microsoft.Web/sites/$app_name/config/web --set properties.acrUseManagedIdentityCreds=True
|
||||||
|
|
||||||
|
az webapp config container set --name $app_name -g suhailgroup --docker-registry-server-url https://$suhailregistry.azurecr.io
|
||||||
|
|
||||||
|
echo 'app started'
|
||||||
|
|
||||||
|
|
||||||
|
|
2
z3/src/.gitignore
vendored
Normal file
2
z3/src/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.history
|
||||||
|
.idea
|
104
z3/src/Readme.md
Normal file
104
z3/src/Readme.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Overview of the APP
|
||||||
|
|
||||||
|
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
|
||||||
|
|
||||||
|
![homepage](images/homepage.png?raw=true "System Architecture")
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
The basic system requirements are as follows
|
||||||
|
|
||||||
|
- Any OS, preferably Linux
|
||||||
|
- Docker
|
||||||
|
- Docker compose
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
![Architecture](images/fruitapp.jpg?raw=true "System Architecture")
|
||||||
|
|
||||||
|
### Technology used
|
||||||
|
|
||||||
|
- Flask (Backend Rest API)
|
||||||
|
- Angular (Frontend)
|
||||||
|
- MySQL - as persistent database
|
||||||
|
|
||||||
|
# Docker environment
|
||||||
|
|
||||||
|
For shipping and deploying the application docker-compose is used. All the configurations are in the docker-compose.yml file.
|
||||||
|
|
||||||
|
Key points of the docker-compose.yml is given below.
|
||||||
|
|
||||||
|
## services
|
||||||
|
|
||||||
|
- **backend:** runs the Flask web API.
|
||||||
|
- **db** runs the mysql database required for the backend API
|
||||||
|
- **web** this is the front-end application
|
||||||
|
|
||||||
|
## Virtual networks
|
||||||
|
|
||||||
|
One virtual network is used
|
||||||
|
|
||||||
|
- main
|
||||||
|
|
||||||
|
## list of the containers
|
||||||
|
|
||||||
|
### fruitapp-backend
|
||||||
|
|
||||||
|
This container runs under the service name backend. The dockerfile user for this container is located at `fruitapp-backend/Dockerfile`. This container is based on python:3.9 image.
|
||||||
|
|
||||||
|
### fruitapp-frontend
|
||||||
|
|
||||||
|
The frontend container for the application. The dockerfile is located at `fruitapp-frontend/.docker/dev/Dockerfile`. Base image used to build this container is `Node:14`.
|
||||||
|
|
||||||
|
### db container
|
||||||
|
|
||||||
|
The container is built using the official `mysql:5.7.22` image pulled from dockerhub.
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
To prepare the environment for the first time run the following command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Run the app background
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash start-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of backend
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f backend
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of forntend
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
see the logs of database
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stopping
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash stop-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Removing containers
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash remove-app.sh
|
||||||
|
```
|
||||||
|
# Viewing the app
|
||||||
|
|
||||||
|
After running the app. visit [http://localhost:4200](http://localhost:4200) to view the app
|
56
z3/src/docker-compose.yml
Normal file
56
z3/src/docker-compose.yml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: fruitapp-backend:latest
|
||||||
|
container_name: fruitapp-backend
|
||||||
|
command: 'bash scripts/entrypoint.sh'
|
||||||
|
ports:
|
||||||
|
- '8001:5000'
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
env:
|
||||||
|
- name: SQLALCHEMY_DATABASE_URI
|
||||||
|
value: mysql://user:password@mysql-service/main
|
||||||
|
- name: FLASK_APP
|
||||||
|
value: app.py
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
value: main
|
||||||
|
- name: MYSQL_USER
|
||||||
|
value: user
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
value: password
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
value: root
|
||||||
|
db:
|
||||||
|
image: mysql:5.7.38
|
||||||
|
container_name: fruitapp-db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- msql-data:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- '33067:3306'
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
env:
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
value: 'main'
|
||||||
|
- name: MYSQL_USER
|
||||||
|
value: 'user'
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
value: 'password'
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
value: 'root1'
|
||||||
|
web:
|
||||||
|
image: fruitapp-frontend:latest
|
||||||
|
container_name: fruitapp-frontend
|
||||||
|
ports:
|
||||||
|
- 80:4200
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- main
|
8
z3/src/fruitapp-backend/.env
Normal file
8
z3/src/fruitapp-backend/.env
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
MYSQL_DATABASE=main
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=root
|
||||||
|
MYSQL_ROOT_PASSWORD=root
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI=mysql://root:root@fruitapp-backend/main
|
||||||
|
|
||||||
|
FLASK_APP=app.py
|
6
z3/src/fruitapp-backend/.env.template
Normal file
6
z3/src/fruitapp-backend/.env.template
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
MYSQL_DATABASE=main
|
||||||
|
MYSQL_USER=root
|
||||||
|
MYSQL_PASSWORD=root
|
||||||
|
MYSQL_ROOT_PASSWORD=root
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI=mysql://root:root@test/main
|
23
z3/src/fruitapp-backend/.gitignore
vendored
Normal file
23
z3/src/fruitapp-backend/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
.flaskenv
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
_mailinglist
|
||||||
|
.tox/
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.idea/
|
||||||
|
docs/_build/
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
*,cover
|
16
z3/src/fruitapp-backend/Dockerfile
Normal file
16
z3/src/fruitapp-backend/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3-alpine
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache mariadb-dev && \
|
||||||
|
apk add --no-cache --virtual .build-deps gcc musl-dev && \
|
||||||
|
python3 -m pip install -r requirements.txt --no-cache-dir && \
|
||||||
|
apk --purge del .build-deps
|
||||||
|
|
||||||
|
COPY ./scripts/entrypoint.sh /app/scripts/entrypoint.sh
|
||||||
|
COPY . /app
|
||||||
|
CMD sh /app/scripts/entrypoint.sh
|
||||||
|
EXPOSE 5000
|
||||||
|
|
2
z3/src/fruitapp-backend/config.py
Normal file
2
z3/src/fruitapp-backend/config.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
63
z3/src/fruitapp-backend/main.py
Normal file
63
z3/src/fruitapp-backend/main.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from flask import Flask, jsonify, abort
|
||||||
|
from flask_cors import CORS
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
||||||
|
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
|
||||||
|
# app.config["SQLALCHEMY_ENGINE_OPTIONS"] = dict(
|
||||||
|
# connect_args=dict(
|
||||||
|
# sslmode='require'
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
app.config.update({
|
||||||
|
'SQLALCHEMY_POOL_SIZE': None,
|
||||||
|
'SQLALCHEMY_POOL_TIMEOUT': None
|
||||||
|
})
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Product(db.Model):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
image: str
|
||||||
|
likes: int
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||||
|
title = db.Column(db.String(200))
|
||||||
|
image = db.Column(db.TEXT(20000000))
|
||||||
|
likes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProductUser(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer)
|
||||||
|
product_id = db.Column(db.Integer)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/products')
|
||||||
|
def index():
|
||||||
|
return jsonify(Product.query.all())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/products/<int:id>/like', methods=['POST'])
|
||||||
|
def like(id):
|
||||||
|
# define a static user id for demo
|
||||||
|
try:
|
||||||
|
product = Product.query.get(id)
|
||||||
|
product.likes += 1
|
||||||
|
db.session.commit()
|
||||||
|
except:
|
||||||
|
abort(400, 'bad request')
|
||||||
|
|
||||||
|
return jsonify(product)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0')
|
12
z3/src/fruitapp-backend/manage.py
Normal file
12
z3/src/fruitapp-backend/manage.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
|
from flask_script import Manager
|
||||||
|
|
||||||
|
from main import app, db
|
||||||
|
|
||||||
|
migrate = Migrate(app, db, compare_type=True)
|
||||||
|
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
1
z3/src/fruitapp-backend/migrations/README
Normal file
1
z3/src/fruitapp-backend/migrations/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
45
z3/src/fruitapp-backend/migrations/alembic.ini
Normal file
45
z3/src/fruitapp-backend/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
96
z3/src/fruitapp-backend/migrations/env.py
Normal file
96
z3/src/fruitapp-backend/migrations/env.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
from flask import current_app
|
||||||
|
config.set_main_option(
|
||||||
|
'sqlalchemy.url',
|
||||||
|
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||||
|
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=target_metadata, literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
process_revision_directives=process_revision_directives,
|
||||||
|
**current_app.extensions['migrate'].configure_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
24
z3/src/fruitapp-backend/migrations/script.py.mako
Normal file
24
z3/src/fruitapp-backend/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
40
z3/src/fruitapp-backend/migrations/versions/843c810aec1f_.py
Normal file
40
z3/src/fruitapp-backend/migrations/versions/843c810aec1f_.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 843c810aec1f
|
||||||
|
Revises:
|
||||||
|
Create Date: 2020-12-08 08:40:37.736465
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '843c810aec1f'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('product',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('image', sa.String(length=200), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('product_user',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('product_user')
|
||||||
|
op.drop_table('product')
|
||||||
|
# ### end Alembic commands ###
|
34
z3/src/fruitapp-backend/migrations/versions/85749c0c4589_.py
Normal file
34
z3/src/fruitapp-backend/migrations/versions/85749c0c4589_.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 85749c0c4589
|
||||||
|
Revises: b3ff59df2833
|
||||||
|
Create Date: 2022-04-08 07:46:57.160792
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '85749c0c4589'
|
||||||
|
down_revision = 'b3ff59df2833'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=mysql.VARCHAR(length=20000),
|
||||||
|
type_=sa.TEXT(length=20000000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=sa.TEXT(length=20000000),
|
||||||
|
type_=mysql.VARCHAR(length=20000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
34
z3/src/fruitapp-backend/migrations/versions/b3ff59df2833_.py
Normal file
34
z3/src/fruitapp-backend/migrations/versions/b3ff59df2833_.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: b3ff59df2833
|
||||||
|
Revises: fee4d1b1d192
|
||||||
|
Create Date: 2022-04-08 07:33:52.082355
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b3ff59df2833'
|
||||||
|
down_revision = 'fee4d1b1d192'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=mysql.VARCHAR(length=200),
|
||||||
|
type_=sa.String(length=20000),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('product', 'image',
|
||||||
|
existing_type=sa.String(length=20000),
|
||||||
|
type_=mysql.VARCHAR(length=200),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
28
z3/src/fruitapp-backend/migrations/versions/fee4d1b1d192_.py
Normal file
28
z3/src/fruitapp-backend/migrations/versions/fee4d1b1d192_.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: fee4d1b1d192
|
||||||
|
Revises: 843c810aec1f
|
||||||
|
Create Date: 2022-04-08 07:05:04.647143
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fee4d1b1d192'
|
||||||
|
down_revision = '843c810aec1f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('product', sa.Column('likes', sa.Integer(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('product', 'likes')
|
||||||
|
# ### end Alembic commands ###
|
27
z3/src/fruitapp-backend/producer.py
Normal file
27
z3/src/fruitapp-backend/producer.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pika
|
||||||
|
|
||||||
|
RABBIT_ENDPOINT = os.environ['RABBIT_ENDPOINT']
|
||||||
|
|
||||||
|
|
||||||
|
class Producer:
|
||||||
|
"""Implement the producer logic."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.params = pika.URLParameters(RABBIT_ENDPOINT)
|
||||||
|
self.connection = pika.BlockingConnection(self.params)
|
||||||
|
|
||||||
|
def publish(self, method, body):
|
||||||
|
properties = pika.BasicProperties(method)
|
||||||
|
if not self.connection or self.connection.is_closed:
|
||||||
|
self.connection = pika.BlockingConnection(self.params)
|
||||||
|
channel = self.connection.channel()
|
||||||
|
channel.basic_publish(
|
||||||
|
exchange='',
|
||||||
|
routing_key='admin',
|
||||||
|
body=json.dumps(body),
|
||||||
|
properties=properties,
|
||||||
|
)
|
||||||
|
print('Product published in queue: MAIN')
|
13
z3/src/fruitapp-backend/requirements.txt
Normal file
13
z3/src/fruitapp-backend/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Flask==1.1.2
|
||||||
|
werkzeug==1.0.1
|
||||||
|
Flask-SQLAlchemy==2.4.4
|
||||||
|
SQLAlchemy==1.3.20
|
||||||
|
Flask-Migrate==2.5.3
|
||||||
|
Flask-Script==2.0.6
|
||||||
|
Flask-Cors==3.0.9
|
||||||
|
requests==2.25.0
|
||||||
|
mysqlclient==2.0.1
|
||||||
|
pika==1.1.0
|
||||||
|
jinja2<3.1.0
|
||||||
|
itsdangerous==2.0.1
|
||||||
|
python-dotenv==0.20.0
|
5
z3/src/fruitapp-backend/scripts/entrypoint.sh
Normal file
5
z3/src/fruitapp-backend/scripts/entrypoint.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
python manage.py db upgrade
|
||||||
|
echo "DB migration done."
|
||||||
|
python seeder.py
|
||||||
|
python main.py
|
40
z3/src/fruitapp-backend/seeder.py
Normal file
40
z3/src/fruitapp-backend/seeder.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from main import Product, db
|
||||||
|
|
||||||
|
|
||||||
|
def insert_data(data):
|
||||||
|
product = Product.query.get(data['id'])
|
||||||
|
if not product:
|
||||||
|
product = Product(id=data['id'], title=data['title'], image=data['image'])
|
||||||
|
db.session.add(product)
|
||||||
|
db.session.commit()
|
||||||
|
print('inserted ', data['title'])
|
||||||
|
else:
|
||||||
|
print('Product already exists.')
|
||||||
|
|
||||||
|
|
||||||
|
PRODUCTS = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'title': 'Apple',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/red-apple-picture-id184276818?k=20&m=184276818&s=612x612&w=0&h=QxOcueqAUVTdiJ7DVoCu-BkNCIuwliPEgtAQhgvBA_g='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'title': 'Orange',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/tiny-tangerine-picture-id89951356?k=20&m=89951356&s=170667a&w=0&h=9oW2EBTASfaBol8qYVYB8YNYcSoNe1AImG4wqdgErEs='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'title': 'Banana',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/banana-picture-id1184345169?k=20&m=1184345169&s=170667a&w=0&h=t22KqOZ9EEwyRj7i35uxY-Xf6P_gAgLejd-SeReTnPY='
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'title': 'Avocado',
|
||||||
|
'image': 'https://media.istockphoto.com/photos/half-of-fresh-ripe-avocado-isolated-on-white-background-picture-id1278032327?b=1&k=20&m=1278032327&s=170667a&w=0&h=7eMAI9S_YBiVaDN49zk5gUZEhhA1oGDIWh3Tdst6q4Y='
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in PRODUCTS:
|
||||||
|
insert_data(p)
|
||||||
|
|
17
z3/src/fruitapp-frontend/.browserslistrc
Normal file
17
z3/src/fruitapp-frontend/.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
last 1 Chrome version
|
||||||
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
|
Firefox ESR
|
||||||
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
45
z3/src/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
45
z3/src/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#FROM node:14
|
||||||
|
|
||||||
|
#RUN npm install @angular/cli@11.0.0 -g
|
||||||
|
|
||||||
|
# Create and define the node_modules's cache directory.
|
||||||
|
#WORKDIR /cache
|
||||||
|
|
||||||
|
#COPY package.json .
|
||||||
|
#COPY package-lock.json .
|
||||||
|
#RUN npm install
|
||||||
|
|
||||||
|
# define the application's working directory.
|
||||||
|
#WORKDIR /app
|
||||||
|
#COPY . .
|
||||||
|
#ENV PATH /app/node_modules/.bin:$PATH
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 1: Compile and Build angular codebase
|
||||||
|
|
||||||
|
# Use official node image as the base image
|
||||||
|
FROM node:latest as build
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /usr/local/app
|
||||||
|
ENV NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
# Add the source code to app
|
||||||
|
COPY ./ /usr/local/app/
|
||||||
|
|
||||||
|
# Install all the dependencies
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# Generate the build of the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 2: Serve app with nginx server
|
||||||
|
|
||||||
|
# Use official nginx image as the base image
|
||||||
|
FROM nginx:latest
|
||||||
|
|
||||||
|
# Copy the build output to replace the default nginx contents.
|
||||||
|
COPY --from=build /usr/local/app/dist/dockerize-angular /usr/share/nginx/html
|
||||||
|
COPY .docker/dev/default.conf /etc/nginx/conf.d/
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
14
z3/src/fruitapp-frontend/.docker/dev/default.conf
Normal file
14
z3/src/fruitapp-frontend/.docker/dev/default.conf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
location /api/ {
|
||||||
|
# The following statement will proxy traffic to the upstream named Backend
|
||||||
|
proxy_pass http://backend-service:5000;
|
||||||
|
}
|
||||||
|
}
|
9
z3/src/fruitapp-frontend/.dockerignore
Normal file
9
z3/src/fruitapp-frontend/.dockerignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
.history
|
16
z3/src/fruitapp-frontend/.editorconfig
Normal file
16
z3/src/fruitapp-frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
46
z3/src/fruitapp-frontend/.gitignore
vendored
Normal file
46
z3/src/fruitapp-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user