diff --git a/.github/templates/bug_report.md b/.github/templates/bug_report.md new file mode 100644 index 0000000..59b4075 --- /dev/null +++ b/.github/templates/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Coloque os detalhes do bug +title: "[BUG] nome da tarefa no verbo infinitivo" +labels: bug +assignees: '' + +--- + +**Descrição do bug** + + +**Para reproduzir o bug** + + +**Comportamento esperado** + + +**Tarefas** + + +**Screenshots** + + +**Adicional** + diff --git a/.github/templates/feature_request.md b/.github/templates/feature_request.md new file mode 100644 index 0000000..6231f15 --- /dev/null +++ b/.github/templates/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Coloque os detalhes da issue +title: '[Doc] ou [Feature] e o nome da tarefa no verbo infinitivo' +labels: '' +assignees: '' + +--- + +**Descreva sua issue** + + +**Tarefas** + + +**Critérios de Aceitação** + + + +**Contexto adicional** + diff --git a/.github/templates/pull_request_template.md b/.github/templates/pull_request_template.md new file mode 100644 index 0000000..c227171 --- /dev/null +++ b/.github/templates/pull_request_template.md @@ -0,0 +1,26 @@ + + +## Descrição + + +## _Issue_ Relacionada + + + + + + + +## Como Isso Foi Testado? + + + + +## Capturas de Tela (se apropriado): + +## Tipos de Mudanças + +- [ ] _Bug fix_ (alteração que corrige uma _issue_ e não altera funcionalidades já existentes); +- [ ] Nova _feature_ (alteração que adiciona uma funcionalidade e não altera funcionalidades já existentes); +- [ ] Alteração disruptiva (_Breaking change_) (Correção ou funcionalidade que causa alteração nas funcionalidades existentes); +- [ ] Documentação. \ No newline at end of file diff --git a/.github/templates/user_stories.md b/.github/templates/user_stories.md new file mode 100644 index 0000000..2a0ae6b --- /dev/null +++ b/.github/templates/user_stories.md @@ -0,0 +1,24 @@ +--- +name: User Stories +about: Coloque os detalhes da US +title: 'USX' +labels: ux +assignees: '' + +--- + +**Descrição da Issue** + + +**Tarefas** + + +**Protótipo** + + +**Critérios de Aceitação** + diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml new file mode 100644 index 0000000..fb14b7b --- /dev/null +++ b/.github/workflows/code-analysis.yml @@ -0,0 +1,57 @@ +name: Análise de Código +on: push + +jobs: + sonarcloud: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Creating env file + run: | + echo "${{ vars.ENV_FILE }}" > .env + + - name: Setup virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Executa Pytest + run: PYTHONPATH=src python -m coverage run -m pytest --continue-on-collection-errors --junitxml=./junit.xml + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST: localhost + POSTGRES_DB: postgres + POSTGRES_PORT: 5432 + + - name: Gera arquivos de testes no formato .xml + run: python3 -m coverage xml + + - name: Executa SonarCloud Scan + if: ${{ always() }} + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.API_TOKEN_GITHUB }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cd1250b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + pull_request: + branches: + - main + - develop + types: [ closed ] + +jobs: + release: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'NOT RELEASE') == false + runs-on: "ubuntu-latest" + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Cria arquivo .env + run: | + touch ./scripts/.env + echo TOKEN=${{ secrets.API_TOKEN_GITHUB }} >> ./scripts/.env + echo RELEASE_MAJOR=${{ contains(github.event.pull_request.labels.*.name, 'MAJOR RELEASE') }} >> ./scripts/.env + echo RELEASE_MINOR=${{ contains(github.event.pull_request.labels.*.name, 'MINOR RELEASE') }} >> ./scripts/.env + echo RELEASE_FIX=${{ contains(github.event.pull_request.labels.*.name, 'FIX RELEASE') }} >> ./scripts/.env + echo DEVELOP=${{ contains(github.event.pull_request.labels.*.name, 'DEVELOP') }} >> ./scripts/.env + + - name: Gera release e envia métricas para repositório de DOC + run: | + cd scripts && yarn install && node release.js + git config --global user.email "${{secrets.GIT_USER_EMAIL}}" + git config --global user.name "${{secrets.GIT_USER_NAME}}" + git clone --single-branch --branch main "https://x-access-token:${{secrets.API_TOKEN_GITHUB}}@github.com/fga-eps-mds/${{secrets.GIT_DOC_REPO}}" ${{secrets.GIT_DOC_REPO}} + mkdir -p ${{secrets.GIT_DOC_REPO}}/analytics-raw-data + cp -R analytics-raw-data/*.json ${{secrets.GIT_DOC_REPO}}/analytics-raw-data + cd ${{secrets.GIT_DOC_REPO}} + git add . + git commit -m "Adicionando métricas do repositório ${{ github.event.repository.name }} ${{ github.ref_name }}" + git push \ No newline at end of file diff --git a/.gitignore b/.gitignore index 68bc17f..1828ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +.vscode/ +.vs/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c2c1ef8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.10.9-slim-buster + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONBUFFERED 1 + +COPY requirements.txt /app/requirements.txt + +RUN pip3 install --no-cache-dir -r /app/requirements.txt + +COPY . /app/ + +WORKDIR src + +CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"] \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..0ef4803 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python src/main.py ${PORT} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c255572 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +services: + app: + build: . + ports: + - 8001:8001 + volumes: + - .:/app/ + env_file: + - ./.env + environment: + - POSTGRES_HOST=db + depends_on: + db: + condition: service_healthy + restart: always + networks: + - backend_videos + + db: + image: postgres + volumes: + - postgres_data:/var/lib/postgresql/data/ + expose: + - 5432 + environment: + - POSTGRES_HOST=${POSTGRES_HOST} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_DB=${POSTGRES_DB} + env_file: + - ./.env + restart: always + networks: + - backend_videos + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}", + ] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + +networks: + backend_videos: diff --git a/env.example b/env.example new file mode 100644 index 0000000..234714e --- /dev/null +++ b/env.example @@ -0,0 +1,5 @@ +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_HOST= +POSTGRES_DB= +POSTGRES_PORT= \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55ff0ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,78 @@ +aiosmtplib==2.0.2 +annotated-types==0.6.0 +anyio==3.7.1 +beautifulsoup4==4.12.2 +bs4==0.0.1 +astroid==3.0.1 +bcrypt==4.0.1 +blinker==1.6.3 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.2 +click==8.1.7 +coverage==7.3.2 +cryptography==41.0.4 +DateTime==5.2 +deprecation==2.1.0 +dill==0.3.7 +dnspython==2.4.2 +docker==6.1.3 +ecdsa==0.18.0 +email-validator==2.0.0.post2 +fastapi==0.104.0 +fastapi-filter==1.0.0 +fastapi-mail==1.4.1 +fastapi-sso==0.7.2 +greenlet==3.0.0 +h11==0.14.0 +httpcore==0.18.0 +httptools==0.6.1 +httpx==0.25.0 +idna==3.4 +iniconfig==2.0.0 +isort==5.12.0 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +mccabe==0.7.0 +oauthlib==3.2.2 +packaging==23.2 +passlib==1.7.4 +platformdirs==3.11.0 +pluggy==1.3.0 +psycopg2-binary==2.9.9 +pyasn1==0.5.0 +pycparser==2.21 +pydantic==2.4.2 +pydantic-settings==2.0.3 +pydantic_core==2.10.1 +pylint==3.0.2 +PyMySQL==1.1.0 +pytest==7.4.2 +pytest-asyncio==0.21.1 +pytest-html==4.1.1 +pytest-metadata==3.0.0 +pytest-mock==3.12.0 +python-dotenv==1.0.0 +python-jose==3.3.0 +python-multipart==0.0.6 +pytz==2023.3.post1 +PyYAML==6.0.1 +requests==2.31.0 +rsa==4.9 +six==1.16.0 +sniffio==1.3.0 +soupsieve==2.5 +SQLAlchemy==2.0.22 +starlette==0.27.0 +testcontainers==3.7.1 +tomlkit==0.12.2 +typing_extensions==4.8.0 +ujson==5.8.0 +Unidecode==1.3.7 +urllib3==2.1.0 +uvicorn==0.23.2 +watchfiles==0.21.0 +websocket-client==1.6.4 +websockets==11.0.3 +wrapt==1.15.0 +zope.interface==6.1 \ No newline at end of file diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..e3d06d7 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.12 \ No newline at end of file diff --git a/scripts/consts.js b/scripts/consts.js new file mode 100644 index 0000000..c57cf32 --- /dev/null +++ b/scripts/consts.js @@ -0,0 +1,26 @@ +const REPO = '2023.2-UnB-TV-VideoService'; // Nome do repositório +const OWNER = 'fga-eps-mds'; +const SONAR_ID = 'fga-eps-mds_2023.2-UnB-TV-VideoService'; // Id do projeto no SonarCloud +const METRIC_LIST = [ + 'files', + 'functions', + 'complexity', + 'comment_lines_density', + 'duplicated_lines_density', + 'coverage', + 'ncloc', + 'tests', + 'test_errors', + 'test_failures', + 'test_execution_time', + 'security_rating', +]; +const SONAR_URL = `https://sonarcloud.io/api/measures/component_tree?component=${SONAR_ID}&metricKeys=${METRIC_LIST.join( + ',' +)}`; + +module.exports = { + SONAR_URL, + REPO, + OWNER, +}; diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..9443de6 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,13 @@ +{ + "name": "scripts", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@octokit/core": "^3.4.0", + "axios": "^0.21.1", + "dotenv": "^8.2.0", + "fs": "^0.0.1-security", + "gh-release-assets": "^2.0.0" + } +} \ No newline at end of file diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 0000000..c373926 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,103 @@ +const { Octokit } = require('@octokit/core'); +const ghReleaseAssets = require('gh-release-assets'); +const axios = require('axios'); +const fs = require('fs'); +require('dotenv').config(); + +const { SONAR_URL, REPO, OWNER } = require('./consts.js'); + +const { TOKEN, RELEASE_MAJOR, RELEASE_MINOR, RELEASE_FIX, DEVELOP } = process.env; + +const octokit = new Octokit({ auth: TOKEN }); + +const now = new Date(); +const year = now.getFullYear().toString(); +const month = (now.getMonth() + 1).toString().padStart(2, '0'); +const day = now.getDate().toString().padStart(2, '0'); +const hours = now.getHours().toString().padStart(2, '0'); +const minutes = now.getMinutes().toString().padStart(2, '0'); +const seconds = now.getSeconds().toString().padStart(2, '0'); + +const getLatestRelease = async () => { + const releases = await octokit.request('GET /repos/{owner}/{repo}/releases', { + owner: OWNER, + repo: REPO, + }); + if (releases?.data.length > 0) { + return releases?.data?.[0]?.tag_name; + } + return '0.0.0'; +}; + +const newTagName = async () => { + let oldTag = await getLatestRelease(); + oldTag = oldTag.split('.'); + + if (RELEASE_MAJOR === 'true') { + const majorTagNum = parseInt(oldTag[0]) + 1; + return `${majorTagNum}.0.0`; + } + if (RELEASE_MINOR === 'true') { + const minorTagNum = parseInt(oldTag[1]) + 1; + return `${oldTag[0]}.${minorTagNum}.0`; + } + if (RELEASE_FIX === 'true') { + const fixTagNum = parseInt(oldTag[2]) + 1; + return `${oldTag[0]}.${oldTag[1]}.${fixTagNum}`; + } + if (DEVELOP === 'true') { + return `develop`; + } + // Caso não tenha nenhuma flag de release, é feito um release de fix + const fixTagNum = parseInt(oldTag[2]) + 1; + return `${oldTag[0]}.${oldTag[1]}.${fixTagNum}`; +}; + +const createRelease = async () => { + const tag = await newTagName(); + const res = await octokit.request('POST /repos/{owner}/{repo}/releases', { + owner: OWNER, + repo: REPO, + tag_name: tag, + name: tag, + }); + return [res?.data?.upload_url, tag]; +}; + +const saveSonarFile = async (tag) => { + const dirPath = './analytics-raw-data/'; + let filePath = `${dirPath}fga-eps-mds-${REPO}-${month}-${day}-${year}-${hours}-${minutes}-${seconds}-v${tag}.json`; + fs.mkdirSync(dirPath); + if(tag === 'develop') { + filePath = `${dirPath}fga-eps-mds-${REPO}-${month}-${day}-${year}-${hours}-${minutes}-${seconds}-${tag}.json`; + } + await axios.get(SONAR_URL).then((res) => { + fs.writeFileSync(filePath, JSON.stringify(res?.data)); + }); +}; + +const uploadSonarFile = async (release) => { + await saveSonarFile(release[1]); + ghReleaseAssets({ + url: release[0], + token: [TOKEN], + assets: [ + `./analytics-raw-data/fga-eps-mds-${REPO}-${month}-${day}-${year}-${hours}-${minutes}-${seconds}-v${release[1]}.json`, + { + name: `fga-eps-mds-${REPO}-${month}-${day}-${year}-${hours}-${minutes}-${seconds}-v${release[1]}.json`, + path: '', + }, + ], + }); +}; + +const script = async () => { + if(DEVELOP === 'true') { + await saveSonarFile('develop'); + return; + } + const release = await createRelease(); + await uploadSonarFile(release); +}; + +script(); diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..c49b644 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,14 @@ +sonar.projectKey=fga-eps-mds_2023.2-UnB-TV-Admin +sonar.projectKey=fga-eps-mds_2023.2-UnB-TV-VideoService +sonar.organization=fga-eps-mds-1 + +sonar.sources=src +sonar.tests=tests + +sonar.exclusions=__pycache__, tests + +sonar.sourceEncoding=UTF-8 + +sonar.python.version=3.11.5 +sonar.python.xunit.reportPath=junit.xml +sonar.python.coverage.reportPaths=coverage.xml \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/constants/__init__.py b/src/constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/constants/errorMessages.py b/src/constants/errorMessages.py new file mode 100644 index 0000000..82c34a6 --- /dev/null +++ b/src/constants/errorMessages.py @@ -0,0 +1,8 @@ +INVALID_SCHEDULE_DAY = "INVALID SCHEDULE DAY" +ERROR_RETRIEVING_SCHEDULE = "ERROR RETRIEVING SCHEDULE" +USER_ID_REQUIRED = "USER ID FIELD REQUIRED" +VIDEO_ID_REQUIRED = "VIDEO ID FIELD REQUIRED" +CONTENT_REQUIRED = "CONTENT FIELD REQUIRED" +COMMENT_NOT_FOUND = "COMMENT NOT FOUND" +MISSING_ENV_VALUES = "SOME ENVIRONMENT VALUES WERE NOT DEFINED" +INVALID_REQUEST = "INVALID_REQUEST" \ No newline at end of file diff --git a/src/controller/__init__.py b/src/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controller/commentController.py b/src/controller/commentController.py new file mode 100644 index 0000000..44f850e --- /dev/null +++ b/src/controller/commentController.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, HTTPException, Response, status, Depends +from database import get_db +from sqlalchemy.orm import Session + +from domain import commentSchema +from repository import commentRepository +from constants import errorMessages + +comment = APIRouter( + prefix="/comments" +) + +@comment.get("/{video_id}", response_model=list[commentSchema.Comment]) +def read_comment(video_id: int, db: Session = Depends(get_db)): + comment = commentRepository.get_comments_by_video_id(db, video_id=video_id) + return comment + +@comment.post("/", response_model=commentSchema.Comment) +def create_comment(comment: commentSchema.CommentCreate, db: Session = Depends(get_db)): + return commentRepository.create_comment(db=db, video_id=comment.video_id, user_id=comment.user_id, user_name= comment.user_name ,content=comment.content) + +@comment.delete("/{id}", response_model=commentSchema.Comment) +def delete_comment(id: int, db: Session = Depends(get_db)): + comment = commentRepository.get_comment_by_id(db, id) + if not comment: + raise HTTPException(status_code=404, detail=errorMessages.COMMENT_NOT_FOUND) + commentRepository.delete_comment(db,comment) + return comment + +@comment.patch("/{id}", response_model=commentSchema.Comment) +def update_comment(id: int, data: commentSchema.CommentUpdate,db: Session = Depends(get_db)): + comment = commentRepository.get_comment_by_id(db, id) + if not comment: + raise HTTPException(status_code=404, detail=errorMessages.COMMENT_NOT_FOUND) + updated_comment = commentRepository.update_comment(db,comment,data) + return updated_comment diff --git a/src/controller/scheduleController.py b/src/controller/scheduleController.py new file mode 100644 index 0000000..9a955a0 --- /dev/null +++ b/src/controller/scheduleController.py @@ -0,0 +1,55 @@ +import requests +from typing import Optional +from fastapi import APIRouter +from bs4 import BeautifulSoup +from unidecode import unidecode +from starlette.responses import JSONResponse + +from utils import enumeration +from constants import errorMessages + +schedule = APIRouter( + prefix="/schedule" +) + +@schedule.get("/") +async def get_schedule_day(day: Optional[str] = None): + if day: + day = unidecode(day).upper() + if not enumeration.ScheduleDaysEnum.has_value(day): + return JSONResponse(status_code=400, content={ "detail": errorMessages.INVALID_SCHEDULE_DAY }) + + try: + re = requests.get('https://unbtv.unb.br/grade') + html = re.text + + soup = BeautifulSoup(html, 'html.parser') + tables = soup.find_all("table") + + schedule_data = {} + current_day = None + + for table in tables: + for row in table.find_all("tr"): + if len(row.find_all("td")) == 1: + cell = row.find_all("td")[0] + schedule_day = unidecode(cell.text.replace("-FEIRA", "")) + + if day: + if current_day: + return schedule_data + if day == schedule_day: + current_day = schedule_day + schedule_data[schedule_day] = [] + else: + current_day = schedule_day + schedule_data[schedule_day] = [] + else: + if current_day: + day_schedule = [item.text for item in row.find_all("td")[:2]] + if (day_schedule[0].strip() != "" and day_schedule[1].strip() != ""): + schedule_data[current_day].append({ "time": day_schedule[0], "activity": day_schedule[1] }) + + return schedule_data + except: + return JSONResponse(status_code=400, content={ "error": errorMessages.ERROR_RETRIEVING_SCHEDULE }) diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..923804f --- /dev/null +++ b/src/database.py @@ -0,0 +1,24 @@ +import os, sys +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +POSTGRES_USER = os.getenv("POSTGRES_USER") +POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD") +POSTGRES_HOST = os.getenv("POSTGRES_HOST") +POSTGRES_DB = os.getenv("POSTGRES_DB") +POSTGRES_PORT = os.getenv("POSTGRES_PORT", default=5432) + +POSTGRES_URI = f'postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}' + +engine = create_engine(POSTGRES_URI) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/src/domain/__init__.py b/src/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/commentSchema.py b/src/domain/commentSchema.py new file mode 100644 index 0000000..719cb20 --- /dev/null +++ b/src/domain/commentSchema.py @@ -0,0 +1,22 @@ +from datetime import date +from pydantic import BaseModel, ConfigDict + +class CommentUpdate(BaseModel): + content: str | None = None + +class Comment(BaseModel): + model_config = ConfigDict(from_attributes = True) + id: int + user_id: int + user_name: str + video_id: int + content: str + created_at: date + +class CommentCreate(BaseModel): + model_config = ConfigDict(from_attributes = True) + user_id: int + user_name: str + video_id: int + content: str + \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..98c0647 --- /dev/null +++ b/src/main.py @@ -0,0 +1,38 @@ +import uvicorn, sys +from fastapi import FastAPI +from dotenv import load_dotenv +from fastapi.middleware.cors import CORSMiddleware + +load_dotenv() + +from controller import commentController, scheduleController +from database import SessionLocal, engine +from model import commentModel + +commentModel.Base.metadata.create_all(bind=engine) + +app = FastAPI() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(prefix="/api", router=commentController.comment) +app.include_router(prefix="/api", router=scheduleController.schedule) + +@app.get("/") +async def root(): + return {"message": "Hello from Video Service"} + +if __name__ == '__main__': # pragma: no cover + port = 8001 + if (len(sys.argv) == 2): + port = sys.argv[1] + + uvicorn.run('main:app', reload=True, port=int(port), host="0.0.0.0") \ No newline at end of file diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/model/commentModel.py b/src/model/commentModel.py new file mode 100644 index 0000000..0c5fd42 --- /dev/null +++ b/src/model/commentModel.py @@ -0,0 +1,19 @@ +# Referencia: https://fastapi.tiangolo.com/tutorial/sql-databases/#create-the-database-models + +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Date +from sqlalchemy.orm import relationship +from datetime import datetime + +from database import Base + +class Comment(Base): + __tablename__ = "comments" + __table_args__ = {'extend_existing': True} + + id = Column(Integer, primary_key=True, index=True) + video_id = Column(Integer, nullable=False) + user_id = Column(Integer, nullable=False) + user_name = Column(String, nullable=False) + content = Column(String, nullable=False) + created_at = Column(Date, nullable=False, default=datetime.now()) + \ No newline at end of file diff --git a/src/repository/__init__.py b/src/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/repository/commentRepository.py b/src/repository/commentRepository.py new file mode 100644 index 0000000..c8d23f7 --- /dev/null +++ b/src/repository/commentRepository.py @@ -0,0 +1,32 @@ +# Referencia: https://fastapi.tiangolo.com/tutorial/sql-databases/#crud-utils +from sqlalchemy.orm import Session + +from domain import commentSchema +from model import commentModel + +def get_comments_by_video_id(db: Session, video_id: int): + return db.query(commentModel.Comment).filter(commentModel.Comment.video_id == video_id).all() + +def get_comment_by_id(db: Session, id: int): + return db.query(commentModel.Comment).filter(commentModel.Comment.id == id).first() + +def create_comment(db: Session, video_id, user_id, user_name,content): + db_comment = commentModel.Comment(video_id=video_id, user_id=user_id,user_name=user_name, content=content) + db.add(db_comment) + db.commit() + db.refresh(db_comment) + return db_comment + +def update_comment(db: Session, db_comment: commentSchema.Comment, content: commentSchema.CommentUpdate): + comment_data = content.dict(exclude_unset=True) + for key, value in comment_data.items(): + setattr(db_comment, key, value) + + db.add(db_comment) + db.commit() + db.refresh(db_comment) + return db_comment + +def delete_comment(db: Session, db_comment: commentSchema.Comment): + db.delete(db_comment) + db.commit() \ No newline at end of file diff --git a/src/utils/enumeration.py b/src/utils/enumeration.py new file mode 100644 index 0000000..30ac49a --- /dev/null +++ b/src/utils/enumeration.py @@ -0,0 +1,14 @@ +from enum import Enum + +class ScheduleDaysEnum(Enum): + SEGUNDA = "SEGUNDA" + TERCA = "TERCA" + QUARTA = "QUARTA" + QUINTA = "QUINTA" + SEXTA = "SEXTA" + SABADO = "SABADO" + DOMINGO = "DOMINGO" + + @classmethod + def has_value(cls, value): + return value in cls._value2member_map_ \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_comments.py b/tests/test_comments.py new file mode 100644 index 0000000..9fb4c60 --- /dev/null +++ b/tests/test_comments.py @@ -0,0 +1,80 @@ +import pytest +from fastapi.testclient import TestClient + +from src.main import app +from src.model import commentModel +from src.database import engine +from src.constants import errorMessages + +client = TestClient(app) + +comment = { + 'user_id': 1, + 'user_name': 'Lucas', + 'video_id': 1, + 'content': 'Comentario' +} + +comment_update = { + 'content': 'Comentario Atualizado' +} + +class TestVideoComment: + @pytest.fixture(scope="session", autouse=True) + def setup(self): + response = client.post('/api/comments/', json=comment) + data = response.json() + assert response.status_code == 200 + assert data['user_id'] == comment['user_id'] + assert data['user_name'] == comment['user_name'] + assert data['video_id'] == comment['video_id'] + assert data['content'] == comment['content'] + + yield + + commentModel.Base.metadata.drop_all(bind=engine) + + def test_root(self, setup): + response = client.get('/') + data = response.json() + assert response.status_code == 200 + assert data["message"] == "Hello from Video Service" + + def test_comment_read_comment(self, setup): + response = client.get('/api/comments/1') + data = response.json() + assert response.status_code == 200 + assert len(data) == 1 + assert data[0]['user_id'] == comment['user_id'] + assert data[0]['user_name'] == comment['user_name'] + assert data[0]['video_id'] == comment['video_id'] + assert data[0]['content'] == comment['content'] + + def test_comment_update_comment_not_found(self, setup): + response = client.patch('/api/comments/2', json=comment_update) + data = response.json() + assert response.status_code == 404 + assert data['detail'] == errorMessages.COMMENT_NOT_FOUND + + def test_comment_update_comment(self, setup): + response = client.patch('/api/comments/1', json=comment_update) + data = response.json() + assert response.status_code == 200 + assert data['user_id'] == comment['user_id'] + assert data['user_name'] == comment['user_name'] + assert data['video_id'] == comment['video_id'] + assert data['content'] == comment_update['content'] + + def test_comment_delete_comment_not_found(self, setup): + response = client.delete('/api/comments/2') + data = response.json() + assert response.status_code == 404 + assert data['detail'] == errorMessages.COMMENT_NOT_FOUND + + def test_comment_delete_comment(self, setup): + response = client.delete('/api/comments/1') + data = response.json() + assert response.status_code == 200 + assert data['user_id'] == comment['user_id'] + assert data['user_name'] == comment['user_name'] + assert data['video_id'] == comment['video_id'] \ No newline at end of file diff --git a/tests/test_schedule.py b/tests/test_schedule.py new file mode 100644 index 0000000..e45753b --- /dev/null +++ b/tests/test_schedule.py @@ -0,0 +1,29 @@ +import pytest +from fastapi.testclient import TestClient + +from src.main import app +from src.constants import errorMessages + +client = TestClient(app) + +class TestSchedule: + def test_schedule_get_schedule_day(self): + response = client.get("/api/schedule/") + data = response.json() + assert response.status_code == 200 + assert len(list(data.keys())) == 7 + assert all([a == b for a, b in zip(list(data.keys()), ['SEGUNDA', 'TERCA', 'QUARTA', 'QUINTA', 'SEXTA', 'SABADO', 'DOMINGO'])]) + + def test_schedule_get_schedule_specific_day_invalid(self): + params = { 'day': 'INVALID' } + response = client.get("/api/schedule/", params=params) + data = response.json() + assert response.status_code == 400 + assert data['detail'] == errorMessages.INVALID_SCHEDULE_DAY + + def test_schedule_get_schedule_specific_day(self): + params = { 'day': 'segunda' } + response = client.get("/api/schedule/", params=params) + data = response.json() + assert response.status_code == 200 + assert len(data) > 0 \ No newline at end of file