Add 'Programming/ERSMS-project/' from commit 'd060e8285ad6f3dddde51a842f2c8498138afb0b'
git-subtree-dir: Programming/ERSMS-project git-subtree-mainline:7861d69ae9git-subtree-split:d060e8285a
310
Programming/ERSMS-project/.gitignore
vendored
Normal file
@ -0,0 +1,310 @@
|
||||
connector/Lib/*
|
||||
connector/Scripts/*
|
||||
pyvenv.cfg
|
||||
|
||||
## Core latex/pdflatex auxiliary files:
|
||||
*.aux
|
||||
*.lof
|
||||
*.log
|
||||
*.lot
|
||||
*.fls
|
||||
*.out
|
||||
*.toc
|
||||
*.fmt
|
||||
*.fot
|
||||
*.cb
|
||||
*.cb2
|
||||
.*.lb
|
||||
|
||||
## Intermediate documents:
|
||||
*.dvi
|
||||
*.xdv
|
||||
*-converted-to.*
|
||||
# these rules might exclude image files for figures etc.
|
||||
# *.ps
|
||||
# *.eps
|
||||
# *.pdf
|
||||
|
||||
## Generated if empty string is given at "Please type another file name for output:"
|
||||
.pdf
|
||||
|
||||
## Bibliography auxiliary files (bibtex/biblatex/biber):
|
||||
*.bbl
|
||||
*.bcf
|
||||
*.blg
|
||||
*-blx.aux
|
||||
*-blx.bib
|
||||
*.run.xml
|
||||
|
||||
## Build tool auxiliary files:
|
||||
*.fdb_latexmk
|
||||
*.synctex
|
||||
*.synctex(busy)
|
||||
*.synctex.gz
|
||||
*.synctex.gz(busy)
|
||||
*.pdfsync
|
||||
*.rubbercache
|
||||
rubber.cache
|
||||
|
||||
## Build tool directories for auxiliary files
|
||||
# latexrun
|
||||
latex.out/
|
||||
|
||||
## Auxiliary and intermediate files from other packages:
|
||||
# algorithms
|
||||
*.alg
|
||||
*.loa
|
||||
|
||||
# achemso
|
||||
acs-*.bib
|
||||
|
||||
# amsthm
|
||||
*.thm
|
||||
|
||||
# beamer
|
||||
*.nav
|
||||
*.pre
|
||||
*.snm
|
||||
*.vrb
|
||||
|
||||
# changes
|
||||
*.soc
|
||||
|
||||
# comment
|
||||
*.cut
|
||||
|
||||
# cprotect
|
||||
*.cpt
|
||||
|
||||
# elsarticle (documentclass of Elsevier journals)
|
||||
*.spl
|
||||
|
||||
# endnotes
|
||||
*.ent
|
||||
|
||||
# fixme
|
||||
*.lox
|
||||
|
||||
# feynmf/feynmp
|
||||
*.mf
|
||||
*.mp
|
||||
*.t[1-9]
|
||||
*.t[1-9][0-9]
|
||||
*.tfm
|
||||
|
||||
#(r)(e)ledmac/(r)(e)ledpar
|
||||
*.end
|
||||
*.?end
|
||||
*.[1-9]
|
||||
*.[1-9][0-9]
|
||||
*.[1-9][0-9][0-9]
|
||||
*.[1-9]R
|
||||
*.[1-9][0-9]R
|
||||
*.[1-9][0-9][0-9]R
|
||||
*.eledsec[1-9]
|
||||
*.eledsec[1-9]R
|
||||
*.eledsec[1-9][0-9]
|
||||
*.eledsec[1-9][0-9]R
|
||||
*.eledsec[1-9][0-9][0-9]
|
||||
*.eledsec[1-9][0-9][0-9]R
|
||||
|
||||
# glossaries
|
||||
*.acn
|
||||
*.acr
|
||||
*.glg
|
||||
*.glo
|
||||
*.gls
|
||||
*.glsdefs
|
||||
*.lzo
|
||||
*.lzs
|
||||
*.slg
|
||||
*.slo
|
||||
*.sls
|
||||
|
||||
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
|
||||
# *.ist
|
||||
|
||||
# gnuplot
|
||||
*.gnuplot
|
||||
*.table
|
||||
|
||||
# gnuplottex
|
||||
*-gnuplottex-*
|
||||
|
||||
# gregoriotex
|
||||
*.gaux
|
||||
*.glog
|
||||
*.gtex
|
||||
|
||||
# htlatex
|
||||
*.4ct
|
||||
*.4tc
|
||||
*.idv
|
||||
*.lg
|
||||
*.trc
|
||||
*.xref
|
||||
|
||||
# hypdoc
|
||||
*.hd
|
||||
|
||||
# hyperref
|
||||
*.brf
|
||||
|
||||
# knitr
|
||||
*-concordance.tex
|
||||
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
|
||||
# *.tikz
|
||||
*-tikzDictionary
|
||||
|
||||
# listings
|
||||
*.lol
|
||||
|
||||
# luatexja-ruby
|
||||
*.ltjruby
|
||||
|
||||
# makeidx
|
||||
*.idx
|
||||
*.ilg
|
||||
*.ind
|
||||
|
||||
# minitoc
|
||||
*.maf
|
||||
*.mlf
|
||||
*.mlt
|
||||
*.mtc[0-9]*
|
||||
*.slf[0-9]*
|
||||
*.slt[0-9]*
|
||||
*.stc[0-9]*
|
||||
|
||||
# minted
|
||||
_minted*
|
||||
*.pyg
|
||||
|
||||
# morewrites
|
||||
*.mw
|
||||
|
||||
# newpax
|
||||
*.newpax
|
||||
|
||||
# nomencl
|
||||
*.nlg
|
||||
*.nlo
|
||||
*.nls
|
||||
|
||||
# pax
|
||||
*.pax
|
||||
|
||||
# pdfpcnotes
|
||||
*.pdfpc
|
||||
|
||||
# sagetex
|
||||
*.sagetex.sage
|
||||
*.sagetex.py
|
||||
*.sagetex.scmd
|
||||
|
||||
# scrwfile
|
||||
*.wrt
|
||||
|
||||
# svg
|
||||
svg-inkscape/
|
||||
|
||||
# sympy
|
||||
*.sout
|
||||
*.sympy
|
||||
sympy-plots-for-*.tex/
|
||||
|
||||
# pdfcomment
|
||||
*.upa
|
||||
*.upb
|
||||
|
||||
# pythontex
|
||||
*.pytxcode
|
||||
pythontex-files-*/
|
||||
|
||||
# tcolorbox
|
||||
*.listing
|
||||
|
||||
# thmtools
|
||||
*.loe
|
||||
|
||||
# TikZ & PGF
|
||||
*.dpth
|
||||
*.md5
|
||||
*.auxlock
|
||||
|
||||
# titletoc
|
||||
*.ptc
|
||||
|
||||
# todonotes
|
||||
*.tdo
|
||||
|
||||
# vhistory
|
||||
*.hst
|
||||
*.ver
|
||||
|
||||
# easy-todo
|
||||
*.lod
|
||||
|
||||
# xcolor
|
||||
*.xcp
|
||||
|
||||
# xmpincl
|
||||
*.xmpi
|
||||
|
||||
# xindy
|
||||
*.xdy
|
||||
|
||||
# xypic precompiled matrices and outlines
|
||||
*.xyc
|
||||
*.xyd
|
||||
|
||||
# endfloat
|
||||
*.ttt
|
||||
*.fff
|
||||
|
||||
# Latexian
|
||||
TSWLatexianTemp*
|
||||
|
||||
## Editors:
|
||||
# WinEdt
|
||||
*.bak
|
||||
*.sav
|
||||
|
||||
# Texpad
|
||||
.texpadtmp
|
||||
|
||||
# LyX
|
||||
*.lyx~
|
||||
|
||||
# Kile
|
||||
*.backup
|
||||
|
||||
# gummi
|
||||
.*.swp
|
||||
|
||||
# KBibTeX
|
||||
*~[0-9]*
|
||||
|
||||
# TeXnicCenter
|
||||
*.tps
|
||||
|
||||
# auto folder when using emacs and auctex
|
||||
./auto/*
|
||||
*.el
|
||||
|
||||
# expex forward references with \gathertags
|
||||
*-tags.tex
|
||||
|
||||
# standalone packages
|
||||
*.sta
|
||||
|
||||
# Makeindex log files
|
||||
*.lpz
|
||||
|
||||
# xwatermark package
|
||||
*.xwm
|
||||
|
||||
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
|
||||
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
|
||||
# Uncomment the next line to have this generated file ignored.
|
||||
#*Notes.bib
|
||||
15
Programming/ERSMS-project/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM node:latest
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
|
||||
# If you also need http-server globally
|
||||
RUN npm install -g http-server
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["http-server", "-p 8080"]
|
||||
39
Programming/ERSMS-project/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Projekt z ERSMS
|
||||
|
||||
|
||||
## Backend
|
||||
|
||||
### budowanie i uruchamianie obrazu z bazą danych oraz serwerem:
|
||||
docker compose up
|
||||
|
||||
w przypadku zmian w bazie danych, usunąć kontener oraz dołączone do niego volumy
|
||||
|
||||
### stronka administracyjna do bazy danych
|
||||
|
||||
Najpierw wpisujemy w przeglądarce http://localhost:8080/
|
||||
Przy logowaniu wpisujemy:
|
||||
* login - admin@admin.com
|
||||
* hasło - admin
|
||||
|
||||
Po zalogowaniu się poraz pierwszy od zbudowania obrazu, widzimy stronę główną. Klikamy na niej duży przycisk "Add New Server" i pojawia się okienko z menu dodawania serwera.
|
||||
|
||||
W nazwie można napisać cokolwiek, jednak w zakładce "Connection" piszemy następujące rzeczy:
|
||||
* host name - db
|
||||
* login - root
|
||||
* password - root
|
||||
|
||||
Reszta pozostaje jak była. Po zapisaniu ustawień, mamy już dostęp do narzędzi administratora dla naszej bazy danych.
|
||||
|
||||
### gadanie z endpointami
|
||||
|
||||
GET http://localhost:8090/ - testowy domowy
|
||||
|
||||
GET http://localhost:8090//api/v3/get/<string:username> - info o userach
|
||||
|
||||
GET http://localhost:8090//api/v3/get_movie/<int:movie_ID> - info o filmie
|
||||
|
||||
POST http://localhost:8090/api/v3/add/<string:oauth_ID>/<string:username> - dodawanie usera
|
||||
|
||||
POST http://localhost:8090/api/v3/add/<string:oauth_ID>/<string:movie_ID>/<int:rating> - dodawanie oceny do filmu przez usera
|
||||
|
||||
GET http://localhost:8090/api/v3/ai/<string:oauth_ID> - wyciąganie rekomendacji od AI
|
||||
25
Programming/ERSMS-project/analytics/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set the working directory to /app2
|
||||
WORKDIR /app2
|
||||
|
||||
# Install necessary OS packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the Python script, requirements, and datasets into the container at /app2
|
||||
COPY ./analytics.py .
|
||||
COPY init_scripts/constants.ini ./init_scripts/constants.ini
|
||||
COPY init_scripts/movies.csv /app/init_scripts/
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install requests
|
||||
|
||||
# Run frontend_AI_connector.py when the container launches
|
||||
CMD ["python", "./analytics.py"]
|
||||
116
Programming/ERSMS-project/analytics/analytics.py
Normal file
@ -0,0 +1,116 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_caching import Cache
|
||||
import psycopg2
|
||||
import pandas
|
||||
import json
|
||||
from configparser import ConfigParser
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
|
||||
db_connector = None
|
||||
conn = None
|
||||
|
||||
|
||||
@app.route("/api/get_number_of_ratings", methods=["GET"])
|
||||
@cache.cached(timeout=500)
|
||||
def get_number_of_ratings():
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select count(*) as num_of_ratings from ratings")
|
||||
res = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(res[0]), 200
|
||||
|
||||
|
||||
@app.route("/api/get_movie_ratings/<string:movie_id>", methods=["GET"])
|
||||
@cache.cached(timeout=50)
|
||||
def get_movie_ratings(movie_id):
|
||||
cursor = conn.cursor()
|
||||
ratings = {}
|
||||
rating_values = [5, 4, 3, 2, 1]
|
||||
|
||||
for rating in rating_values:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM ratings
|
||||
WHERE rating = %s AND movie_ID = %s;
|
||||
""", (rating, movie_id))
|
||||
result = cursor.fetchone()
|
||||
ratings[f'{rating}_star'] = result[0]
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(ratings), 200
|
||||
|
||||
|
||||
@app.route("/api/get_users_number", methods=["GET"])
|
||||
@cache.cached(timeout=50)
|
||||
def get_number_of_users():
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select count(*) as num_of_users from users")
|
||||
res = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(res[0]), 200
|
||||
|
||||
|
||||
@app.route("/api/get_movie_rating_avg/<string:movie_id>", methods=["GET"])
|
||||
@cache.cached(timeout=50)
|
||||
def get_movie_rating_avg(movie_id):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT AVG(rating) as avg_rating
|
||||
FROM ratings
|
||||
WHERE movie_ID = %s;
|
||||
""", (movie_id,))
|
||||
res = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(res[0]), 200
|
||||
|
||||
|
||||
@app.route("/api/get_user_ratings/<string:user_id>", methods=["GET"])
|
||||
@cache.cached(timeout=50)
|
||||
def get_user_ratings(user_id):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT movie_ID, rating
|
||||
FROM ratings
|
||||
WHERE oauth_ID = %s;
|
||||
""", (user_id,))
|
||||
res = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(res), 200
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = ConfigParser()
|
||||
config.read("init_scripts/constants.ini")
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=config["postgres"]["host"],
|
||||
database=config["postgres"]["database"],
|
||||
user=config["postgres"]["user"],
|
||||
password=config["postgres"]["password"],
|
||||
port=int(config["postgres"]["port"])
|
||||
)
|
||||
|
||||
except Exception:
|
||||
print("Trying to connect with database")
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
cache.init_app(app)
|
||||
app.run(host="localhost", port=8082, debug=True)
|
||||
|
||||
conn.close()
|
||||
@ -0,0 +1,9 @@
|
||||
[postgres]
|
||||
host=db
|
||||
database=test_db
|
||||
user=root
|
||||
password=root
|
||||
port=5432
|
||||
|
||||
[movie]
|
||||
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv
|
||||
4804
Programming/ERSMS-project/analytics/init_scripts/movies.csv
Normal file
4
Programming/ERSMS-project/analytics/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
flask==3.0.3
|
||||
psycopg2==2.9.9
|
||||
pandas==2.2.2
|
||||
Flask-Caching==2.3.0
|
||||
1
Programming/ERSMS-project/backend/.env
Normal file
@ -0,0 +1 @@
|
||||
TMDB_BEARER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxYmMxYTM1Y2U5MmIwZjc5MjUzODJhYWQxN2NkMDI5NiIsInN1YiI6IjY2NDFkOTBlNTM3MzE0ZGRmMGZiNGYxZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.PbJkpuXgokjZPd8tL_ZVUyCW4Hf0dW67JInkgld27Ew"
|
||||
4
Programming/ERSMS-project/backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
instance
|
||||
# .env
|
||||
.json
|
||||
22
Programming/ERSMS-project/backend/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
|
||||
# Install necessary OS packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the Python script, requirements, and constants.ini file into the container at /app
|
||||
COPY ./app.py /app/
|
||||
|
||||
COPY requirements.txt .
|
||||
COPY .env .
|
||||
|
||||
# Install any needed packages specified in requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Run frontend_AI_connector.py when the container launches
|
||||
CMD ["python", "./app.py"]
|
||||
201
Programming/ERSMS-project/backend/app.py
Normal file
@ -0,0 +1,201 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
import firebase_admin
|
||||
from firebase_admin import credentials, auth
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import requests
|
||||
|
||||
load_dotenv()
|
||||
TMDB_BEARER_TOKEN = os.getenv('TMDB_BEARER_TOKEN')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.sqlite3'
|
||||
app.config['SQLALCHEY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
CORS(app)
|
||||
|
||||
cred = credentials.Certificate('movie-recommendation-firebase-adminsdk.json')
|
||||
firebase_admin.initialize_app(cred)
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(80), unique=True, nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
|
||||
|
||||
class Rating(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.uid'), nullable=False)
|
||||
movie_id = db.Column(db.Integer, nullable=False)
|
||||
value = db.Column(db.Integer, nullable=False)
|
||||
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
|
||||
@app.route('/login', methods=['POST'])
|
||||
def login():
|
||||
token = request.json.get('token')
|
||||
try:
|
||||
decoded_token = auth.verify_id_token(token)
|
||||
uid = decoded_token['uid']
|
||||
print(uid)
|
||||
email = decoded_token.get('email')
|
||||
|
||||
user = User.query.filter_by(uid=uid).first()
|
||||
if user is None:
|
||||
user = User(uid=uid, email=email)
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Login successful!', 'email': email, 'is_admin': user.is_admin}), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': 'Login failed'}), 401
|
||||
|
||||
|
||||
@app.route('/count_user_ratings', methods=['POST'])
|
||||
def count_user_ratings():
|
||||
token = request.json.get('token')
|
||||
try:
|
||||
decoded_token = auth.verify_id_token(token)
|
||||
user_id = decoded_token['uid']
|
||||
|
||||
user = User.query.filter_by(uid=user_id).first()
|
||||
if user is None:
|
||||
return jsonify({'message': 'Error'}), 500
|
||||
|
||||
rating_count = Rating.query.filter_by(user_id=user_id).count()
|
||||
return jsonify({'message': 'Ratings counted!', 'rating_count': rating_count}), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/movie/<int:movie_id>', methods=['GET'])
|
||||
def get_tmdb_data_movie_id(movie_id):
|
||||
url = f'https://api.themoviedb.org/3/movie/{movie_id}'
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {TMDB_BEARER_TOKEN}'}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return jsonify(response.json()), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/movie', methods=['GET'])
|
||||
def get_tmdb_data_query():
|
||||
query = request.args.get('query')
|
||||
|
||||
if query:
|
||||
url = f'https://api.themoviedb.org/3/search/movie?query={query}'
|
||||
else:
|
||||
url = 'https://api.themoviedb.org/3/trending/movie/day'
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {TMDB_BEARER_TOKEN}'}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return jsonify(response.json()), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/rating', methods=['POST'])
|
||||
def add_rating():
|
||||
token = request.json.get('token')
|
||||
movie = request.json.get('movie')
|
||||
value = request.json.get('value')
|
||||
try:
|
||||
decoded_token = auth.verify_id_token(token)
|
||||
user_id = decoded_token['uid']
|
||||
|
||||
# user = User.query.filter_by(uid=user_id).first()
|
||||
# if user is None:
|
||||
# return jsonify({'message': 'Error'}), 500
|
||||
|
||||
rating = Rating.query.filter_by(
|
||||
user_id=user_id, movie_id=movie).first()
|
||||
if rating is None:
|
||||
rating = Rating(user_id=user_id, movie_id=movie, value=value)
|
||||
|
||||
db.session.add(rating)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Rating added successfully!'}), 201
|
||||
else:
|
||||
rating.value = value
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Rating updated successfully!'}), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/rating', methods=['DELETE'])
|
||||
def remove_rating():
|
||||
token = request.json.get('token')
|
||||
movie = request.json.get('movie')
|
||||
try:
|
||||
decoded_token = auth.verify_id_token(token)
|
||||
user_id = decoded_token['uid']
|
||||
|
||||
# user = User.query.filter_by(uid=user_id).first()
|
||||
# if user is None:
|
||||
# return jsonify({'message': 'Error'}), 500
|
||||
|
||||
rating = Rating.query.filter_by(
|
||||
user_id=user_id, movie_id=movie).first()
|
||||
if rating is None:
|
||||
return jsonify({'message': 'Error'}), 500
|
||||
|
||||
db.session.delete(rating)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Rating removed successfully!'}), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/get_rating', methods=['POST'])
|
||||
def get_rating():
|
||||
token = request.json.get('token')
|
||||
movie = request.json.get('movie')
|
||||
try:
|
||||
decoded_token = auth.verify_id_token(token)
|
||||
user_id = decoded_token['uid']
|
||||
|
||||
# user = User.query.filter_by(uid=user_id).first()
|
||||
# if user is None:
|
||||
# return jsonify({'message': 'Error'}), 500
|
||||
|
||||
rating = Rating.query.filter_by(
|
||||
user_id=user_id, movie_id=movie).first()
|
||||
if rating is None:
|
||||
return jsonify({'message': 'Rating not found!'}), 200
|
||||
else:
|
||||
return jsonify({'message': 'Rating found!', 'movie': rating.movie_id, 'value': rating.value}), 200
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'message': str(e)}), 500
|
||||
|
||||
app.run(host="0.0.0.0", port=8084)
|
||||
8
Programming/ERSMS-project/backend/requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
flask==3.0.3
|
||||
psycopg2==2.9.9
|
||||
pandas==2.2.2
|
||||
Flask-Caching==2.3.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
firebase-admin==6.5.0
|
||||
python-dotenv==1.0.1
|
||||
requests==2.3.2
|
||||
29
Programming/ERSMS-project/connector/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set the working directory to /app
|
||||
WORKDIR /app
|
||||
|
||||
# Install necessary OS packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the Python script, requirements, and constants.ini file into the container at /app
|
||||
COPY Include/frontend_AI_connector.py /app/
|
||||
COPY Include/requirements.txt /app/
|
||||
COPY Include/init_scripts/constants.ini /app/init_scripts/
|
||||
COPY Include/init_scripts/movies.csv /app/init_scripts/
|
||||
# tls
|
||||
# COPY ./certs/connector.crt /app/
|
||||
# COPY ./certs/connector.key /app/
|
||||
|
||||
# Modify requirements.txt to use psycopg2-binary
|
||||
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
|
||||
|
||||
# Install any needed packages specified in requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Run frontend_AI_connector.py when the container launches
|
||||
CMD ["python", "./frontend_AI_connector.py"]
|
||||
@ -0,0 +1,164 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_caching import Cache
|
||||
import psycopg2
|
||||
import pandas
|
||||
import json
|
||||
from configparser import ConfigParser
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
|
||||
db_connector = None
|
||||
conn = None
|
||||
movie_list = None
|
||||
|
||||
|
||||
def error_decorator(fun):
|
||||
def inner1(*args, **kwargs):
|
||||
try:
|
||||
fun(*args, **kwargs)
|
||||
except psycopg2.DatabaseError:
|
||||
return jsonify({"status": "Something... unexpected has occurred :sweat_smile:"}), 500
|
||||
|
||||
return inner1
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@cache.cached(timeout=69)
|
||||
def hello():
|
||||
return jsonify({"response": "Hello there", "time": datetime.now()}), 200
|
||||
|
||||
|
||||
# endpoint do wyciągania danych o userze
|
||||
@app.route("/api/v3/get/<string:username>", methods=["GET"])
|
||||
def access_user(username):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select * from users where username='{}';".format(username))
|
||||
res = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
||||
return jsonify(res[0]), 200
|
||||
|
||||
|
||||
# endpoint służący do zapisu danych nowo stworzonego użytkownika, podajemy mu
|
||||
# id z oautha oraz login
|
||||
@app.route("/api/v3/add/<string:oauth_ID>/<string:username>", methods=["POST"])
|
||||
def add_user(oauth_ID, username):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select * from users where username='{}';".format(username))
|
||||
res = cursor.fetchall()
|
||||
|
||||
if len(res):
|
||||
cursor.close()
|
||||
return jsonify({"status": "User already exists"}), 409
|
||||
|
||||
cursor.execute("INSERT INTO users (username, oauth_ID) VALUES ('{}','{}');".format(
|
||||
username, oauth_ID
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
|
||||
# roboczy endpoint służący do wyciąganiu rekomendacji
|
||||
@app.route("/api/v3/ai/<string:oauth_ID>", methods=["GET"])
|
||||
def get_recommendations(oauth_ID):
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select movie_ID from ratings where oauth_ID='{}'", oauth_ID)
|
||||
res = cursor.fetchall()
|
||||
movies = [int(i) for i in res[0]]
|
||||
url = 'http://localhost:8081/api/v3/AI_recommendations'
|
||||
response = requests.post(url,
|
||||
json=movies,
|
||||
headers={'Content-Type': 'application/json'})
|
||||
return jsonify(response.json()), 200
|
||||
|
||||
|
||||
@app.route("/api/v3/get_movie/<int:movie_ID>", methods=["GET"])
|
||||
def get_movie(movie_ID):
|
||||
movie_info = movie_list.loc[movie_list['movie_id'] == movie_ID]
|
||||
if movie_info.empty:
|
||||
return jsonify({"status": "Movie with ID {} doesn't exist".format(movie_ID)}
|
||||
), 404
|
||||
|
||||
cast = json.loads(movie_info["cast"][0].replace('\\"', '"'))
|
||||
crew = json.loads(movie_info["crew"][0].replace('\\"', '"'))
|
||||
|
||||
output_json = {"movie_id": movie_ID,
|
||||
"title": movie_info["title"][0],
|
||||
"cast": cast,
|
||||
"crew": crew}
|
||||
|
||||
return jsonify(output_json), 200
|
||||
|
||||
|
||||
@app.route("/api/v3/rate_movie/<string:uID>/<string:movie_ID>/<int:rating>", methods=["POST"])
|
||||
def rate_movie(uID, movie_ID, rating):
|
||||
movie_info = movie_list.loc[movie_list['movie_id'] == int(movie_ID)]
|
||||
if movie_info.empty:
|
||||
return jsonify({"status": "Movie with ID {} doesn't exist".format(movie_ID)}
|
||||
), 404
|
||||
|
||||
if rating < 1 or rating > 5:
|
||||
return jsonify({"status": "Incorrect rating"}), 400
|
||||
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("select * from users where oauth_ID='{}';".format(uID))
|
||||
res = cursor.fetchall()
|
||||
|
||||
if not len(res):
|
||||
cursor.close()
|
||||
return jsonify({"status": "User doesn't exists"}), 404
|
||||
|
||||
cursor.execute("select * from ratings where oauth_ID='{}' AND movie_ID='{}';".format(uID, movie_ID))
|
||||
res = cursor.fetchall()
|
||||
|
||||
if len(res):
|
||||
sql = """ UPDATE ratings
|
||||
SET rating = {},
|
||||
rdate = CURRENT_TIMESTAMP
|
||||
WHERE oauth_ID = '{}' AND
|
||||
movie_ID = '{}'
|
||||
"""
|
||||
cursor.execute(sql.format(rating, uID, movie_ID))
|
||||
else:
|
||||
cursor.execute("INSERT INTO ratings (movie_ID, oauth_ID, rating) VALUES ('{}','{}',{});".format(
|
||||
movie_ID, uID, rating
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = ConfigParser()
|
||||
config.read("init_scripts/constants.ini")
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=config["postgres"]["host"],
|
||||
database=config["postgres"]["database"],
|
||||
user=config["postgres"]["user"],
|
||||
password=config["postgres"]["password"],
|
||||
port=int(config["postgres"]["port"])
|
||||
)
|
||||
|
||||
except Exception:
|
||||
print("Trying to connect with database")
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
movie_list = pandas.read_csv(config["movie"]["csv_path"])
|
||||
cache.init_app(app)
|
||||
app.run(host="localhost", port=8090, debug=True)
|
||||
|
||||
conn.close()
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
[postgres]
|
||||
host=db
|
||||
database=test_db
|
||||
user=root
|
||||
password=root
|
||||
port=5432
|
||||
|
||||
[movie]
|
||||
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv
|
||||
4804
Programming/ERSMS-project/connector/Include/init_scripts/movies.csv
Normal file
@ -0,0 +1,4 @@
|
||||
flask==3.0.3
|
||||
psycopg2==2.9.9
|
||||
pandas==2.2.2
|
||||
Flask-Caching==2.3.0
|
||||
20
Programming/ERSMS-project/database/init.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
oauth_ID VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ratings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
movie_ID VARCHAR(255) NOT NULL,
|
||||
oauth_ID VARCHAR(255) NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
rdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO users (username, oauth_ID) VALUES
|
||||
('Mkyong', 40),
|
||||
('Ali', 28),
|
||||
('Teoh', 18);
|
||||
|
||||
|
||||
97
Programming/ERSMS-project/docker-compose.yml
Normal file
@ -0,0 +1,97 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: ./connector
|
||||
ports:
|
||||
- "5000:8090" # Adjust if your app uses a different port
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- my-bridge-network
|
||||
db:
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_DB: test_db
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: root
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- my-bridge-network
|
||||
|
||||
pgadmin:
|
||||
container_name: admin_container
|
||||
image: dpage/pgadmin4
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
networks:
|
||||
- my-bridge-network
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./nginx/ssl:/etc/ssl/certs
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- my-bridge-network
|
||||
|
||||
frontend:
|
||||
container_name: frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8000:80"
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- my-bridge-network
|
||||
movie_rec:
|
||||
container_name: movie_rec
|
||||
build: ./movie_recommendations
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- my-bridge-network
|
||||
analitics:
|
||||
container_name: analytics
|
||||
build: ./analytics
|
||||
ports:
|
||||
- "8082:8082"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- my-bridge-network
|
||||
firebase:
|
||||
container_name: firebase
|
||||
build: .
|
||||
ports:
|
||||
- "8084:8084"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- my-bridge-network
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
my-bridge-network:
|
||||
driver: bridge
|
||||
BIN
Programming/ERSMS-project/docs/Architecture_Requirements.docx
Normal file
@ -0,0 +1,12 @@
|
||||
1. Receive data from database
|
||||
2. Calculate data:
|
||||
a) User Count
|
||||
b) Movies Count
|
||||
c) Rating Count
|
||||
d) Total data size
|
||||
e) "Hot" movies -> movies that received most ratings during last
|
||||
week
|
||||
f) System logs (keeps tracks of all messages exchanged by all services)
|
||||
1. timestamp (when was a message send)
|
||||
2. Message raw data
|
||||
3. Send requests to frondend upon request
|
||||
BIN
Programming/ERSMS-project/docs/Meetings/first_meeting.docx
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,0C114.62,0,0,114.62,0,256s114.62,256,256,256s256-114.62,256-256S397.38,0,256,0z M172.211,41.609
|
||||
c-24.934,27.119-44.68,66.125-56.755,111.992H49.749C75.179,102.741,118.869,62.524,172.211,41.609z M25.6,256
|
||||
c0-26.999,5.077-52.727,13.662-76.8h70.494c-4.608,24.294-7.356,49.963-7.356,76.8s2.748,52.506,7.347,76.8H39.262
|
||||
C30.677,308.727,25.6,283,25.6,256z M49.749,358.4h65.707c12.083,45.867,31.821,84.872,56.755,111.991
|
||||
C118.869,449.476,75.179,409.259,49.749,358.4z M243.2,485.188c-43.81-8.252-81.877-58.24-101.359-126.788H243.2V485.188z
|
||||
M243.2,332.8H135.74c-4.924-24.166-7.74-49.997-7.74-76.8s2.816-52.634,7.74-76.8H243.2V332.8z M243.2,153.6H141.841
|
||||
C161.323,85.052,199.39,35.063,243.2,26.812V153.6z M462.251,153.6h-65.707c-12.083-45.867-31.821-84.873-56.755-111.992
|
||||
C393.131,62.524,436.821,102.741,462.251,153.6z M268.8,26.812c43.81,8.252,81.877,58.24,101.359,126.788H268.8V26.812z
|
||||
M268.8,179.2h107.46c4.924,24.166,7.74,49.997,7.74,76.8s-2.816,52.634-7.74,76.8H268.8V179.2z M268.8,485.188V358.4h101.359
|
||||
C350.677,426.948,312.61,476.937,268.8,485.188z M339.789,470.391c24.934-27.127,44.672-66.125,56.755-111.991h65.707
|
||||
C436.821,409.259,393.131,449.476,339.789,470.391z M402.244,332.8c4.608-24.294,7.356-49.963,7.356-76.8
|
||||
s-2.748-52.506-7.347-76.8h70.494c8.576,24.073,13.653,49.801,13.653,76.8c0,27-5.077,52.727-13.662,76.8H402.244z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
|
||||
<g id="Bell">
|
||||
<path d="M46.6674995,8.6586504c-0.5527992,0-1,0.4473-1,1c0,0.5527992,0.4472008,1,1,1
|
||||
c3.5185013,0,6.3808022,2.8622999,6.3808022,6.3809004c0,0.5527,0.4473,1,1,1c0.5527992,0,1-0.4473,1-1
|
||||
C55.0483017,12.4184504,51.2886009,8.6586504,46.6674995,8.6586504z"/>
|
||||
<path d="M8.9517002,17.0395508c0,0.5527,0.4471998,1,1,1c0.5527,0,1-0.4473,1-1
|
||||
c0-3.5186005,2.8622999-6.3809004,6.3808002-6.3809004c0.5527992,0,1-0.4472008,1-1c0-0.5527-0.4472008-1-1-1
|
||||
C12.7114,8.6586504,8.9517002,12.4184504,8.9517002,17.0395508z"/>
|
||||
<path d="M48.1431007,1.27785c-0.5527992,0-1,0.4473-1,0.9999999s0.4472008,1,1,1
|
||||
c6.7743988,0,12.2860985,5.5107002,12.2860985,12.2851c0,0.5527992,0.4473,1,1,1s1-0.4472008,1-1
|
||||
C62.4291992,7.6860499,56.0200005,1.27785,48.1431007,1.27785z"/>
|
||||
<path d="M16.8560009,2.2778499c0-0.5526999-0.4473-0.9999999-1.000001-0.9999999
|
||||
c-7.8769999,0-14.2852001,6.4081998-14.2852001,14.2851c0,0.5527992,0.4473001,1,1.0000001,1s1-0.4472008,1-1
|
||||
c0-6.7743998,5.5107002-12.2851,12.2852001-12.2851C16.4087009,3.2778499,16.8560009,2.83055,16.8560009,2.2778499z"/>
|
||||
<path d="M51.7932014,46.2020493c-0.1280022-0.3828011-0.3692017-0.6965981-0.6621017-0.9463997
|
||||
c0.0236015-0.6092987,0.0386009-1.2222977,0.0386009-1.8409996c0-13.6198997-5.641201-25.1546001-13.4345016-29.1104012
|
||||
c0.1161003-0.4706001,0.1844025-0.9601002,0.1844025-1.4666996c0-3.3774004-2.7380028-6.1154003-6.1154022-6.1154003
|
||||
s-6.1153984,2.7379999-6.1153984,6.1154003c0,0.5065994,0.0682983,0.9960995,0.1843987,1.4666996
|
||||
C18.0799007,18.2600498,12.4386997,29.7947502,12.4386997,43.41465c0,0.6187019,0.0150003,1.2317009,0.0386,1.8409996
|
||||
c-0.2929001,0.2498016-0.5340996,0.5635986-0.6620998,0.9463997l-2.8975,8.6665993
|
||||
c-0.4692001,1.4033012,0.5754004,2.8535004,2.0555,2.8535004h14.1247005c0.8610001,2.8908005,3.535799,5,6.7062988,5
|
||||
s5.8453026-2.1091995,6.7063026-5h14.1246986c1.4800987,0,2.5247002-1.4501991,2.0555-2.8535004L51.7932014,46.2020493z
|
||||
M26.7959003,16.1219501l1.4443989-0.7332001l-0.3878994-1.5727005c-0.0841999-0.3413-0.1252003-0.6613998-0.1252003-0.9784994
|
||||
c0-2.2480001,1.8290005-4.0769005,4.0769997-4.0769005s4.0769997,1.8289003,4.0769997,4.0769005
|
||||
c0,0.3170996-0.0410004,0.6371994-0.1251984,0.9784994l-0.3879013,1.5727005L36.8125,16.1219501
|
||||
c7.2531013,3.6816006,12.3186989,14.9046993,12.3186989,27.2926998c0,0.4183006-0.0060997,0.8419991-0.0181007,1.2691002
|
||||
H14.4953003c-0.0120001-0.4271011-0.0181007-0.8507996-0.0181007-1.2691002
|
||||
C14.4771996,31.0266495,19.5428009,19.8035507,26.7959003,16.1219501z M31.8041992,60.7221489
|
||||
c-2.0464001,0-3.8094997-1.2355003-4.5824986-3h9.164999C35.6137009,59.4866486,33.8506012,60.7221489,31.8041992,60.7221489z
|
||||
M52.7711983,55.6526489c-0.0500984,0.0695-0.1102982,0.0695-0.1359978,0.0695H38.8041992h-14H10.9731998
|
||||
c-0.0256996,0-0.0859003,0-0.1359997-0.0695c-0.0496998-0.0690994-0.0307999-0.1256981-0.0227003-0.1497993l2.8975-8.6665993
|
||||
c0.0229006-0.0681992,0.0866003-0.1141014,0.1588001-0.1141014h35.8667984c0.0722008,0,0.1359024,0.0459023,0.158802,0.1140022
|
||||
l2.8974991,8.6666985C52.8019981,55.5269508,52.8209,55.5835495,52.7711983,55.6526489z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M118.922624,0.37140771 C175.483691,-3.5505123 212.986837,24.1282078 234.430251,57.8515157 C245.182251,74.7603157 255.731584,100.441382 255.780224,126.827729 C255.832277,155.497169 246.544597,180.324476 234.430251,198.541009 C221.979264,217.264422 205.875157,232.728956 185.709184,242.883196 C151.999957,259.857276 104.889984,260.321489 74.033024,243.978022 C39.6684361,225.777276 13.2466761,199.798822 3.41456926,154.746662 C-0.520150741,136.717436 -0.972417408,113.421862 4.50939593,93.4346624 C5.79579605,88.7460224 8.13350272,83.8116224 9.98395605,79.2014891 C18.8765427,57.0472491 34.0125427,37.7823945 52.6834773,24.4580211 C60.185984,19.1042078 69.2876373,13.7794078 77.3179307,10.2248478 C87.3096107,5.80244779 104.132224,1.3966877 118.922624,0.37140771 L118.922624,0.37140771 Z" fill="#FFFFFF">
|
||||
</path>
|
||||
<path d="M226.211797,130.015782 C226.211797,183.471996 182.876971,226.803836 129.421611,226.803836 C75.9662507,226.803836 32.6322761,183.471996 32.6322761,130.015782 C32.6322761,76.5608491 75.9662507,33.2255945 129.421611,33.2255945 C182.876544,33.2255945 226.211797,76.5608491 226.211797,130.015782 L226.211797,130.015782 Z" fill="#000000">
|
||||
</path>
|
||||
<path d="M118.922624,0.37140771 C175.483691,-3.5505123 212.986837,24.1282078 234.430251,57.8515157 C245.182251,74.7603157 255.731584,100.441382 255.780224,126.827729 C255.832277,155.497169 246.544597,180.324476 234.430251,198.541009 C221.979264,217.264422 205.875157,232.728956 185.709184,242.883196 C151.999957,259.857276 104.889984,260.321489 74.033024,243.978022 C39.6684361,225.777276 13.2466761,199.798822 3.41456926,154.746662 C-0.520150741,136.717436 -0.972417408,113.421862 4.50939593,93.4346624 C5.79579605,88.7460224 8.13350272,83.8116224 9.98395605,79.2014891 C18.8765427,57.0472491 34.0125427,37.7823945 52.6834773,24.4580211 C60.185984,19.1042078 69.2876373,13.7794078 77.3179307,10.2248478 C87.3096107,5.80244779 104.132224,1.3966877 118.922624,0.37140771 L118.922624,0.37140771 Z M99.762304,9.67786112 C78.753664,15.1246878 63.3497173,24.8829811 49.9464107,35.4071411 C30.6188361,50.5828224 18.2975561,71.7604224 11.0787827,97.2665557 C3.04763593,125.643302 8.20646272,159.982289 19.2904094,181.570769 C30.7843827,203.958822 46.217344,221.337382 68.0114773,233.576742 C89.2146773,245.484156 119.036971,253.130022 150.126464,247.262502 C177.748864,242.049489 198.727637,230.016209 215.818197,212.226769 C238.684117,188.425169 257.061931,144.585596 244.832384,98.3613824 C241.563264,86.0072491 237.289344,73.1313024 230.598784,62.2308224 C226.984064,56.3419691 221.679744,50.5486891 216.365611,44.7131691 C196.309717,22.6882078 163.894571,3.70879437 122.207104,6.39295445 C114.273664,6.90367445 107.301504,7.72287445 99.762304,9.67786112 L99.762304,9.67786112 Z" fill="#000000">
|
||||
</path>
|
||||
<g transform="translate(7.680000, 9.386667)" fill="#FFFFFF">
|
||||
<g transform="translate(0.000000, 2.986667)">
|
||||
<path d="M127.896362,234.025436 L239.741909,122.182449 L239.138518,121.579044 L127.292971,233.422031 L127.896362,234.025436 L127.896362,234.025436 Z">
|
||||
</path>
|
||||
<path d="M118.118869,225.167835 L230.452096,112.836742 L229.848704,112.233338 L117.515477,224.564432 L118.118869,225.167835 L118.118869,225.167835 Z">
|
||||
</path>
|
||||
<path d="M108.34095,216.311515 L221.16271,103.491461 L220.559317,102.888059 L107.737557,215.708112 L108.34095,216.311515 L108.34095,216.311515 Z">
|
||||
</path>
|
||||
<path d="M98.5630294,207.453915 L211.872896,94.1461817 L211.269504,93.5427783 L97.9596373,206.850512 L98.5630294,207.453915 L98.5630294,207.453915 Z">
|
||||
</path>
|
||||
<path d="M88.7855366,198.596741 L202.58351,84.8004745 L201.980117,84.1970722 L88.1821434,197.993339 L88.7855366,198.596741 L88.7855366,198.596741 Z">
|
||||
</path>
|
||||
<path d="M79.00762,189.741698 L193.293273,75.4551911 L192.689873,74.8517956 L78.40422,189.138302 L79.00762,189.741698 L79.00762,189.741698 Z">
|
||||
</path>
|
||||
<path d="M69.2296967,180.882394 L184.003883,66.1099145 L183.40049,65.5065122 L68.6263033,180.278992 L69.2296967,180.882394 L69.2296967,180.882394 Z">
|
||||
</path>
|
||||
<path d="M59.4517778,172.02522 L174.713644,56.7642067 L174.110249,56.1608067 L58.8483822,171.42182 L59.4517778,172.02522 L59.4517778,172.02522 Z">
|
||||
</path>
|
||||
<path d="M49.6738606,163.168044 L165.421701,47.4189239 L164.818299,46.8155294 L49.0704594,162.564649 L49.6738606,163.168044 L49.6738606,163.168044 Z">
|
||||
</path>
|
||||
<path d="M39.8963639,154.310447 L156.134444,38.0736472 L155.531049,37.4702461 L39.2929694,153.707046 L39.8963639,154.310447 L39.8963639,154.310447 Z">
|
||||
</path>
|
||||
<path d="M30.1184445,145.4537 L146.844631,28.7283667 L146.241236,28.1249667 L29.5150489,144.8503 L30.1184445,145.4537 L30.1184445,145.4537 Z">
|
||||
</path>
|
||||
<path d="M20.3405245,136.596527 L137.555244,19.38266 L136.951849,18.77926 L19.7371289,135.993127 L20.3405245,136.596527 L20.3405245,136.596527 Z">
|
||||
</path>
|
||||
<path d="M10.5626039,127.738927 L128.265431,10.0373805 L127.662036,9.43397947 L9.95920941,127.135526 L10.5626039,127.738927 L10.5626039,127.738927 Z">
|
||||
</path>
|
||||
<path d="M0.784683926,118.881754 L118.975617,0.692100527 L118.372223,0.088699473 L0.181289407,118.278353 L0.784683926,118.881754 L0.784683926,118.881754 Z">
|
||||
</path>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M0.14330767,122.999079 L114.223734,237.084625 L114.827146,236.481241 L0.746718997,122.395695 L0.14330767,122.999079 L0.14330767,122.999079 Z">
|
||||
</path>
|
||||
<path d="M9.39472045,113.615398 L123.697014,227.923238 L124.300426,227.319855 L9.99813288,113.012015 L9.39472045,113.615398 L9.39472045,113.615398 Z">
|
||||
</path>
|
||||
<path d="M18.6461344,104.231719 L133.170294,218.760999 L133.773706,218.157615 L19.2495456,103.628335 L18.6461344,104.231719 L18.6461344,104.231719 Z">
|
||||
</path>
|
||||
<path d="M27.8975477,94.8480388 L142.644001,209.599612 L143.247412,208.996228 L28.500959,94.2446545 L27.8975477,94.8480388 L27.8975477,94.8480388 Z">
|
||||
</path>
|
||||
<path d="M37.1485349,85.4643594 L152.116855,200.437373 L152.720265,199.833987 L37.7519451,84.8609739 L37.1485349,85.4643594 L37.1485349,85.4643594 Z">
|
||||
</path>
|
||||
<path d="M46.3999494,76.0806805 L161.590989,191.275561 L162.194397,190.672173 L47.0033573,75.4772928 L46.3999494,76.0806805 L46.3999494,76.0806805 Z">
|
||||
</path>
|
||||
<path d="M55.6513628,66.6970005 L171.064269,182.113747 L171.667677,181.510359 L56.2547706,66.0936128 L55.6513628,66.6970005 L55.6513628,66.6970005 Z">
|
||||
</path>
|
||||
<path d="M64.9023505,57.3133217 L180.537551,172.951508 L181.140956,172.348118 L65.5057561,56.7099317 L64.9023505,57.3133217 L64.9023505,57.3133217 Z">
|
||||
</path>
|
||||
<path d="M74.1537644,47.9296422 L190.011258,163.789696 L190.614662,163.186304 L74.7571689,47.3262511 L74.1537644,47.9296422 L74.1537644,47.9296422 Z">
|
||||
</path>
|
||||
<path d="M83.4051783,38.5459628 L199.484112,154.627029 L200.087515,154.023637 L84.0085817,37.9425705 L83.4051783,38.5459628 L83.4051783,38.5459628 Z">
|
||||
</path>
|
||||
<path d="M92.6565911,29.1622822 L208.957818,145.466069 L209.561222,144.862678 L93.2599955,28.5588911 L92.6565911,29.1622822 L92.6565911,29.1622822 Z">
|
||||
</path>
|
||||
<path d="M101.907579,19.7786034 L218.431099,136.30383 L219.034501,135.700437 L102.510981,19.17521 L101.907579,19.7786034 L101.907579,19.7786034 Z">
|
||||
</path>
|
||||
<path d="M111.158992,10.39535 L227.904379,127.142443 L228.507781,126.53905 L111.762394,9.79195665 L111.158992,10.39535 L111.158992,10.39535 Z">
|
||||
</path>
|
||||
<path d="M120.410833,1.01167058 L237.378086,117.980204 L237.981487,117.376809 L121.014234,0.408276091 L120.410833,1.01167058 L120.410833,1.01167058 Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M209.796224,45.2605824 C199.729877,35.1942345 187.353984,26.1233011 172.570837,20.6261278 C157.247531,14.9279945 139.913771,10.1250078 119.470037,11.8670878 C86.9042773,14.6429811 64.0392107,29.2541811 46.661504,46.3554091 C34.4895561,58.3332224 25.3337161,73.0109824 19.2899827,90.1497557 C10.4161694,115.314982 10.9546227,145.018236 20.9322227,170.622076 C29.4429427,192.461862 43.3727573,210.077222 63.0843307,223.722876 C81.6148907,236.551462 108.210731,246.500902 138.082091,243.978022 C174.464811,240.905169 201.085397,222.239356 218.554411,200.731089 C223.024171,195.228796 227.468331,189.009276 230.050091,181.571196 C232.579797,176.211836 233.817984,172.568529 234.977237,170.622502 C239.829291,159.750182 242.399957,148.278822 243.188437,135.587196 C245.614037,96.5506091 230.594517,66.0593024 209.796224,45.2605824 L209.796224,45.2605824 Z M199.073664,164.193062 C199.004117,164.341542 198.941824,164.479782 198.869291,164.632956 C197.064491,169.797756 193.955371,174.116476 190.830464,177.937702 C178.613291,192.872742 159.998251,205.834022 134.555264,207.968209 C113.665237,209.719249 95.0664107,202.811089 82.1081173,193.903142 C70.8475307,186.162129 62.287744,176.580049 56.2853973,165.064316 C55.9193173,164.371409 52.4334507,156.340262 52.4232107,155.986982 C47.6680107,141.237542 47.255424,117.520849 52.136064,103.288529 C55.3932373,93.7896491 60.8217173,83.5121024 68.0114773,75.3695957 C77.938304,64.1265024 87.872384,56.9222357 103.594624,51.8299691 C110.773291,49.5046357 117.199744,47.1379157 126.038997,46.9032491 C147.594197,46.3293824 169.544064,56.0074624 181.329451,66.6105557 C192.159957,76.3543424 204.503424,95.3346091 207.606571,113.141969 C210.800171,131.470289 207.391957,149.365116 199.073664,164.193062 L199.073664,164.193062 Z" fill="#000000">
|
||||
</path>
|
||||
<g transform="translate(83.626667, 76.373333)">
|
||||
<path d="M77.9810133,105.400747 C72.67328,105.400747 68.01536,102.039467 66.38976,97.0363733 L60.4778667,79.02336 L30.9034667,79.02336 L25.41056,96.8721067 C23.7525333,101.97248 19.0592,105.386667 13.72416,105.386667 C12.4433067,105.386667 11.1709867,105.184 9.94261333,104.785493 C3.56565333,102.87104 -0.0546133333,96.0072533 1.89909333,89.49376 L26.8616533,10.4226133 C28.48256,5.39264 33.2544,1.88416 38.4669867,1.88416 L51.7405867,1.88416 C56.9826133,1.88416 61.7540267,5.33290667 63.34336,10.2711467 L89.5957333,89.3128533 C91.6949333,95.7751467 88.21888,102.71232 81.8513067,104.785067 C80.5922133,105.193813 79.2904533,105.400747 77.9810133,105.400747 L77.9810133,105.400747 L77.9810133,105.400747 Z" fill="#FFFFFF">
|
||||
</path>
|
||||
<path d="M77.9810133,103.69408 C73.2261841,103.69408 69.0608465,100.688308 67.6071126,96.2141657 L61.6940408,78.1975435 L61.4049446,77.3166933 L60.4778667,77.3166933 L30.9034667,77.3166933 L29.958141,77.3166933 L29.6800885,78.2202018 L24.1871818,96.0689485 C22.7072691,100.620899 18.5067226,103.68 13.72416,103.68 C12.5787502,103.68 11.4396612,103.498823 10.3376028,103.141295 C4.61174908,101.421988 1.37421306,95.2722447 3.12512866,89.4348397 L28.0822714,10.3812927 C29.5308212,5.88624847 33.8113885,2.73749333 38.4669867,2.73749333 L51.7405867,2.73749333 C56.4288544,2.73749333 60.7065138,5.82950865 62.1249103,10.2366284 L88.3809821,89.2896459 C90.2599306,95.0739946 87.1485772,101.287946 81.4551086,103.141261 C80.3246516,103.508247 79.1562581,103.69408 77.9810133,103.69408 L77.9810133,103.69408 Z M77.9810133,106.25408 C79.4253475,106.25408 80.8604464,106.025828 82.2465357,105.575854 C89.2922648,103.282357 93.1289211,95.6198406 90.8131148,88.4907346 L64.5581113,9.4410208 C62.8016101,3.98319054 57.536605,0.177493333 51.7405867,0.177493333 L38.4669867,0.177493333 C32.6951659,0.177493333 27.4336011,4.04786331 25.6433485,9.60334848 L0.678475274,88.6817473 C-1.48096359,95.8806734 2.51145487,103.464326 9.57456724,105.584772 C10.9041173,106.016429 12.3097398,106.24 13.72416,106.24 C19.6168848,106.24 24.7994128,102.465759 26.6278552,96.8411577 L32.1268448,78.9731849 L30.9034667,79.8766933 L60.4778667,79.8766933 L59.2616926,78.9958432 L65.1735859,97.0088565 C66.9698962,102.537361 72.1204498,106.25408 77.9810133,106.25408 L77.9810133,106.25408 Z" fill="#000000">
|
||||
</path>
|
||||
</g>
|
||||
<g transform="translate(61.440000, 19.200000)" fill="#FFFFFF">
|
||||
<path d="M2.13376,33.8577067 L2.10261333,33.8154667 C-1.01034667,29.5492267 -0.0968533333,23.5810133 4.48768,20.2350933 C9.07221333,16.8896 14.9614933,17.8286933 18.0744533,22.0945067 L18.1056,22.1367467 C21.21856,26.4029867 20.3050667,32.3716267 15.7205333,35.71712 C11.136,39.0626133 5.24672,38.1239467 2.13376,33.8577067 L2.13376,33.8577067 Z M13.93408,25.2462933 L13.9029333,25.2040533 C12.3387733,23.06048 9.42634667,22.3232 7.15562667,23.9803733 C4.90581333,25.6221867 4.73088,28.5469867 6.29504,30.6909867 L6.32618667,30.7332267 C7.89077333,32.8768 10.8027733,33.61408 13.0525867,31.9722667 C15.3237333,30.3150933 15.4986667,27.3902933 13.93408,25.2462933 L13.93408,25.2462933 Z">
|
||||
</path>
|
||||
<path d="M32.1467733,5.89525333 L36.8251733,4.39424 L49.94048,19.6407467 L44.7364267,21.3102933 L42.4571733,18.5924267 L35.70176,20.7598933 L35.4542933,24.2888533 L30.3505067,25.9264 L32.1467733,5.89525333 L32.1467733,5.89525333 Z M39.84384,15.264 L36.2948267,10.9111467 L35.91552,16.5243733 L39.84384,15.264 L39.84384,15.264 Z">
|
||||
</path>
|
||||
<path d="M58.3658667,10.48192 L58.4068267,0.155306667 L63.5831467,0.175786667 L63.5426133,10.39744 C63.5319467,13.0513067 64.8669867,14.3176533 66.9166933,14.3261867 C68.9664,14.3342933 70.3112533,13.1310933 70.3210667,10.55616 L70.3624533,0.20352 L75.5387733,0.224 L75.49824,10.4192 C75.4747733,16.3575467 72.0750933,18.9457067 66.8458667,18.9248 C61.6170667,18.9034667 58.3432533,16.2363733 58.3658667,10.48192 L58.3658667,10.48192 Z">
|
||||
</path>
|
||||
<path d="M94.2331733,8.67754667 L88.95616,7.06688 L90.2600533,2.79466667 L105.6896,7.50378667 L104.385707,11.776 L99.1086933,10.16576 L95.04384,23.4845867 L90.1687467,21.9968 L94.2331733,8.67754667 L94.2331733,8.67754667 Z">
|
||||
</path>
|
||||
<path d="M119.471787,13.4651733 L123.715413,16.28928 L119.901867,22.0202667 L125.348693,25.6452267 L129.162667,19.9138133 L133.406293,22.73792 L123.216213,38.05056 L118.97216,35.2264533 L122.844587,29.4075733 L117.397333,25.7826133 L113.525333,31.6014933 L109.281707,28.7773867 L119.471787,13.4651733 L119.471787,13.4651733 Z">
|
||||
</path>
|
||||
</g>
|
||||
<g transform="translate(65.280000, 196.266667)" fill="#FFFFFF">
|
||||
<path d="M130.622293,3.79008 L130.65472,3.83146667 C133.92896,7.97568 133.243733,13.9754667 128.790187,17.49376 C124.33664,21.0120533 118.41536,20.2986667 115.14112,16.1544533 L115.108693,16.1130667 C111.834453,11.9684267 112.520107,5.96906667 116.973227,2.45077333 C121.426773,-1.06709333 127.348053,-0.354133333 130.622293,3.79008 L130.622293,3.79008 Z M119.158187,12.8469333 L119.190613,12.88832 C120.83584,14.97088 123.774293,15.5963733 125.980587,13.85344 C128.165973,12.12672 128.229547,9.19722667 126.583893,7.11466667 L126.551467,7.07328 C124.906667,4.99072 121.967787,4.36522667 119.7824,6.09194667 C117.576107,7.83488 117.51296,10.7643733 119.158187,12.8469333 L119.158187,12.8469333 Z">
|
||||
</path>
|
||||
<path d="M101.84832,32.9309867 L97.24032,34.6363733 L83.4666667,19.98208 L88.5922133,18.0846933 L90.9892267,20.70016 L97.6426667,18.23744 L97.7344,14.7012267 L102.761387,12.8405333 L101.84832,32.9309867 L101.84832,32.9309867 Z M93.7463467,23.9104 L97.48352,28.1024 L97.61536,22.47808 L93.7463467,23.9104 L93.7463467,23.9104 Z">
|
||||
</path>
|
||||
<path d="M75.4722133,29.4336 L75.9274667,39.7499733 L70.7562667,39.97824 L70.30528,29.7668267 C70.1883733,27.11552 68.7940267,25.91488 66.7464533,26.0053333 C64.69888,26.0957867 63.4133333,27.36256 63.5268267,29.9349333 L63.98336,40.2773333 L58.81216,40.5056 L58.3624533,30.32064 C58.10048,24.3882667 61.37216,21.63968 66.59584,21.40928 C71.8199467,21.1784533 75.2183467,23.6846933 75.4722133,29.4336 L75.4722133,29.4336 Z">
|
||||
</path>
|
||||
<path d="M39.5396267,32.8068267 L44.8674133,34.24512 L43.70304,38.5578667 L28.1250133,34.3530667 L29.2893867,30.0398933 L34.6171733,31.4781867 L38.2468267,18.03136 L43.1688533,19.36 L39.5396267,32.8068267 L39.5396267,32.8068267 Z">
|
||||
</path>
|
||||
<path d="M14.3573333,29.0594133 L9.98442667,26.4388267 L13.5236267,20.5333333 L7.91168,17.1694933 L4.37248,23.0749867 L0,20.4544 L9.45664,4.67669333 L13.82912,7.29770667 L10.2357333,13.2932267 L15.8481067,16.6570667 L19.4414933,10.6615467 L23.8139733,13.28256 L14.3573333,29.0594133 L14.3573333,29.0594133 Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 407.392 407.392" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M402.016,322.382l-88.896-88.902c12.424-17.657,19.143-38.649,19.143-60.69c0-28.254-10.998-54.816-30.978-74.795
|
||||
c-19.979-19.979-46.545-30.982-74.795-30.982c-2.074,0-4.137,0.066-6.19,0.184v30.658c2.05-0.166,4.111-0.27,6.19-0.27
|
||||
c20.09,0,38.971,7.823,53.176,22.028c14.207,14.206,22.029,33.091,22.029,53.177c0,20.09-7.822,38.976-22.029,53.177
|
||||
c-14.205,14.206-33.086,22.029-53.176,22.029c-20.092,0-38.973-7.823-53.178-22.029c-4.686-4.684-8.66-9.885-11.905-15.463
|
||||
c-3.989,2.305-8.61,3.635-13.54,3.635c-9.436,0-17.762-4.836-22.632-12.16c-0.104,0.157-0.22,0.307-0.327,0.462
|
||||
c4.904,16.888,14.006,32.365,26.787,45.145c19.979,19.98,46.543,30.982,74.795,30.982c22.035,0,43.033-6.723,60.684-19.147
|
||||
l88.906,88.903c3.58,3.583,8.273,5.373,12.973,5.373c4.693,0,9.385-1.79,12.965-5.373
|
||||
C409.184,341.159,409.184,329.544,402.016,322.382z"/>
|
||||
<path d="M102.602,135.172c-6.662,0-12.07,5.404-12.07,12.071v39.736c0,6.666,5.408,12.071,12.07,12.071
|
||||
c6.67,0,12.07-5.405,12.07-12.071v-39.736C114.672,140.577,109.271,135.172,102.602,135.172z"/>
|
||||
<path d="M12.07,135.172c-6.66,0-12.07,5.404-12.07,12.071v39.736c0,6.666,5.41,12.071,12.07,12.071
|
||||
c6.67,0,12.07-5.405,12.07-12.071v-39.736C24.141,140.577,18.74,135.172,12.07,135.172z"/>
|
||||
<path d="M57.336,85.381c-6.662,0-12.072,5.405-12.072,12.071v89.527c0,6.666,5.41,12.071,12.072,12.071
|
||||
c6.67,0,12.07-5.405,12.07-12.071V97.452C69.406,90.786,64.006,85.381,57.336,85.381z"/>
|
||||
<path d="M193.139,199.05c6.662,0,12.07-5.405,12.07-12.071V65.766c0-6.666-5.408-12.07-12.07-12.07
|
||||
c-6.67,0-12.078,5.404-12.078,12.07v121.213C181.061,193.645,186.469,199.05,193.139,199.05z"/>
|
||||
<path d="M147.865,199.05c6.67,0,12.072-5.405,12.072-12.071V97.452c0-6.666-5.402-12.071-12.072-12.071
|
||||
c-6.66,0-12.07,5.405-12.07,12.071v89.527C135.795,193.645,141.205,199.05,147.865,199.05z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 238 KiB |
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 419.931 419.931"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M282.895,352.367c-2.176-1.324-4.072-3.099-5.579-5.25c-0.696-0.992-1.284-2.041-1.771-3.125H28.282V100.276h335.624
|
||||
v159.138c7.165,0.647,13.177,5.353,15.701,11.797c2.235-1.225,4.726-1.982,7.344-2.213c1.771-0.154,3.53-0.044,5.236,0.293
|
||||
V39.561c0-12.996-10.571-23.569-23.566-23.569H23.568C10.573,15.992,0,26.565,0,39.561v309.146
|
||||
c0,12.996,10.573,23.568,23.568,23.568h257.179c-2.007-4.064-2.483-8.652-1.302-13.066
|
||||
C280.126,356.67,281.304,354.354,282.895,352.367z M338.025,55.569c0-4.806,3.896-8.703,8.702-8.703h8.702
|
||||
c4.807,0,8.702,3.896,8.702,8.703v9.863c0,4.806-3.896,8.702-8.702,8.702h-8.702c-4.807,0-8.702-3.896-8.702-8.702V55.569z
|
||||
M297.56,55.569c0-4.806,3.896-8.703,8.702-8.703h8.703c4.807,0,8.702,3.896,8.702,8.703v9.863c0,4.806-3.896,8.702-8.702,8.702
|
||||
h-8.703c-4.806,0-8.702-3.896-8.702-8.702V55.569z M257.094,55.569c0-4.806,3.897-8.703,8.702-8.703h8.702
|
||||
c4.807,0,8.703,3.896,8.703,8.703v9.863c0,4.806-3.896,8.702-8.703,8.702h-8.702c-4.805,0-8.702-3.896-8.702-8.702V55.569z"/>
|
||||
<path d="M419.875,335.77l-2.615-14.83c-0.353-1.997-2.256-3.331-4.255-2.979l-13.188,2.324c-1.583-3.715-3.605-7.195-6.005-10.38
|
||||
l8.614-10.268c0.626-0.744,0.931-1.709,0.847-2.68c-0.086-0.971-0.554-1.867-1.3-2.494l-11.534-9.68
|
||||
c-0.746-0.626-1.713-0.93-2.683-0.845c-0.971,0.085-1.867,0.552-2.493,1.298l-8.606,10.26c-3.533-1.8-7.312-3.188-11.271-4.104
|
||||
v-13.392c0-2.028-1.645-3.674-3.673-3.674h-15.06c-2.027,0-3.673,1.646-3.673,3.674v13.392
|
||||
c-3.961,0.915-7.736,2.304-11.271,4.104l-8.608-10.259c-1.304-1.554-3.62-1.756-5.175-0.453l-11.535,9.679
|
||||
c-0.746,0.627-1.213,1.523-1.299,2.494c-0.084,0.971,0.22,1.937,0.846,2.683l8.615,10.266c-2.396,3.184-4.422,6.666-6.005,10.38
|
||||
l-13.188-2.325c-1.994-0.351-3.901,0.982-4.255,2.979l-2.614,14.83c-0.169,0.959,0.05,1.945,0.607,2.744
|
||||
c0.561,0.799,1.41,1.342,2.37,1.511l13.198,2.326c0.215,4.089,0.927,8.045,2.073,11.812l-11.6,6.695
|
||||
c-0.844,0.485-1.459,1.289-1.712,2.229c-0.252,0.941-0.119,1.943,0.367,2.787l7.529,13.041c0.485,0.844,1.289,1.459,2.229,1.711
|
||||
c0.313,0.084,0.632,0.125,0.951,0.125c0.639,0,1.272-0.166,1.836-0.492l11.609-6.703c2.73,2.925,5.812,5.517,9.18,7.709
|
||||
l-4.584,12.593c-0.332,0.916-0.289,1.926,0.123,2.809s1.157,1.566,2.072,1.898l14.148,5.149c0.406,0.148,0.832,0.224,1.257,0.224
|
||||
c0.53,0,1.063-0.115,1.554-0.345c0.883-0.411,1.564-1.157,1.897-2.073l4.583-12.593c1.965,0.238,3.965,0.361,5.994,0.361
|
||||
s4.029-0.125,5.994-0.361l4.584,12.593c0.332,0.916,1.016,1.662,1.897,2.073c0.49,0.229,1.021,0.345,1.554,0.345
|
||||
c0.424,0,0.85-0.074,1.256-0.224l14.15-5.149c0.913-0.332,1.659-1.017,2.07-1.898c0.412-0.883,0.456-1.893,0.123-2.809
|
||||
l-4.584-12.591c3.365-2.192,6.447-4.786,9.18-7.709l11.609,6.703c0.563,0.324,1.197,0.492,1.836,0.492
|
||||
c0.318,0,0.64-0.043,0.951-0.125c0.941-0.252,1.743-0.869,2.229-1.711l7.529-13.043c0.486-0.842,0.619-1.846,0.367-2.787
|
||||
c-0.253-0.938-0.868-1.742-1.712-2.229l-11.598-6.693c1.146-3.768,1.856-7.724,2.071-11.812l13.198-2.327
|
||||
c0.96-0.169,1.812-0.712,2.37-1.511C419.825,337.715,420.044,336.729,419.875,335.77z M354.184,359.336
|
||||
c-11.155,0-20.2-9.045-20.2-20.201s9.046-20.2,20.2-20.2c11.156,0,20.201,9.044,20.201,20.2S365.34,359.336,354.184,359.336z"/>
|
||||
<g>
|
||||
<path d="M164.695,235.373c0-4.752-2.785-9.117-7.096-11.119l-39.455-18.332l39.456-18.334c4.31-2.004,7.095-6.368,7.095-11.118
|
||||
v-0.319c0-4.21-2.119-8.075-5.665-10.334c-1.962-1.253-4.247-1.916-6.606-1.916c-1.778,0-3.563,0.391-5.16,1.133l-63.078,29.333
|
||||
c-4.309,2.004-7.092,6.368-7.092,11.117v0.877c0,4.743,2.782,9.104,7.093,11.118l63.084,29.336
|
||||
c1.631,0.755,3.368,1.138,5.162,1.138c2.338,0,4.616-0.664,6.597-1.924c3.548-2.268,5.666-6.13,5.666-10.335L164.695,235.373
|
||||
L164.695,235.373z"/>
|
||||
<path d="M226.932,134.012c-2.301-3.15-6.002-5.03-9.901-5.03h-0.314c-5.354,0-10.048,3.425-11.679,8.516L163.478,266.27
|
||||
c-1.183,3.718-0.517,7.813,1.781,10.962c2.301,3.148,6.002,5.029,9.901,5.029h0.315c5.352,0,10.043-3.426,11.672-8.516
|
||||
l41.555-128.762C229.896,141.268,229.234,137.167,226.932,134.012z"/>
|
||||
<path d="M308.001,194.366l-63.079-29.333c-1.592-0.74-3.374-1.131-5.152-1.131c-2.358,0-4.644,0.661-6.605,1.912
|
||||
c-3.552,2.263-5.671,6.127-5.671,10.337v0.319c0,4.746,2.783,9.111,7.097,11.123l39.454,18.33l-39.455,18.331
|
||||
c-4.311,2.002-7.096,6.367-7.096,11.119v0.321c0,4.205,2.119,8.066,5.669,10.336c1.974,1.258,4.254,1.923,6.595,1.923
|
||||
c1.792,0,3.527-0.383,5.169-1.141l63.082-29.336c4.307-2.009,7.088-6.371,7.088-11.114v-0.877
|
||||
C315.094,200.735,312.311,196.371,308.001,194.366z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Programming/ERSMS-project/docs/final_documentation/main.pdf
Normal file
79
Programming/ERSMS-project/docs/final_documentation/main.tex
Normal file
@ -0,0 +1,79 @@
|
||||
\documentclass{article}[11pt]
|
||||
\usepackage{graphicx}
|
||||
\usepackage{float}
|
||||
|
||||
\title{ERSMS, Group F, Project Documentation}
|
||||
\author{Hubert Dwornik, Michał Łezka, Jakub Mazur \\ Michał Sar, Krzysztof Rudnicki}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\section{System Architecture}
|
||||
We have designed and implemented 4 microservices,
|
||||
all of microservices are written in Python using Flask framework
|
||||
\paragraph{AI recommendations}
|
||||
Based on a list of movies ids, calculates and returns list of ids recommended for user who likes given movies
|
||||
\paragraph{Analytics}
|
||||
Holds information about number of
|
||||
ratings, number of users,
|
||||
average movie ratings, ratings of
|
||||
given user which are later used in
|
||||
webinterface to show data for admins
|
||||
concerning website usage
|
||||
\paragraph{Notifications}
|
||||
Notifies user whenever there is a new movie recommend by AI recommender for them
|
||||
\paragraph{Backend}
|
||||
Updates database and mantains all ongoing and incoming communication between all microservices, both between microservies and from microservices to the webinterface
|
||||
\paragraph{Caching}
|
||||
We implemented two caches
|
||||
\begin{enumerate}
|
||||
\item Backend is cached inside Analytics Service \\
|
||||
Analytics service holds tata about users and movies,
|
||||
in order to not pull all the data from backend every time we update analytics
|
||||
(for example every day), we keep the cache of backend date in our analytics
|
||||
service
|
||||
\item AI recommendations are cached inside Notification Service \\
|
||||
AI recommendations can change whenever new movie appears, notification
|
||||
service keeps the cache of ai recommendation service in order to not
|
||||
pull all the data from the ai recommendation every time it wants to notify
|
||||
the users about new movies to recommend
|
||||
\end{enumerate}
|
||||
\paragraph{Database}
|
||||
We use postgresql database to contain data about users, movies and user ratings
|
||||
|
||||
|
||||
\begin{figure}[H]
|
||||
\caption{System architecture representation, webinterface although not part of microservices included to show relation with backend}
|
||||
\centering
|
||||
\includegraphics[width=\textwidth]{images/systemArchitecture.drawio.pdf}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\section{Automated Infrastructure Management solution}
|
||||
We use \textbf{Dockerfiles}
|
||||
for each microservice, webinterface and database which
|
||||
later get combined in docker compose file, after each commit on
|
||||
\textbf{GitHub} main repository docker compose gets automatically run
|
||||
on \textbf{Google Cloud} platform and deployed
|
||||
\section{Federated authorization and authentication management in the project}
|
||||
We use industry standard \textbf{OAuth} protocol in our webinterface,
|
||||
user creates their account and logs in, we use user token to
|
||||
authorize their access on backend to their ratings and
|
||||
recommendations. We use \textbf{firebase} services to manage OAuth protocol.
|
||||
\section{Threat model with mitigations}
|
||||
Our single most important asset are user likes for specific movies \\
|
||||
We expect either bots or human agents trying to access those likes for a specific users or to modify user ratings to improve or decrease certain movies ratings \\
|
||||
To mitigate that we use:
|
||||
\begin{enumerate}
|
||||
\item Certificates on our frontend, which encrypt data transmitted between website and user
|
||||
\item OAuth which is used to authenticate user and lower amount of bots accessing our Infrastructure
|
||||
\item TLS encryption between our microservices so that even our inside communication is encrypted
|
||||
\item Google cloud default security policies allowing us to monitor odd and potentially harmfull behaviours
|
||||
\end{enumerate}
|
||||
\begin{figure}[H]
|
||||
\caption{Threat model}
|
||||
\centering
|
||||
\includegraphics[width=\textwidth]{images/threat_model.png}
|
||||
\end{figure}
|
||||
|
||||
|
||||
\end{document}
|
||||
25
Programming/ERSMS-project/docs/toDos/deployment_script.txt
Normal file
@ -0,0 +1,25 @@
|
||||
One script upon being ran should deploy the entire solution to some
|
||||
cloud service
|
||||
|
||||
It should:
|
||||
1. Build everything
|
||||
2. Connect to cloud service (Azure?)
|
||||
3. Send the data
|
||||
|
||||
After it will be run website should be accessible under some address
|
||||
(cloud service should provide this address?)
|
||||
|
||||
Decide:
|
||||
What cloud service? (Azure?) Requirements:
|
||||
a. Free (https://github.com/cloudcommunity/Cloud-Free-Tier-Comparison)
|
||||
b. Popular
|
||||
AWS:
|
||||
+ Most popular
|
||||
+ "Always" free
|
||||
+ AWS CDK available
|
||||
Azure
|
||||
-Microsoft
|
||||
Google Cloud
|
||||
+We have google accounts anyway
|
||||
What technology for script (Ansible?)
|
||||
|
||||
12
Programming/ERSMS-project/docs/toDos/monitoring_service.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Monitoring service
|
||||
Monitoring service should keep track of all communication
|
||||
send between all services
|
||||
Store logs with:
|
||||
1. timestamp (when was a message send)
|
||||
2. Message raw data
|
||||
|
||||
To decide:
|
||||
What to use for Monitoring service?
|
||||
Maybe cloud service will provide us with functioning one?
|
||||
Maybe deployment tool already has one?
|
||||
|
||||
19
Programming/ERSMS-project/frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
24
Programming/ERSMS-project/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
Programming/ERSMS-project/frontend/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
19
Programming/ERSMS-project/frontend/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM node:18-alpine AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
13
Programming/ERSMS-project/frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Movie Recommendation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="flex min-h-dvh flex-col bg-gray-800 text-white"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5548
Programming/ERSMS-project/frontend/package-lock.json
generated
Normal file
43
Programming/ERSMS-project/frontend/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "movie-recommendation-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@smastrom/react-rating": "^1.5.0",
|
||||
"axios": "^1.6.8",
|
||||
"firebase": "^10.11.1",
|
||||
"react": "^18.2.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-firebase-hooks": "^5.1.1",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"recharts": "^2.12.7",
|
||||
"swr": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-mkcert": "^1.17.5"
|
||||
}
|
||||
}
|
||||
6
Programming/ERSMS-project/frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
Programming/ERSMS-project/frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
26
Programming/ERSMS-project/frontend/src/App.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||
import Layout from './Layout';
|
||||
import Home from './pages/Home';
|
||||
import Recommendations from './pages/Recommendations';
|
||||
import Rate from './pages/Rate';
|
||||
import Analytics from './pages/Analytics';
|
||||
|
||||
function App() {
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{ path: '/', element: <Home /> },
|
||||
{ path: '/recommendations', element: <Recommendations /> },
|
||||
{ path: '/rate', element: <Rate /> },
|
||||
{ path: '/analytics', element: <Analytics /> },
|
||||
{ path: '*', element: <code>404</code> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
32
Programming/ERSMS-project/frontend/src/Layout.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAuthState } from 'react-firebase-hooks/auth';
|
||||
import { Triangle } from 'react-loader-spinner';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Footer from './components/Footer';
|
||||
import { auth } from './firebase';
|
||||
|
||||
function Layout() {
|
||||
const [user, userLoading] = useAuthState(auth);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!userLoading && !user) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate, user, userLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
{userLoading ? (
|
||||
<Triangle wrapperClass="m-auto" height={120} width={120} />
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
1
Programming/ERSMS-project/frontend/src/assets/google.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#fff" d="M44.59 4.21a63.28 63.28 0 004.33 120.9 67.6 67.6 0 0032.36.35 57.13 57.13 0 0025.9-13.46 57.44 57.44 0 0016-26.26 74.33 74.33 0 001.61-33.58H65.27v24.69h34.47a29.72 29.72 0 01-12.66 19.52 36.16 36.16 0 01-13.93 5.5 41.29 41.29 0 01-15.1 0A37.16 37.16 0 0144 95.74a39.3 39.3 0 01-14.5-19.42 38.31 38.31 0 010-24.63 39.25 39.25 0 019.18-14.91A37.17 37.17 0 0176.13 27a34.28 34.28 0 0113.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.22 61.22 0 0087.2 4.59a64 64 0 00-42.61-.38z"/><path fill="#e33629" d="M44.59 4.21a64 64 0 0142.61.37 61.22 61.22 0 0120.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.28 34.28 0 00-13.64-8 37.17 37.17 0 00-37.46 9.74 39.25 39.25 0 00-9.18 14.91L8.76 35.6A63.53 63.53 0 0144.59 4.21z"/><path fill="#f8bd00" d="M3.26 51.5a62.93 62.93 0 015.5-15.9l20.73 16.09a38.31 38.31 0 000 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 01-5.5-40.9z"/><path fill="#587dbd" d="M65.27 52.15h59.52a74.33 74.33 0 01-1.61 33.58 57.44 57.44 0 01-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0012.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68z"/><path fill="#319f43" d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0044 95.74a37.16 37.16 0 0014.08 6.08 41.29 41.29 0 0015.1 0 36.16 36.16 0 0013.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 01-25.9 13.47 67.6 67.6 0 01-32.36-.35 63 63 0 01-23-11.59A63.73 63.73 0 018.75 92.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="790" height="512.20805" viewBox="0 0 790 512.20805" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M925.56335,704.58909,903,636.49819s24.81818,24.81818,24.81818,45.18181l-4.45454-47.09091s12.72727,17.18182,11.45454,43.27273S925.56335,704.58909,925.56335,704.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M441.02093,642.58909,419,576.13509s24.22155,24.22155,24.22155,44.09565l-4.34745-45.95885s12.42131,16.76877,11.17917,42.23245S441.02093,642.58909,441.02093,642.58909Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><path d="M784.72555,673.25478c.03773,43.71478-86.66489,30.26818-192.8092,30.35979s-191.53562,13.68671-191.57335-30.028,86.63317-53.29714,192.77748-53.38876S784.68782,629.54,784.72555,673.25478Z" transform="translate(-205 -193.89598)" fill="#e6e6e6"/><rect y="509.69312" width="790" height="2" fill="#3f3d56"/><polygon points="505.336 420.322 491.459 420.322 484.855 366.797 505.336 366.797 505.336 420.322" fill="#a0616a"/><path d="M480.00587,416.35743H508.3101a0,0,0,0,1,0,0V433.208a0,0,0,0,1,0,0H464.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,480.00587,416.35743Z" fill="#2f2e41"/><polygon points="607.336 499.322 593.459 499.322 586.855 445.797 607.336 445.797 607.336 499.322" fill="#a0616a"/><path d="M582.00587,495.35743H610.3101a0,0,0,0,1,0,0V512.208a0,0,0,0,1,0,0H566.69674a0,0,0,0,1,0,0v-1.54149A15.30912,15.30912,0,0,1,582.00587,495.35743Z" fill="#2f2e41"/><path d="M876.34486,534.205A10.31591,10.31591,0,0,0,873.449,518.654l-32.23009-131.2928L820.6113,396.2276l38.33533,126.949a10.37185,10.37185,0,0,0,17.39823,11.0284Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M851.20767,268.85955a11.38227,11.38227,0,0,0-17.41522,1.15247l-49.88538,5.72709,7.58861,19.24141,45.36779-8.49083a11.44393,11.44393,0,0,0,14.3442-17.63014Z" transform="translate(-205 -193.89598)" fill="#a0616a"/><path d="M769,520.58909l21.76811,163.37417,27.09338-5.578s-3.98437-118.98157,9.56238-133.32513S810,505.58909,810,505.58909Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M778,475.58909l-10,15s-77-31.99929-77,19-4.40631,85.60944-6,88,18.43762,8.59375,28,7c0,0,11.79687-82.21884,11-87,0,0,75.53355,37.03335,89.87712,33.84591S831.60944,536.964,834,530.58909s-1-57-1-57l-47.81-14.59036Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><path d="M779.34915,385.52862l-2.85032-3.42039s-31.92361-71.82815-19.3822-91.21035,67.26762-22.23252,68.97783-21.0924-4.08488,15.9428-.09446,22.78361c0,0-42.394,9.19121-45.24435,10.33134s21.96615,43.2737,21.96615,43.2737l-2.85031,25.6529Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M835.21549,350.18459S805.57217,353.605,804.432,353.605s-1.71017-7.41084-1.71017-7.41084l-26.223,35.91406S763.57961,486.29929,767,484.58909s66.50531,8.11165,67.07539,3.55114-.57008-27.3631,1.14014-28.50324,29.64328-71.82811,29.64328-71.82811-2.85032-14.82168-12.54142-19.95227S835.21549,350.18459,835.21549,350.18459Z" transform="translate(-205 -193.89598)" fill="#ccc"/><path d="M855.73783,378.11779l9.121,9.69109S878.41081,499.1687,871,502.58909s-22,3-22,3l-14.35458-52.79286Z" transform="translate(-205 -193.89598)" fill="#ccc"/><circle cx="601.72966" cy="122.9976" r="26.2388" fill="#a0616a"/><path d="M800.57267,320.98789c-.35442-5.44445-7.22306-5.631-12.67878-5.68255s-11.97836.14321-15.0654-4.35543c-2.0401-2.973-1.65042-7.10032.035-10.28779s4.45772-5.639,7.18508-7.99742c7.04139-6.08884,14.29842-12.12936,22.7522-16.02662s18.36045-5.472,27.12788-2.3435c10.77008,3.84307,25.32927,23.62588,26.5865,34.99176s-3.28507,22.95252-10.9419,31.44586-25.18188,5.0665-36.21069,8.088c6.7049-9.48964,2.28541-26.73258-8.45572-31.164Z" transform="translate(-205 -193.89598)" fill="#2f2e41"/><circle cx="361.7217" cy="403.5046" r="62.98931" fill="#f9c326"/><path d="M524.65625,529.9355a45.15919,45.15919,0,0,1-41.25537-26.78614L383.44873,278.05757a59.83039,59.83039,0,1,1,111.87012-41.86426l72.37744,235.41211a45.07978,45.07978,0,0,1-43.04,58.33008Z" transform="translate(-205 -193.89598)" fill="#f9c326"/></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
1
Programming/ERSMS-project/frontend/src/assets/tmdb.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 423.04 35.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="17.7" x2="423.04" y2="17.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M227.5,0h8.9l8.75,23.2h.1L254.15,0h8.35L247.9,35.4h-6.25Zm46.6,0h7.8V35.4h-7.8Zm22.2,0h24.05V7.2H304.1v6.6h15.35V21H304.1v7.2h17.15v7.2H296.3Zm55,0H363a33.54,33.54,0,0,1,8.07,1A18.55,18.55,0,0,1,377.75,4a15.1,15.1,0,0,1,4.52,5.53A18.5,18.5,0,0,1,384,17.8a16.91,16.91,0,0,1-1.63,7.58,16.37,16.37,0,0,1-4.37,5.5,19.52,19.52,0,0,1-6.35,3.37A24.59,24.59,0,0,1,364,35.4H351.29Zm7.81,28.2h4a21.57,21.57,0,0,0,5-.55,10.87,10.87,0,0,0,4-1.83,8.69,8.69,0,0,0,2.67-3.34,11.92,11.92,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9,9,0,0,0-2.62-3.18,11.68,11.68,0,0,0-3.88-1.88,17.43,17.43,0,0,0-4.67-.62h-4.6ZM395.24,0h13.2a34.42,34.42,0,0,1,4.63.32,12.9,12.9,0,0,1,4.17,1.3,7.88,7.88,0,0,1,3,2.73A8.34,8.34,0,0,1,421.39,9a7.42,7.42,0,0,1-1.67,5,9.28,9.28,0,0,1-4.43,2.82v.1a10,10,0,0,1,3.18,1,8.38,8.38,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.42,9.42,0,0,1-3.1,3,13.38,13.38,0,0,1-4.27,1.65,23.11,23.11,0,0,1-4.73.5h-14.5ZM403,14.15h5.65a8.16,8.16,0,0,0,1.78-.2A4.78,4.78,0,0,0,412,13.3a3.34,3.34,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8,3.22,3.22,0,0,0-.47-1.82,3.33,3.33,0,0,0-1.23-1.13,5.77,5.77,0,0,0-1.7-.58,10.79,10.79,0,0,0-1.85-.17H403Zm0,14.65h7a8.91,8.91,0,0,0,1.83-.2,4.78,4.78,0,0,0,1.67-.7,4,4,0,0,0,1.23-1.3,3.71,3.71,0,0,0,.47-2,3.13,3.13,0,0,0-.62-2A4,4,0,0,0,413,21.45,7.83,7.83,0,0,0,411,20.9a15.12,15.12,0,0,0-2.05-.15H403Zm-199,6.53H205a17.66,17.66,0,0,0,17.66-17.66h0A17.67,17.67,0,0,0,205,0h-.91A17.67,17.67,0,0,0,186.4,17.67h0A17.66,17.66,0,0,0,204.06,35.33ZM10.1,6.9H0V0H28V6.9H17.9V35.4H10.1ZM39,0h7.8V13.2H61.9V0h7.8V35.4H61.9V20.1H46.75V35.4H39ZM80.2,0h24V7.2H88v6.6h15.35V21H88v7.2h17.15v7.2h-25Zm55,0H147l8.15,23.1h.1L163.45,0H175.2V35.4h-7.8V8.25h-.1L158,35.4h-5.95l-9-27.15H143V35.4h-7.8Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
30
Programming/ERSMS-project/frontend/src/components/Footer.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import tmdb from '../assets/tmdb.svg';
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<div className="mt-auto mx-auto">
|
||||
<footer className="py-6 px-4 mt-16 flex flex-col items-center gap-4">
|
||||
<a
|
||||
href="https://www.themoviedb.org"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
<img src={tmdb} alt="The Movie Database" className="w-[180px]" />
|
||||
</a>
|
||||
<small className="text-sm text-[#a0d7d8] text-center leading-relaxed">
|
||||
<span>This product uses the </span>
|
||||
<a
|
||||
href="https://www.themoviedb.org"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
TMDB
|
||||
</a>
|
||||
<span> API but is not endorsed or certified by TMDB.</span>
|
||||
</small>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
61
Programming/ERSMS-project/frontend/src/components/Navbar.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useAuthState, useSignOut } from 'react-firebase-hooks/auth';
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { auth } from '../firebase';
|
||||
|
||||
function Navbar() {
|
||||
const [user, userLoading] = useAuthState(auth);
|
||||
const [signOut] = useSignOut(auth);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSignOut = () => {
|
||||
navigate('/');
|
||||
signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="px-6 py-4 flex justify-between items-center">
|
||||
<Link to="/">
|
||||
<h1 className="text-xl sm:text-2xl">Movie Recommendation</h1>
|
||||
</Link>
|
||||
{userLoading && <p>Loading user info</p>}
|
||||
{!userLoading && !user && (
|
||||
<p className="text-gray-300 text-lg">Logged out</p>
|
||||
)}
|
||||
{!!user && (
|
||||
<div className="flex gap-8">
|
||||
<nav>
|
||||
<ul className="flex flex-col text-center gap-4 md:flex-row">
|
||||
<li>
|
||||
<NavLink
|
||||
to="/recommendations"
|
||||
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||
>
|
||||
Recommendations
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/rate"
|
||||
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||
>
|
||||
Rate
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/analytics"
|
||||
className={({ isActive }) => (isActive ? 'font-medium' : '')}
|
||||
>
|
||||
Analytics
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<button onClick={handleSignOut}>Log out</button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuthState } from 'react-firebase-hooks/auth';
|
||||
import { useCollectionData } from 'react-firebase-hooks/firestore';
|
||||
import {
|
||||
addDoc,
|
||||
collection,
|
||||
deleteDoc,
|
||||
query,
|
||||
setDoc,
|
||||
where,
|
||||
} from 'firebase/firestore';
|
||||
import { Rating, RatingChange } from '@smastrom/react-rating';
|
||||
import { auth, db } from '../firebase';
|
||||
import TMDBMovie from '../types/TMDBMovie';
|
||||
import posterPlaceholder from '../assets/poster_placeholder.svg';
|
||||
|
||||
interface RateMovieProps {
|
||||
movie: TMDBMovie;
|
||||
}
|
||||
|
||||
const ratingsRef = collection(db, 'ratings');
|
||||
|
||||
function RateMovie({ movie }: RateMovieProps) {
|
||||
const [user] = useAuthState(auth);
|
||||
|
||||
if (!user) throw new Error('No user');
|
||||
|
||||
const [rating, setRating] = useState<number | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const q = query(
|
||||
ratingsRef,
|
||||
where('userId', '==', user.uid),
|
||||
where('movieId', '==', movie.id),
|
||||
);
|
||||
|
||||
const [values, isLoading, , snapshot] = useCollectionData<{
|
||||
userId?: string;
|
||||
movieId?: number;
|
||||
rating?: number;
|
||||
}>(q);
|
||||
|
||||
const dbRating = values?.[0]?.rating;
|
||||
const docRef = snapshot?.docs[0]?.ref;
|
||||
|
||||
useEffect(() => {
|
||||
setRating(dbRating || null);
|
||||
}, [dbRating]);
|
||||
|
||||
const handleChange: RatingChange = async (value: number) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
const data = {
|
||||
userId: user.uid,
|
||||
movieId: movie.id,
|
||||
rating: value,
|
||||
};
|
||||
|
||||
try {
|
||||
if (docRef) {
|
||||
if (value !== 0) {
|
||||
await setDoc(docRef, data);
|
||||
} else {
|
||||
await deleteDoc(docRef);
|
||||
}
|
||||
} else {
|
||||
await addDoc(ratingsRef, data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const posterUrl = `https://image.tmdb.org/t/p/w154${movie.poster_path}`;
|
||||
|
||||
return (
|
||||
<article className="flex flex-col items-center w-[250px]">
|
||||
<img
|
||||
src={movie.poster_path ? posterUrl : posterPlaceholder}
|
||||
alt={movie.title}
|
||||
className="w-[154px] h-[231px] object-contain mb-2"
|
||||
/>
|
||||
<h3 className="my-auto text-lg text-center text-gray-200 font-medium">
|
||||
{movie.title}
|
||||
</h3>
|
||||
<Rating
|
||||
value={rating || 0}
|
||||
onChange={handleChange}
|
||||
isDisabled={isLoading || isProcessing}
|
||||
className="max-w-[150px] mt-2"
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default RateMovie;
|
||||
@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
|
||||
import IRecommendation from '../types/Recommendation';
|
||||
import TMDBMovie from '../types/TMDBMovie';
|
||||
import posterPlaceholder from '../assets/poster_placeholder.svg';
|
||||
|
||||
interface RecommendationProps {
|
||||
recommendation: IRecommendation;
|
||||
}
|
||||
|
||||
function Recommendation({ recommendation }: RecommendationProps) {
|
||||
const [movie, setMovie] = useState<TMDBMovie | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const url = `https://api.themoviedb.org/3/movie/${recommendation.movieId}`;
|
||||
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${import.meta.env.VITE_TMDB_BEARER_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
const movieDetails = res.data as TMDBMovie;
|
||||
|
||||
setMovie(movieDetails);
|
||||
})();
|
||||
}, [recommendation.movieId]);
|
||||
|
||||
if (!movie) return null;
|
||||
|
||||
const posterUrl = `https://image.tmdb.org/t/p/w154${movie.poster_path}`;
|
||||
|
||||
return (
|
||||
<article className="flex flex-col items-center w-[250px]">
|
||||
<img
|
||||
src={movie.poster_path ? posterUrl : posterPlaceholder}
|
||||
alt={movie.title}
|
||||
className="w-[154px] h-[231px] object-contain mb-2"
|
||||
/>
|
||||
<h3 className="my-auto text-lg text-center text-gray-200 font-medium">
|
||||
{movie.title}
|
||||
</h3>
|
||||
<CircularProgressbar
|
||||
value={recommendation.match}
|
||||
text={`${recommendation.match}%`}
|
||||
className="size-[75px] mt-4"
|
||||
strokeWidth={11}
|
||||
circleRatio={0.75}
|
||||
styles={buildStyles({
|
||||
rotation: 1 / 2 + 1 / 8,
|
||||
strokeLinecap: 'butt',
|
||||
trailColor: '#eee',
|
||||
textSize: '24px',
|
||||
textColor: '#fff',
|
||||
})}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default Recommendation;
|
||||
@ -0,0 +1,27 @@
|
||||
import { RotatingLines } from 'react-loader-spinner';
|
||||
|
||||
interface RefreshProps {
|
||||
mutate: () => void;
|
||||
isValidating: boolean;
|
||||
}
|
||||
|
||||
function Refresh({ mutate, isValidating }: RefreshProps) {
|
||||
return (
|
||||
<div className="flex relative">
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
disabled={isValidating}
|
||||
className={`mt-10 mb-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100 ${isValidating ? 'cursor-wait' : ''}`}
|
||||
>
|
||||
{isValidating ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
{isValidating && (
|
||||
<div className="absolute -right-2 translate-x-full top-1/2 -translate-y-1/2">
|
||||
<RotatingLines width="24" strokeColor="#fff" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Refresh;
|
||||
@ -0,0 +1,63 @@
|
||||
import {
|
||||
collection,
|
||||
getCountFromServer,
|
||||
query,
|
||||
where,
|
||||
} from 'firebase/firestore';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuthState } from 'react-firebase-hooks/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { auth, db } from '../firebase';
|
||||
|
||||
const ratingsRef = collection(db, 'ratings');
|
||||
|
||||
function Welcome() {
|
||||
const [user] = useAuthState(auth);
|
||||
const [ratingsCount, setRatingsCount] = useState<number | null>(null);
|
||||
|
||||
if (!user) throw new Error('No user');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const ratingsQuery = query(ratingsRef, where('userId', '==', user.uid));
|
||||
const snapshot = await getCountFromServer(ratingsQuery);
|
||||
|
||||
setRatingsCount(snapshot.data().count);
|
||||
})();
|
||||
}, [user.uid]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<p className="text-lg mb-12">Hello {user.displayName}!</p>
|
||||
{ratingsCount === null && <p>Loading your data</p>}
|
||||
{Number.isInteger(ratingsCount) &&
|
||||
(ratingsCount === 0 ? (
|
||||
<p className="leading-loose">
|
||||
ℹ️
|
||||
<br />
|
||||
You don't have any ratings yet.
|
||||
<br />
|
||||
<span>Please </span>
|
||||
<Link to="/rate" className="underline">
|
||||
rate
|
||||
</Link>
|
||||
<span> your first movie.</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="leading-loose">
|
||||
<span>You have </span>
|
||||
<span className="text-yellow-400 font-bold">{ratingsCount}</span>
|
||||
<span> movie rating{ratingsCount! > 1 ? 's' : ''}!</span>
|
||||
<br />
|
||||
<span>Go to </span>
|
||||
<Link to="/recommendations" className="underline">
|
||||
recommendations
|
||||
</Link>
|
||||
<span> to get inspired.</span>
|
||||
</p>
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Welcome;
|
||||
18
Programming/ERSMS-project/frontend/src/firebase.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: 'AIzaSyAF34ovW0qaDlV68sbpCjylz4TQ0he_XEM',
|
||||
authDomain: 'movie-recommendation-2024.firebaseapp.com',
|
||||
projectId: 'movie-recommendation-2024',
|
||||
storageBucket: 'movie-recommendation-2024.appspot.com',
|
||||
messagingSenderId: '777010585410',
|
||||
appId: '1:777010585410:web:7db1436b72879d512032db',
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
const db = getFirestore(app);
|
||||
|
||||
export { auth, db };
|
||||
3
Programming/ERSMS-project/frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
12
Programming/ERSMS-project/frontend/src/main.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
import '@smastrom/react-rating/style.css';
|
||||
import 'react-circular-progressbar/dist/styles.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
73
Programming/ERSMS-project/frontend/src/pages/Analytics.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { ThreeDots } from 'react-loader-spinner';
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import useSWR, { Fetcher } from 'swr';
|
||||
import Refresh from '../components/Refresh';
|
||||
|
||||
interface VisitorsData {
|
||||
date: string;
|
||||
visitors: number;
|
||||
}
|
||||
|
||||
const fetcher: Fetcher<VisitorsData[], string> = async () => {
|
||||
const res = {
|
||||
data: {
|
||||
visitors: [
|
||||
{ date: '2024-05-10', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-11', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-12', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-13', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-14', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-15', visitors: Math.floor(Math.random() * 10) },
|
||||
{ date: '2024-05-16', visitors: Math.floor(Math.random() * 10) },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1500));
|
||||
|
||||
return res.data.visitors as VisitorsData[];
|
||||
};
|
||||
|
||||
function Analytics() {
|
||||
const { data, isLoading, isValidating, mutate } = useSWR('visitors', fetcher);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center px-6 flex-1">
|
||||
<h2 className="text-2xl font-medium">Analytics</h2>
|
||||
{isLoading && (
|
||||
<div className="my-auto flex flex-col items-center gap-2">
|
||||
<ThreeDots />
|
||||
<p className="italic">Loading analytics...</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && <Refresh mutate={mutate} isValidating={isValidating} />}
|
||||
{!isLoading && !data?.length && <p>Try again later 😬</p>}
|
||||
{!isLoading && !!data?.length && (
|
||||
<div className="w-full max-w-6xl text-[#2a5e87] my-auto">
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{ top: 5, right: 30, bottom: 5, left: 0 }}
|
||||
>
|
||||
<Legend verticalAlign="top" height={30} />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Area type="monotone" dataKey="visitors" />
|
||||
<Tooltip />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Analytics;
|
||||
33
Programming/ERSMS-project/frontend/src/pages/Home.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth';
|
||||
import Welcome from '../components/Welcome';
|
||||
import { auth } from '../firebase';
|
||||
import googleLogo from '../assets/google.svg';
|
||||
|
||||
function Home() {
|
||||
const [user] = useAuthState(auth);
|
||||
const [signInWithGoogle] = useSignInWithGoogle(auth);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center mt-16 mx-4 text-center">
|
||||
<h2 className="text-2xl font-medium mb-8">
|
||||
Find the perfect movie to watch tonight.
|
||||
</h2>
|
||||
{!user && (
|
||||
<>
|
||||
<p className="mt-4">But first you need to log in:</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => signInWithGoogle()}
|
||||
className="mt-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100"
|
||||
>
|
||||
<img src={googleLogo} alt="Google" className="size-8" />
|
||||
<span>Log in with Google</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!!user && <Welcome />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
52
Programming/ERSMS-project/frontend/src/pages/Rate.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
import { useState } from 'react';
|
||||
import { ThreeDots } from 'react-loader-spinner';
|
||||
import useSWR, { Fetcher } from 'swr';
|
||||
import RateMovie from '../components/RateMovie';
|
||||
import TMDBMovie from '../types/TMDBMovie';
|
||||
|
||||
const fetcher: Fetcher<TMDBMovie[], string> = async (url) => {
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${import.meta.env.VITE_TMDB_BEARER_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
return res.data.results as TMDBMovie[];
|
||||
};
|
||||
|
||||
function Rate() {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const tmdbQuery = value
|
||||
? `https://api.themoviedb.org/3/search/movie?query=${value}`
|
||||
: `https://api.themoviedb.org/3/trending/movie/day`;
|
||||
|
||||
const { data, isLoading } = useSWR(tmdbQuery, fetcher);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center px-6">
|
||||
<h2 className="text-2xl font-medium">Rate a movie</h2>
|
||||
<div className="max-w-full w-[600px]">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Find a movie..."
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="mt-12 bg-gray-700 rounded-full py-2 px-4 mb-12 w-full"
|
||||
/>
|
||||
</div>
|
||||
{isLoading && <ThreeDots />}
|
||||
{!isLoading && !data?.length && <p>Could not find a movie 😬</p>}
|
||||
{!isLoading && !!data?.length && (
|
||||
<section className="flex flex-wrap gap-x-6 gap-y-9 justify-center">
|
||||
{data.slice(0, 12).map((movie) => (
|
||||
<RateMovie movie={movie} key={movie.id} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Rate;
|
||||
@ -0,0 +1,62 @@
|
||||
import { useAuthState } from 'react-firebase-hooks/auth';
|
||||
import { ThreeDots } from 'react-loader-spinner';
|
||||
import useSWR, { Fetcher } from 'swr';
|
||||
import Recommendation from '../components/Recommendation';
|
||||
import Refresh from '../components/Refresh';
|
||||
import { auth } from '../firebase';
|
||||
import IRecommendation from '../types/Recommendation';
|
||||
|
||||
const fetcher: Fetcher<IRecommendation[], string> = async (userId) => {
|
||||
const res = {
|
||||
data: {
|
||||
userId,
|
||||
recommendations: [
|
||||
{ movieId: 174, match: 78 },
|
||||
{ movieId: 70, match: 74 },
|
||||
{ movieId: 421, match: 70 },
|
||||
{ movieId: 5846, match: 54 },
|
||||
{ movieId: 926, match: 48 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await new Promise((res) => setTimeout(res, 2500));
|
||||
|
||||
return res.data.recommendations as IRecommendation[];
|
||||
};
|
||||
|
||||
function Recommendations() {
|
||||
const [user] = useAuthState(auth);
|
||||
|
||||
if (!user) throw new Error('User not found');
|
||||
|
||||
const { data, isLoading, isValidating, mutate } = useSWR(user.uid, fetcher);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center px-6 flex-1">
|
||||
<h2 className="text-2xl font-medium">Recommendations</h2>
|
||||
{isLoading && (
|
||||
<div className="my-auto flex flex-col items-center gap-2">
|
||||
<ThreeDots />
|
||||
<p className="italic">Loading recommendations...</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && <Refresh mutate={mutate} isValidating={isValidating} />}
|
||||
{!isLoading && !data?.length && (
|
||||
<p>Could not find any recommendations for you 😬</p>
|
||||
)}
|
||||
{!isLoading && !!data?.length && (
|
||||
<section className="flex flex-wrap gap-x-6 gap-y-9 justify-center">
|
||||
{data.map((recommendation) => (
|
||||
<Recommendation
|
||||
recommendation={recommendation}
|
||||
key={recommendation.movieId}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Recommendations;
|
||||
@ -0,0 +1,6 @@
|
||||
interface Recommendation {
|
||||
movieId: number;
|
||||
match: number;
|
||||
}
|
||||
|
||||
export default Recommendation;
|
||||
18
Programming/ERSMS-project/frontend/src/types/TMDBMovie.ts
Normal file
@ -0,0 +1,18 @@
|
||||
interface TMDBMovie {
|
||||
adult: boolean;
|
||||
backdrop_path: string | null;
|
||||
genre_ids: number[];
|
||||
id: number;
|
||||
original_language: string;
|
||||
original_title: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path: string | null;
|
||||
release_date: string;
|
||||
title: string;
|
||||
video: boolean;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export default TMDBMovie;
|
||||
1
Programming/ERSMS-project/frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
9
Programming/ERSMS-project/frontend/tailwind.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
25
Programming/ERSMS-project/frontend/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
Programming/ERSMS-project/frontend/tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
8
Programming/ERSMS-project/frontend/vite.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import mkcert from 'vite-plugin-mkcert'
|
||||
|
||||
export default defineConfig({
|
||||
server: { https: true }, // Not needed for Vite 5+
|
||||
plugins: [ react(), mkcert() ]
|
||||
})
|
||||
29
Programming/ERSMS-project/movie_recommendations/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set the working directory to /app2
|
||||
WORKDIR /app
|
||||
|
||||
# Install necessary OS packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the Python script, requirements, and datasets into the container at /app2
|
||||
COPY movie_recommender.py .
|
||||
COPY datasets/tmdb_5000_credits.csv ./movie_recommendations/datasets/
|
||||
COPY datasets/tmdb_5000_movies.csv ./movie_recommendations/datasets/
|
||||
COPY init_scripts/constants.ini ./init_scripts/constants.ini
|
||||
COPY init_scripts/movies.csv /app/init_scripts/
|
||||
|
||||
|
||||
# Modify requirements.txt to use psycopg2-binary
|
||||
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
|
||||
|
||||
# Install any needed packages specified in requirements.txt
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install requests
|
||||
|
||||
# Run frontend_AI_connector.py when the container launches
|
||||
CMD ["python", "./movie_recommender.py"]
|
||||
@ -0,0 +1,9 @@
|
||||
[postgres]
|
||||
host=db
|
||||
database=test_db
|
||||
user=root
|
||||
password=root
|
||||
port=5432
|
||||
|
||||
[movie]
|
||||
csv_path=../../movie_recommendations/datasets/tmdb_5000_credits.csv
|
||||
@ -0,0 +1,170 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from ast import literal_eval
|
||||
from sklearn.feature_extraction.text import CountVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
import hashlib
|
||||
import json
|
||||
from configparser import ConfigParser
|
||||
import psycopg2
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_caching import Cache
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'})
|
||||
db_connector = None
|
||||
conn = None
|
||||
|
||||
|
||||
def get_director(x):
|
||||
for i in x:
|
||||
if i['job'] == 'Director':
|
||||
return i['name']
|
||||
return np.nan
|
||||
|
||||
|
||||
def get_list(x):
|
||||
if isinstance(x, list):
|
||||
names = [i['name'] for i in x]
|
||||
if len(names) > 3:
|
||||
names = names[:3]
|
||||
return names
|
||||
return []
|
||||
|
||||
|
||||
def clean_data(x):
|
||||
if isinstance(x, list):
|
||||
return [str.lower(i.replace(" ", "")) for i in x]
|
||||
else:
|
||||
if isinstance(x, str):
|
||||
return str.lower(x.replace(" ", ""))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def create_soup(x):
|
||||
return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
|
||||
|
||||
|
||||
class MovieRecommender:
|
||||
def __init__(self):
|
||||
self.df = None
|
||||
self.cosine_sim = None
|
||||
|
||||
def fit(self, credits_file, movies_file):
|
||||
"""
|
||||
Fittuje AI do przekazanych danych
|
||||
:param credits_file: csv z creditsami
|
||||
:param movies_file: csv z filmami
|
||||
:return: Nic
|
||||
"""
|
||||
df1 = pd.read_csv(credits_file)
|
||||
df2 = pd.read_csv(movies_file)
|
||||
df1.columns = ['id', 'tittle', 'cast', 'crew']
|
||||
df2 = df2.merge(df1, on='id')
|
||||
df2['overview'] = df2['overview'].fillna('')
|
||||
self.df = df2
|
||||
|
||||
features = ['cast', 'crew', 'keywords', 'genres']
|
||||
for feature in features:
|
||||
df2[feature] = df2[feature].apply(literal_eval)
|
||||
|
||||
df2['director'] = df2['crew'].apply(get_director)
|
||||
|
||||
features = ['cast', 'keywords', 'genres']
|
||||
for feature in features:
|
||||
df2[feature] = df2[feature].apply(get_list)
|
||||
|
||||
features = ['cast', 'keywords', 'director', 'genres']
|
||||
for feature in features:
|
||||
df2[feature] = df2[feature].apply(clean_data)
|
||||
|
||||
df2['soup'] = df2.apply(create_soup, axis=1)
|
||||
|
||||
count = CountVectorizer(stop_words='english')
|
||||
count_matrix = count.fit_transform(df2['soup'])
|
||||
self.cosine_sim = cosine_similarity(count_matrix, count_matrix)
|
||||
|
||||
self.df = df2.reset_index()
|
||||
|
||||
def _get_recommendations_one_input(self, movie_id):
|
||||
"""
|
||||
Tworzy rekomendacje, bazując na jednym filmie
|
||||
:param movie_id: id filmu, dla którego ma zrobić rekomendację
|
||||
:return: Zwraca listę [movie_ids, similarity_scores] gdzie oba argumenty są np.array
|
||||
"""
|
||||
indices = pd.Series(self.df.index, index=self.df['id']).drop_duplicates()
|
||||
idx = indices[movie_id]
|
||||
sim_scores = list(enumerate(self.cosine_sim[idx]))
|
||||
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
|
||||
sim_scores = sim_scores[1:101]
|
||||
movie_indices = [i[0] for i in sim_scores]
|
||||
sim_scores = np.array([t[1] for t in sim_scores])
|
||||
return [self.df['id'].iloc[movie_indices].values, sim_scores]
|
||||
|
||||
def get_recommendations(self, movie_ids: list) -> {}:
|
||||
"""
|
||||
Tworzy listę rekomendacji bazującą na id podanych filmów
|
||||
:param movie_ids: id filmów, na podstawie których ma wybrać rekomendowane filmy
|
||||
:return: Zwraca dicta {movie_id: similarity_scores}
|
||||
"""
|
||||
recommended_movies = {}
|
||||
for movie_id in movie_ids:
|
||||
recommended_ids, sim_scores = self._get_recommendations_one_input(movie_id)
|
||||
for recommended_id, sim_score in zip(recommended_ids, sim_scores):
|
||||
if recommended_id in movie_ids:
|
||||
continue
|
||||
|
||||
if recommended_movies.get(int(recommended_id)) is None:
|
||||
recommended_movies[int(recommended_id)] = float(round((sim_score / len(movie_ids)), 4))
|
||||
else:
|
||||
recommended_movies[int(recommended_id)] += float(round((sim_score / len(movie_ids)), 4))
|
||||
return recommended_movies
|
||||
|
||||
|
||||
recommender = MovieRecommender()
|
||||
recommender.fit('datasets/tmdb_5000_credits.csv',
|
||||
'datasets/tmdb_5000_movies.csv')
|
||||
|
||||
|
||||
def make_cache_key():
|
||||
data = request.get_json()
|
||||
if isinstance(data, list):
|
||||
data = sorted(data)
|
||||
key = hashlib.md5(json.dumps(data).encode('utf-8')).hexdigest()
|
||||
return key
|
||||
|
||||
|
||||
@app.route("/api/v3/AI_recommendations", methods=["POST"])
|
||||
@cache.cached(timeout=300, key_prefix=make_cache_key)
|
||||
def AI_recommendations():
|
||||
ids = request.get_json()
|
||||
recommendations = recommender.get_recommendations(ids)
|
||||
return jsonify(recommendations)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = ConfigParser()
|
||||
config.read("init_scripts/constants.ini")
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=config["postgres"]["host"],
|
||||
database=config["postgres"]["database"],
|
||||
user=config["postgres"]["user"],
|
||||
password=config["postgres"]["password"],
|
||||
port=int(config["postgres"]["port"])
|
||||
)
|
||||
|
||||
except Exception:
|
||||
print("Trying to connect with database")
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
cache.init_app(app)
|
||||
app.run(host="localhost", port=8081, debug=True)
|
||||
|
||||
conn.close()
|
||||
@ -0,0 +1,4 @@
|
||||
flask==3.0.3
|
||||
pandas==2.2.2
|
||||
Flask-Caching==2.3.0
|
||||
scikit-learn==1.5.0
|
||||
24
Programming/ERSMS-project/nginx/nginx.conf
Normal file
@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/nginx.crt;
|
||||
ssl_certificate_key /etc/ssl/certs/nginx.key;
|
||||
|
||||
location / {
|
||||
try_files $uri @app_proxy;
|
||||
}
|
||||
|
||||
location @app_proxy {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://app:5000; # Use the service name 'app' and the internal port '80'
|
||||
}
|
||||
}
|
||||
19
Programming/ERSMS-project/nginx/ssl/nginx.crt
Normal file
@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCTCCAfGgAwIBAgIUaPIEdr82RMes6XSaYOB55pQbYDswDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDYxNjIxNTYxNloXDTI1MDYx
|
||||
NjIxNTYxNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAlgWNGOpLB7FonzAvxoMmcDKu7SJDAnBd8HnrswRb4noI
|
||||
HCy2b3JOYi5RU66AGfDB9N4YFkhSNYkjSGLQdmoPqFoR5IyFAtvdGQUsAhRl4YP9
|
||||
/ust4NlPAVfxYPGhHyrpPozJK/RYIFcwPDKgMEEI636jHWw6mJaOsVgiMpQgs9oM
|
||||
V4b5b9LfB6wD72pi6Ag6xMi5PUvj91UfvkR59Gy844zriyV094saxwBRCL6naSMA
|
||||
vToVFIcaGCIXHfO2DiTVq4dDkCkHYRiva64UcV08x0I2GyxNLCUsjgDrQy/pRfdn
|
||||
QE1QO55LoDk4SI3U0PSGCyBMg0Kr7eC+6MwhjI/2ewIDAQABo1MwUTAdBgNVHQ4E
|
||||
FgQUZQxFwZhHP7+N2huK4T8S4+dCMoIwHwYDVR0jBBgwFoAUZQxFwZhHP7+N2huK
|
||||
4T8S4+dCMoIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALTJ4
|
||||
1bql/QkcM7t3DSCm7H7NK0fW9+3G/FYtCQUxvs3+rFvxymHbwHERbR1AReJy2Brx
|
||||
ayNMQNUA48sUa2VsRxEv1f/Qb5WVue3rIonugZ792bsI+6yMVxYVt97ETAnalSH4
|
||||
L3kWqGWJnkyfMm3RB7sGEFVnO2/s+vEhbzCRpCt2qda1pXPIBagPSCFm3InU9+Tq
|
||||
2bDsASuUEEz7RWTPa/R1qkCU5Sl3+CZUpTIpRDxB2MCaGk+FXQ9u2CpZBiZDOL/p
|
||||
e0Af5yhYlNBHp+Ut+N1kQSKKyOqlrqkN92nx7NLqOYBkjfkns9lTciyKiKefq08G
|
||||
qesECi3RgXXuG6xv5Q==
|
||||
-----END CERTIFICATE-----
|
||||
28
Programming/ERSMS-project/nginx/ssl/nginx.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWBY0Y6ksHsWif
|
||||
MC/GgyZwMq7tIkMCcF3weeuzBFvieggcLLZvck5iLlFTroAZ8MH03hgWSFI1iSNI
|
||||
YtB2ag+oWhHkjIUC290ZBSwCFGXhg/3+6y3g2U8BV/Fg8aEfKuk+jMkr9FggVzA8
|
||||
MqAwQQjrfqMdbDqYlo6xWCIylCCz2gxXhvlv0t8HrAPvamLoCDrEyLk9S+P3VR++
|
||||
RHn0bLzjjOuLJXT3ixrHAFEIvqdpIwC9OhUUhxoYIhcd87YOJNWrh0OQKQdhGK9r
|
||||
rhRxXTzHQjYbLE0sJSyOAOtDL+lF92dATVA7nkugOThIjdTQ9IYLIEyDQqvt4L7o
|
||||
zCGMj/Z7AgMBAAECggEAEYz2s8p9GppLpgvqGuwu7ANR6ZYPhtKEeuSYiWniIf2q
|
||||
wzkL4r/ZoazPgN2ySNacqjvtT3YIgBDaGtbMkn3X9RcMbtNtoCb+l7W+L0QZoydg
|
||||
6Ji01lA16O9T4saB1facMOAhjM3aHXZ1wyUdDmgdVgeLp56IwF8ktGSeI5KmvMOA
|
||||
tHH20ZaG0Iy9kHjqgnRTKsF1giUl4XtNR0tbzVBdzvXZz8BPzAtNkC0QwcA3r6Mr
|
||||
PRzpelCQ9cFdbbpssy73aZ4cu30nVYdIuHFCmB25QdOt6Uzo4yoxmUK3PSKjf0py
|
||||
Ow5fHN5UqB1vZ0C/zNAapeg9elZvsYMU+LSUkGknaQKBgQDNfBb/FasjhPLstiHR
|
||||
U8LXTLinsU5qtKDVQIEpxQOub3lwRFs8bK0T2PfdbNJqUv55B301ORpbyqXLWS9N
|
||||
BNEUWkQ0Tpw9CsnItENvQnObUh6iIPU0uKLSp8Vnj4v3zOLfAMBZMitqbituqooE
|
||||
rrXmv9Oy3Sa/G1gX6HC7Hrdv/wKBgQC65vfMjmYXXRL47puTkfkBVWIZPV5WNq2F
|
||||
VnEarBUcO4ZqwweKKzO38mLXFWVcxz9QGzznoR/ndi1KGJSmBPbYPJwIKEcUCgZv
|
||||
25QUsVstV7mB2O53YEYfr4eGx0+lacofSYOcNV//Mrf1VcEQn8stme+hRp3xsvRa
|
||||
FAni8K05hQKBgQCWc8IeuU3aLvDzMpPmQa8Klwko92CULncITTeFTtRYNxSyh8pJ
|
||||
nsTHIHizrocOBICAO2SIwKu1A9aK4l0IxnsTrNf9eIVbCHggNSSe5QfidKkrSnhf
|
||||
RsUo+mBGaEsyf9ipMVKkvGAfiFTSrZlqvkU/k7q8XsKM8Md6kd1glKf5HwKBgGp0
|
||||
Q4/vS5bjHHtM7LAQ5JMt3sIhgin42rynj6Rxf1SlUtpcW18HXF3ZYRLAzQsbMaSe
|
||||
3wHPdCyR0xnxBjnJeg+P9g9vYYy4aRItLxraKeSZor+in7C+1TIW+Ep8G5FLwCQx
|
||||
6xR+Ej940+6Y+W5OlZtTonlpj2yrUSI9Z6QrEX9BAoGADH9nrJ+Gl6GaTEtlTHUQ
|
||||
XbQLmlfffQPIvc+v6z6TAvuULqQrK/Tya8s5dqDmM+RfoU12vij4yCblChDo8Pef
|
||||
IgInN2S7hmuMHVB5QdVHoiHzVnryIsr29fR9jDp3QP7AnnFJbZ5sOQxId+0KpYtG
|
||||
ZTcpATmbdu3hUJSNSpK1+ig=
|
||||
-----END PRIVATE KEY-----
|
||||
32
Programming/ERSMS-project/test_scripts/Test.http
Normal file
@ -0,0 +1,32 @@
|
||||
###
|
||||
POST http://127.0.0.1:8081/api/v3/AI_recommendations
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
49026,
|
||||
155,
|
||||
312113
|
||||
]
|
||||
###
|
||||
POST http://127.0.0.1:8090/api/v3/add/1234/blep
|
||||
###
|
||||
GET http://127.0.0.1:8090/api/v3/get/blep
|
||||
###
|
||||
POST http://127.0.0.1:8090/api/v3/add/6666/blop
|
||||
###
|
||||
POST http://127.0.0.1:8090/api/v3/rate_movie/1234/155/4
|
||||
###
|
||||
POST http://127.0.0.1:8090/api/v3/rate_movie/6666/155/5
|
||||
###
|
||||
POST http://127.0.0.1:8090/api/v3/rate_movie/1234/19995/5
|
||||
###
|
||||
GET http://localhost:8082/api/get_movie_ratings/155
|
||||
###
|
||||
GET http://localhost:8082/api/get_number_of_ratings
|
||||
###
|
||||
GET http://localhost:8082/api/get_users_number
|
||||
###
|
||||
GET http://localhost:8082/api/get_movie_rating_avg/155
|
||||
###
|
||||
GET http://localhost:8082/api/get_user_ratings/1234
|
||||
###
|
||||