diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..fc282ee --- /dev/null +++ b/.cspell.json @@ -0,0 +1,52 @@ +{ + "words": [ + "adminasd", + "argcount", + "autouse", + "Biopharma", + "BSON", + "conftest", + "FCXL", + "fontawesome", + "Frisby", + "globulus", + "GWSP", + "histssory", + "IHVX", + "instrucao", + "isabstractmethod", + "JNQT", + "joaozinho", + "LHWJ", + "parsel", + "PHENYLEPHRINE", + "Polacrilex", + "Portugues", + "Pymongo", + "pytest", + "Pytest", + "relatorio", + "REMEDYREPACK", + "Reqs", + "requisito", + "Silicea", + "soeusei", + "Spironolactone", + "trybe", + "tryber", + "usuario", + "varnames", + "VCSI", + "venv", + "VJPY", + "VRXJ", + "Werkzeug", + "WIVL", + "XFAIL", + "XKGX", + "YYUI", + "ZCXM", + "ZOWB" + ], + "language": "en, pt-BR" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a177437 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = false + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,html,md,css,sql,py}] +charset = utf-8 +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dc5e0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,147 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof + +# End of https://www.toptal.com/developers/gitignore/api/python + +# vscode docker container +.devcontainer diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d2009b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3-alpine3.17 + +WORKDIR /projeto-traduzo + +# Dica: instale primeiro as dependências antes de copiar todo projeto +COPY *requirements.txt ./ + +RUN apk update && apk add git +RUN python3 -m pip install --upgrade pip + +RUN pip install --no-cache-dir -r requirements.txt + +# Este argumento será passado dentro do docker-compose +ARG FLASK_ENV +RUN if [ "$FLASK_ENV" = "dev" ] ; then pip install --no-cache-dir -r dev-requirements.txt ; fi + +COPY . . + +CMD ["python3", "src/app.py"] \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..a8a5537 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,12 @@ +-r requirements.txt + +beautifulsoup4==4.12.2 +black==23.3.0 +flake8==4.0.1 +parsel==1.8.1 +pytest==7.3.1 +pytest-cov==4.1.0 +pytest-env==0.8.2 +pytest-json==0.4.0 + +pytest-dependency@git+https://github.com/betrybe/pytest-dependency@984f9d7d083870d091e8862a9b9c33fdf815b8d9 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2138a4b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.4' + +services: + translate: + build: + # Aqui que faz funcionar nosso IF do Dockerfile + args: + - FLASK_ENV=dev + context: . + volumes: + - ./:/projeto-traduzo/. + ports: + - 8000:8000 + depends_on: + - mongodb + networks: + - flask_net + environment: + - MONGO_URI=mongodb://mongodb:27017 + - FLASK_ENV=dev + - DB_NAME=db_traduzo + mongodb: + image: mongo:4.4.14 + container_name: mongodb + restart: always + ports: + - 27017:27017 + networks: + - flask_net + +networks: + flask_net: + driver: bridge \ No newline at end of file diff --git a/example.py b/example.py new file mode 100644 index 0000000..2180b2a --- /dev/null +++ b/example.py @@ -0,0 +1,6 @@ +from deep_translator import GoogleTranslator + +translated = GoogleTranslator(source="auto", target="pt").translate( + "Hi, would you like some ice cream?" +) +print(translated) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01ef0ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "traduzo" +version = "0.1.0" +description = "Relatório de estoques" +readme = "readme.md" +requires-python = ">=3.8" + +[project.scripts] +ir = "traduzo.__main__:main" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-vv --capture=fd" +accept_xfail = true +testpaths = ["tests"] +filterwarnings = ["ignore::pytest.PytestDeprecationWarning"] +pythonpath = ["src"] +env = ["DB_NAME = test_db_traduzo"] + +[tool.black] +line-length = 79 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.mypy] +follow_imports = "silent" +show_column_numbers = true +strict = true +ignore_missing_imports = true +no_incremental = true + +[[tool.mypy.overrides]] +module = ["tests.product.conftest", "tests.product_report.conftest"] +disable_error_code = "misc" + +[project.optional-dependencies] +test = [ + "wheel==0.40.0", + "factory-boy==3.2.1", + "Faker==18.9.0", + "pytest==7.3.1", + "pytest-cov==4.1.0", + "pytest-json==0.4.0", + "pytest-dependency@git+https://github.com/betrybe/pytest-dependency@311e45f9c512e89e6832f79047c6c29bc5a9b0f0" +] +alltest = [ + "traduzo[test]", + "coverage==7.2.7", + "iniconfig==2.0.0", + "packaging==23.1", + "pluggy==1.0.0", + "python-dateutil==2.8.2", + "six==1.16.0", +] +dev = [ + "traduzo[test]", + "black==23.3.0", + "flake8==6.0.0", + "isort==5.12.0", + "mypy==1.3.0", +] +alldev = [ + "traduzo[dev]", + "traduzo[alltest]", + "click==8.1.3", + "mccabe==0.7.0", + "mypy-extensions==1.0.0", + "pathspec==0.11.1", + "platformdirs==3.5.1", + "pycodestyle==2.10.0", + "pyflakes==3.0.1", + "typing_extensions==4.6.2", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d7d8e43 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +deep-translator==1.11.1 +Flask==2.3.1 +pymongo==4.3.3 +waitress==2.1.2 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cc19684 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +exclude = + .git, + .github/, + __pycache__, + .venv +max-complexity = 5 \ 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/app.py b/src/app.py new file mode 100644 index 0000000..c5787b7 --- /dev/null +++ b/src/app.py @@ -0,0 +1,24 @@ +from flask import Flask + +from controllers.admin_controller import admin_controller + +from os import environ +from waitress import serve + + +app = Flask(__name__) +app.template_folder = "views/templates" +app.static_folder = "views/static" + +app.register_blueprint(admin_controller, url_prefix="/admin") + + +def start_server(host="0.0.0.0", port=8000): + if environ.get("FLASK_ENV") != "production": + return app.run(debug=True, host=host, port=port) + else: + serve(app, host=host, port=port) + + +if __name__ == "__main__": + start_server() diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/admin_controller.py b/src/controllers/admin_controller.py new file mode 100644 index 0000000..7f41685 --- /dev/null +++ b/src/controllers/admin_controller.py @@ -0,0 +1,24 @@ +from bson import ObjectId +from flask import Blueprint, jsonify, request +from models.history_model import HistoryModel +from models.user_model import UserModel + +admin_controller = Blueprint("admin_controller", __name__) + + +@admin_controller.route("/history/", methods=["DELETE"]) +def history_delete(id): + admin_token = request.headers["Authorization"] + admin_username = request.headers["User"] + + user = UserModel.find_one({"name": admin_username}) + + if not user or not user.token_is_valid(admin_token): + return jsonify({"error": "Unauthorized Access"}), 401 + + history = HistoryModel.find_one({"_id": ObjectId(id)}) + if not history: + return jsonify({"error": "History not found"}), 404 + else: + history.delete() + return "", 204 diff --git a/src/database/seed_language.py b/src/database/seed_language.py new file mode 100644 index 0000000..e98f9be --- /dev/null +++ b/src/database/seed_language.py @@ -0,0 +1,145 @@ +from models.language_model import LanguageModel + +languages = [ + {"name": "afrikaans", "acronym": "af"}, + {"name": "albanian", "acronym": "sq"}, + {"name": "amharic", "acronym": "am"}, + {"name": "arabic", "acronym": "ar"}, + {"name": "armenian", "acronym": "hy"}, + {"name": "assamese", "acronym": "as"}, + {"name": "aymara", "acronym": "ay"}, + {"name": "azerbaijani", "acronym": "az"}, + {"name": "bambara", "acronym": "bm"}, + {"name": "basque", "acronym": "eu"}, + {"name": "belarusian", "acronym": "be"}, + {"name": "bengali", "acronym": "bn"}, + {"name": "bhojpuri", "acronym": "bho"}, + {"name": "bosnian", "acronym": "bs"}, + {"name": "bulgarian", "acronym": "bg"}, + {"name": "catalan", "acronym": "ca"}, + {"name": "cebuano", "acronym": "ceb"}, + {"name": "chichewa", "acronym": "ny"}, + {"name": "chinese (simplified)", "acronym": "zh-CN"}, + {"name": "chinese (traditional)", "acronym": "zh-TW"}, + {"name": "corsican", "acronym": "co"}, + {"name": "croatian", "acronym": "hr"}, + {"name": "czech", "acronym": "cs"}, + {"name": "danish", "acronym": "da"}, + {"name": "dhivehi", "acronym": "dv"}, + {"name": "dogri", "acronym": "doi"}, + {"name": "dutch", "acronym": "nl"}, + {"name": "english", "acronym": "en"}, + {"name": "esperanto", "acronym": "eo"}, + {"name": "estonian", "acronym": "et"}, + {"name": "ewe", "acronym": "ee"}, + {"name": "filipino", "acronym": "tl"}, + {"name": "finnish", "acronym": "fi"}, + {"name": "french", "acronym": "fr"}, + {"name": "frisian", "acronym": "fy"}, + {"name": "galician", "acronym": "gl"}, + {"name": "georgian", "acronym": "ka"}, + {"name": "german", "acronym": "de"}, + {"name": "greek", "acronym": "el"}, + {"name": "guarani", "acronym": "gn"}, + {"name": "gujarati", "acronym": "gu"}, + {"name": "haitian creole", "acronym": "ht"}, + {"name": "hausa", "acronym": "ha"}, + {"name": "hawaiian", "acronym": "haw"}, + {"name": "hebrew", "acronym": "iw"}, + {"name": "hindi", "acronym": "hi"}, + {"name": "hmong", "acronym": "hmn"}, + {"name": "hungarian", "acronym": "hu"}, + {"name": "icelandic", "acronym": "is"}, + {"name": "igbo", "acronym": "ig"}, + {"name": "ilocano", "acronym": "ilo"}, + {"name": "indonesian", "acronym": "id"}, + {"name": "irish", "acronym": "ga"}, + {"name": "italian", "acronym": "it"}, + {"name": "japanese", "acronym": "ja"}, + {"name": "javanese", "acronym": "jw"}, + {"name": "kannada", "acronym": "kn"}, + {"name": "kazakh", "acronym": "kk"}, + {"name": "khmer", "acronym": "km"}, + {"name": "kinyarwanda", "acronym": "rw"}, + {"name": "konkani", "acronym": "gom"}, + {"name": "korean", "acronym": "ko"}, + {"name": "krio", "acronym": "kri"}, + {"name": "kurdish (kurmanji)", "acronym": "ku"}, + {"name": "kurdish (sorani)", "acronym": "ckb"}, + {"name": "kyrgyz", "acronym": "ky"}, + {"name": "lao", "acronym": "lo"}, + {"name": "latin", "acronym": "la"}, + {"name": "latvian", "acronym": "lv"}, + {"name": "lingala", "acronym": "ln"}, + {"name": "lithuanian", "acronym": "lt"}, + {"name": "luganda", "acronym": "lg"}, + {"name": "luxembourgish", "acronym": "lb"}, + {"name": "macedonian", "acronym": "mk"}, + {"name": "maithili", "acronym": "mai"}, + {"name": "malagasy", "acronym": "mg"}, + {"name": "malay", "acronym": "ms"}, + {"name": "malayalam", "acronym": "ml"}, + {"name": "maltese", "acronym": "mt"}, + {"name": "maori", "acronym": "mi"}, + {"name": "marathi", "acronym": "mr"}, + {"name": "meiteilon (manipuri)", "acronym": "mni-Mtei"}, + {"name": "mizo", "acronym": "lus"}, + {"name": "mongolian", "acronym": "mn"}, + {"name": "myanmar", "acronym": "my"}, + {"name": "nepali", "acronym": "ne"}, + {"name": "norwegian", "acronym": "no"}, + {"name": "odia (oriya)", "acronym": "or"}, + {"name": "oromo", "acronym": "om"}, + {"name": "pashto", "acronym": "ps"}, + {"name": "persian", "acronym": "fa"}, + {"name": "polish", "acronym": "pl"}, + {"name": "portuguese", "acronym": "pt"}, + {"name": "punjabi", "acronym": "pa"}, + {"name": "quechua", "acronym": "qu"}, + {"name": "romanian", "acronym": "ro"}, + {"name": "russian", "acronym": "ru"}, + {"name": "samoan", "acronym": "sm"}, + {"name": "sanskrit", "acronym": "sa"}, + {"name": "scots gaelic", "acronym": "gd"}, + {"name": "sepedi", "acronym": "nso"}, + {"name": "serbian", "acronym": "sr"}, + {"name": "sesotho", "acronym": "st"}, + {"name": "shona", "acronym": "sn"}, + {"name": "sindhi", "acronym": "sd"}, + {"name": "sinhala", "acronym": "si"}, + {"name": "slovak", "acronym": "sk"}, + {"name": "slovenian", "acronym": "sl"}, + {"name": "somali", "acronym": "so"}, + {"name": "spanish", "acronym": "es"}, + {"name": "sundanese", "acronym": "su"}, + {"name": "swahili", "acronym": "sw"}, + {"name": "swedish", "acronym": "sv"}, + {"name": "tajik", "acronym": "tg"}, + {"name": "tamil", "acronym": "ta"}, + {"name": "tatar", "acronym": "tt"}, + {"name": "telugu", "acronym": "te"}, + {"name": "thai", "acronym": "th"}, + {"name": "tigrinya", "acronym": "ti"}, + {"name": "tsonga", "acronym": "ts"}, + {"name": "turkish", "acronym": "tr"}, + {"name": "turkmen", "acronym": "tk"}, + {"name": "twi", "acronym": "ak"}, + {"name": "ukrainian", "acronym": "uk"}, + {"name": "urdu", "acronym": "ur"}, + {"name": "uyghur", "acronym": "ug"}, + {"name": "uzbek", "acronym": "uz"}, + {"name": "vietnamese", "acronym": "vi"}, + {"name": "welsh", "acronym": "cy"}, + {"name": "xhosa", "acronym": "xh"}, + {"name": "yiddish", "acronym": "yi"}, + {"name": "yoruba", "acronym": "yo"}, + {"name": "zulu", "acronym": "zu"}, +] + + +def seed_language(): + LanguageModel.drop() + + print("Carregando os Idiomas") + for language in languages: + LanguageModel(language).save() diff --git a/src/database/seed_user.py b/src/database/seed_user.py new file mode 100644 index 0000000..c177a40 --- /dev/null +++ b/src/database/seed_user.py @@ -0,0 +1,14 @@ +from models.user_model import UserModel + +languages = [ + {"name": "Peter", "level": "admin", "token": "token_secreto123"}, + {"name": "Vini", "level": "admin", "token": "soeusei"}, +] + + +def seed_user(): + UserModel.drop() + + print("Carregando os Usuários") + for language in languages: + UserModel(language).save() diff --git a/src/models/abstract_model.py b/src/models/abstract_model.py new file mode 100644 index 0000000..95d632f --- /dev/null +++ b/src/models/abstract_model.py @@ -0,0 +1,44 @@ +from pymongo.collection import ReturnDocument + + +class AbstractModel: + _collection = None + + def __init__(self, data): + self.data = data.copy() + + def save(self): + result = self._collection.insert_one(self.data) + inserted_document = self._collection.find_one( + {"_id": result.inserted_id} + ) + self.data = inserted_document + self.id = str(inserted_document["_id"]) + return self + + def update(self, data): + result = self._collection.find_one_and_update( + {"_id": self.data["_id"]}, + {"$set": data}, + return_document=ReturnDocument.AFTER, + ) + + self.data = result + return self.data + + def delete(self): + self._collection.delete_one({"_id": self.data["_id"]}) + + @classmethod + def find(cls, query={}): + data = cls._collection.find(query) + return [cls(d) for d in data] + + @classmethod + def find_one(cls, query={}): + data = cls._collection.find_one(query) + return cls(data) if data else None + + @classmethod + def drop(cls): + cls._collection.drop() diff --git a/src/models/history_model.py b/src/models/history_model.py new file mode 100644 index 0000000..e70052c --- /dev/null +++ b/src/models/history_model.py @@ -0,0 +1,25 @@ +from database.db import db +from models.abstract_model import AbstractModel + +import json +from bson import ObjectId +from datetime import datetime + + +class HistoryModel(AbstractModel): + _collection = db["history"] + + def __init__(self, json_data): + super().__init__(json_data) + + @classmethod + def list_as_json(cls, query={}): + data = cls._collection.find(query) + return BsonToJson().encode(list(data)) + + +class BsonToJson(json.JSONEncoder): + def default(self, attribute): + if isinstance(attribute, (ObjectId, datetime)): + return str(attribute) + return json.JSONEncoder.default(self, attribute) diff --git a/src/models/user_model.py b/src/models/user_model.py new file mode 100644 index 0000000..81923f5 --- /dev/null +++ b/src/models/user_model.py @@ -0,0 +1,9 @@ +from database.db import db +from models.abstract_model import AbstractModel + + +class UserModel(AbstractModel): + _collection = db["users"] + + def token_is_valid(self, token): + return self.data["token"] == token diff --git a/src/run_seeds.py b/src/run_seeds.py new file mode 100644 index 0000000..1e07d9b --- /dev/null +++ b/src/run_seeds.py @@ -0,0 +1,7 @@ +# import models.languageModel as LanguageModel +from database.seed_language import seed_language +from database.seed_user import seed_user + + +seed_language() +seed_user() diff --git a/src/views/static/css/main.css b/src/views/static/css/main.css new file mode 100644 index 0000000..d2187c5 --- /dev/null +++ b/src/views/static/css/main.css @@ -0,0 +1,97 @@ +/* Estilos gerais */ +*{ + box-sizing: border-box; + font-family: Shanti; +} + +/* Estilos do body */ +body{ + justify-content: center; + background: #094493 +} + + +.header{ + width: 25%; + justify-content: left; +} + +/* Estilos do conteúdo principal */ +.container{ + width: 95%; + padding: 0 30px 30px 30px; + background: #ffffff; + border-radius: 20px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) +} + +.wrapper{ + border-radius: 10px; + border: 3px solid #bdd5ff; +} + +.wrapper .text-input{ + display: flex; + border-bottom: 3px solid #bdd5ff; +} +.text-input .to-text{ + border-radius: 0px; + border-left: 3px solid #bdd5ff; + background-color: #bdd5ff; +} + +.text-input textarea{ + min-height: 45vh; + width: 96%; + border: none; + outline: none; + resize: none; + background: #fafafa; + font-size: 24px; + padding: 10px 10px; + border-radius: 5px; +} + +.controls, li { + display: flex; + align-items: center; + justify-content: space-between; +} + +.controls{ + list-style: none; + padding: 5px 35px; + height: 25px; +} + +.controls .row select{ + border: none; + font-size: 24px; +} + +.tool { + font-size: 25px; + padding: 6px; + border: none; + color: #fff; + cursor: pointer; + border-radius: 10px; + background: #bdd5ff; + width: 115%; +} + +.main-button { + margin-top: 20px; + font-size: 40px; + width: 100%; + padding: 14px; + outline: none; + border: none; + color: #fff; + cursor: pointer; + border-radius: 5px; + background: #0376E2 +} \ No newline at end of file diff --git a/src/views/static/images/get_api.png b/src/views/static/images/get_api.png new file mode 100644 index 0000000..3e65a93 Binary files /dev/null and b/src/views/static/images/get_api.png differ diff --git a/src/views/static/images/logo.png b/src/views/static/images/logo.png new file mode 100644 index 0000000..d0c2a4c Binary files /dev/null and b/src/views/static/images/logo.png differ diff --git a/src/views/static/images/traduzo.png b/src/views/static/images/traduzo.png new file mode 100644 index 0000000..001d465 Binary files /dev/null and b/src/views/static/images/traduzo.png differ diff --git a/src/views/templates/index.html b/src/views/templates/index.html new file mode 100644 index 0000000..6e0df86 --- /dev/null +++ b/src/views/templates/index.html @@ -0,0 +1,35 @@ + + + + Traduzo - Trybe + + + + +
+ +
+
+
+ + +
+
    +
  • + +
  • +
    + +
    +
  • + +
  • +
+
+ + +
+ + \ 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/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a5f1202 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest +from pymongo.database import Database + + +@pytest.fixture(autouse=True) +def set_test_environment(): + try: + from src.database.db import db + except ImportError as error: + pytest.skip(reason=str(error)) + + assert isinstance(db, Database), "db is not a instance of Database" + + db.get_collection("languages").drop() + db.get_collection("history").drop() + db.get_collection("users").drop() + yield diff --git a/tests/controllers/__init__.py b/tests/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/controllers/admin/__init__.py b/tests/controllers/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/controllers/admin/conftest.py b/tests/controllers/admin/conftest.py new file mode 100644 index 0000000..8e5c16e --- /dev/null +++ b/tests/controllers/admin/conftest.py @@ -0,0 +1,34 @@ +import pytest + +from src.models.history_model import HistoryModel + +try: + from src.app import app + from src.database.db import db +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + + +@pytest.fixture(autouse=True) +def app_test(): + return app.test_client() + + +@pytest.fixture(autouse=True) +def prepare_base(app_test): + db.get_collection("history").drop() + HistoryModel( + { + "text_to_translate": "Hello, I like videogame", + "translate_from": "en", + "translate_to": "pt", + } + ).save() + + HistoryModel( + { + "text_to_translate": "Do you love music?", + "translate_from": "en", + "translate_to": "pt", + } + ).save() diff --git a/tests/controllers/admin/mocks.py b/tests/controllers/admin/mocks.py new file mode 100644 index 0000000..0c6dcc6 --- /dev/null +++ b/tests/controllers/admin/mocks.py @@ -0,0 +1,21 @@ +from src.models.user_model import UserModel + + +class _TestUserEmptyAttributes(UserModel): + """ + E se os atributos do usuário forem salvos com valor diferente no banco? + """ + + def __init__(self, json_data: dict): + json_data.update({key: "" for key in json_data}) + super().__init__(json_data) + + +class _TestUserNameEqualsToken(UserModel): + """ + E se o token e nomes possuirem valores iguais? + """ + + def __init__(self, json_data: dict): + json_data.update(name=json_data["token"], token=json_data["name"]) + super().__init__(json_data) diff --git a/tests/controllers/admin/test_admin_controller.py b/tests/controllers/admin/test_admin_controller.py new file mode 100644 index 0000000..d912592 --- /dev/null +++ b/tests/controllers/admin/test_admin_controller.py @@ -0,0 +1,6 @@ +# from src.models.history_model import HistoryModel +# from src.models.user_model import UserModel + + +def test_history_delete(app_test): + raise NotImplementedError diff --git a/tests/controllers/admin/test_to_test_admin_controller.py b/tests/controllers/admin/test_to_test_admin_controller.py new file mode 100644 index 0000000..6a15966 --- /dev/null +++ b/tests/controllers/admin/test_to_test_admin_controller.py @@ -0,0 +1,56 @@ +from typing import Callable +from unittest.mock import patch + +import pytest +from pytest_dependency import ( + assert_fails_with_broken_asset, + get_skip_markers, + get_test_assessment_configs, + run_pytest_quietly, +) + +try: + from src.models.user_model import UserModel # specific + from tests.controllers.admin import mocks # specific +except ImportError as error: + raise NotImplementedError(error) +from tests.controllers.admin import test_admin_controller # specific + +TA_CFG = get_test_assessment_configs( + UserModel, + mocks, + test_admin_controller, +) + +pytestmark = get_skip_markers(TA_CFG) + + +@pytest.mark.dependency() +def test_students_sanity_check(): + return_code = run_pytest_quietly([TA_CFG.STUDENT_TEST_FILE_PATH]) + + if return_code != pytest.ExitCode.OK: + pytest.skip( + f"Seus testes em {TA_CFG.STUDENT_TEST_FILE_PATH} " + "ainda não estão passando! " + "Verifique-os e tente novamente." + ) + + +@pytest.mark.dependency(depends=["test_students_sanity_check"]) +@pytest.mark.parametrize( + "broken_asset", + TA_CFG.BROKEN_ASSETS_LIST, +) +def test_assess_students_user_model(broken_asset: Callable): + with patch(TA_CFG.PATCH_TARGET, broken_asset): + return_code = run_pytest_quietly([TA_CFG.STUDENT_TEST_FILE_PATH]) + + assert_fails_with_broken_asset(broken_asset, return_code, TA_CFG) + + +@pytest.mark.dependency( + depends=["test_assess_students_user_model"], include_all_instances=True +) +def test_assess_students_user_model_final(): + pass diff --git a/tests/controllers/conftest.py b/tests/controllers/conftest.py new file mode 100644 index 0000000..78f925f --- /dev/null +++ b/tests/controllers/conftest.py @@ -0,0 +1,28 @@ +import pytest + +try: + from src.app import app +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + +try: + from src.models.language_model import LanguageModel +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + + +@pytest.fixture(autouse=True) +def app_test(): + return app.test_client() + + +@pytest.fixture(autouse=True) +def add_countries(): + list_of_languages = [ + {"name": "Afrikaans", "acronym": "af"}, + {"name": "english", "acronym": "en"}, + {"name": "Portugues", "acronym": "pt"}, + ] + + for language in list_of_languages: + LanguageModel(language).save() diff --git a/tests/controllers/test_history_controller.py b/tests/controllers/test_history_controller.py new file mode 100644 index 0000000..50780e9 --- /dev/null +++ b/tests/controllers/test_history_controller.py @@ -0,0 +1,36 @@ +import pytest + +try: + from src.database.db import db +except ImportError as error: + pytestmark = pytest.mark.skip(reason=str(error)) + +import pytest + + +@pytest.fixture(autouse=True) +def prepare_base(app_test): + db.get_collection("history").drop() + app_test.post( + "/", + data={ + "text-to-translate": "Hello, I like videogame", + "translate-from": "en", + "translate-to": "pt", + }, + ) + + app_test.post( + "/", + data={ + "text-to-translate": "Do you love music?", + "translate-from": "en", + "translate-to": "pt", + }, + ) + + +def test_request_history(app_test): + response = app_test.get("/history/") + assert "Hello, I like videogame" in response.get_data(as_text=True) + assert "Do you love music?" in response.get_data(as_text=True) diff --git a/tests/controllers/test_translate_controller.py b/tests/controllers/test_translate_controller.py new file mode 100644 index 0000000..ebc6e09 --- /dev/null +++ b/tests/controllers/test_translate_controller.py @@ -0,0 +1,157 @@ +from flask.testing import FlaskClient +from bs4 import BeautifulSoup +import pytest + +try: + from src.models.language_model import LanguageModel +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + + +def test_request_translate(app_test: FlaskClient): + response = app_test.get("/") + assert response.status_code != 404, "A rota '/' não foi encontrada" + + soup = BeautifulSoup(response.text, "html.parser") + + assert ( + soup.find("textarea", {"class": "from-text"}).text + == "O que deseja traduzir?" + ) + assert ( + soup.find("textarea", {"class": "to-text"}).text + == "What do you want to translate?" + ) + + from_languages = [ + language.text.strip() + for language in soup.find( + "select", {"name": "translate-from"} + ).find_all("option") + ] + + assert all( + language in from_languages + for language in ["ENGLISH", "AFRIKAANS", "PORTUGUES"] + ) + + to_languages = [ + language.text.strip() + for language in soup.find("select", {"name": "translate-to"}).find_all( + "option" + ) + ] + + assert all( + language in to_languages + for language in ["ENGLISH", "AFRIKAANS", "PORTUGUES"] + ) + + assert ( + len(from_languages) + len(to_languages) + == len(LanguageModel.find()) * 2 + ) + + selected_from = soup.find("select", {"name": "translate-from"}).find( + "option", {"selected": True} + ) + + assert selected_from, "Uma opção 'translate-from' deve estar selecionada" + + assert ( + selected_from["value"] + == LanguageModel.find({"acronym": selected_from["value"]})[ + 0 + ].to_dict()["acronym"] + ) + + selected_to = soup.find("select", {"name": "translate-to"}).find( + "option", {"selected": True} + ) + + assert selected_to, "Uma opção 'translate-to' deve estar selecionada" + + assert ( + selected_to["value"] + == LanguageModel.find({"acronym": selected_to["value"]})[0].to_dict()[ + "acronym" + ] + ) + + +def test_post_translate(app_test: FlaskClient): + response = app_test.post( + "/", + data={ + "text-to-translate": "Hello, I like videogame", + "translate-from": "en", + "translate-to": "pt", + }, + ) + assert response.status_code != 404, "A rota '/' não foi encontrada" + + soup = BeautifulSoup(response.text, "html.parser") + + assert ( + soup.find("textarea", {"class": "from-text"}).text + == "Hello, I like videogame" + ) + assert ( + soup.find("textarea", {"class": "to-text"}).text + == "Olá, eu gosto de videogame" + ) + + selected_from = soup.find("select", {"name": "translate-from"}).find( + "option", {"selected": True} + ) + + assert selected_from, "Uma opção 'translate-from' deve estar selecionada" + + assert selected_from["value"] == "en" + + selected_to = soup.find("select", {"name": "translate-to"}).find( + "option", {"selected": True} + ) + + assert selected_to, "Uma opção 'translate-to' deve estar selecionada" + + assert selected_to["value"] == "pt" + + +def test_post_reverse(app_test: FlaskClient): + response = app_test.post( + "/reverse", + data={ + "text-to-translate": "Hello, I like videogame", + "translate-from": "en", + "translate-to": "pt", + }, + ) + assert response.status_code != 404, "A rota '/reverse' não foi encontrada" + + soup = BeautifulSoup(response.text, "html.parser") + + assert ( + soup.find("textarea", {"class": "from-text"}).text + == "Olá, eu gosto de videogame" + ) + assert ( + soup.find("textarea", {"class": "to-text"}).text + == "Hello, I like videogame" + ) + + selected_from = soup.find("select", {"name": "translate-from"}).find( + "option", {"selected": True} + ) + + assert selected_from, "Uma opção 'translate-from' deve estar selecionada" + + assert selected_from["value"] == "pt" + + selected_to = soup.find("select", {"name": "translate-to"}).find( + "option", {"selected": True} + ) + + assert selected_to, "Uma opção 'translate-to' deve estar selecionada" + + assert selected_to["value"] == "en" diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/history/__init__.py b/tests/models/history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/history/conftest.py b/tests/models/history/conftest.py new file mode 100644 index 0000000..1ec0e0f --- /dev/null +++ b/tests/models/history/conftest.py @@ -0,0 +1,27 @@ +import pytest + +try: + from src.database.db import db + from src.models.history_model import HistoryModel +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + + +@pytest.fixture(autouse=True) +def prepare_base(): + db.get_collection("history").drop() + HistoryModel( + { + "text_to_translate": "Hello, I like videogame", + "translate_from": "en", + "translate_to": "pt", + } + ).save() + + HistoryModel( + { + "text_to_translate": "Do you love music?", + "translate_from": "en", + "translate_to": "pt", + } + ).save() diff --git a/tests/models/history/mocks.py b/tests/models/history/mocks.py new file mode 100644 index 0000000..b737558 --- /dev/null +++ b/tests/models/history/mocks.py @@ -0,0 +1,17 @@ +from src.models.history_model import HistoryModel + + +class _TestUppperVersionOfJson(HistoryModel): + """E se os dados vierem com todas as letras maiúsculas?""" + + @classmethod + def list_as_json(cls, query={}): + return super(_TestUppperVersionOfJson, cls).list_as_json(query).upper() + + +class _TestLowerVersionOfJson(HistoryModel): + """E se os dados vierem com todas as letras minúsculas?""" + + @classmethod + def list_as_json(cls, query={}): + return super(_TestLowerVersionOfJson, cls).list_as_json(query).lower() diff --git a/tests/models/history/test_history_model.py b/tests/models/history/test_history_model.py new file mode 100644 index 0000000..759ff35 --- /dev/null +++ b/tests/models/history/test_history_model.py @@ -0,0 +1,7 @@ +# import json +# from src.models.history_model import HistoryModel + + +# Req. 7 +def test_request_history(): + raise NotImplementedError diff --git a/tests/models/history/test_to_test_history_model.py b/tests/models/history/test_to_test_history_model.py new file mode 100644 index 0000000..103dc63 --- /dev/null +++ b/tests/models/history/test_to_test_history_model.py @@ -0,0 +1,61 @@ +from typing import Callable +from unittest.mock import patch + +import pytest +from pytest_dependency import ( + assert_fails_with_broken_asset, + get_skip_markers, + get_test_assessment_configs, + run_pytest_quietly, +) + +try: + from src.models.history_model import HistoryModel # specific + from tests.models.history import mocks # specific +except ImportError as error: + raise NotImplementedError(error) +from tests.models.history import test_history_model # specific + +TA_CFG = get_test_assessment_configs( + HistoryModel, + mocks, + test_history_model, +) + +pytestmark = get_skip_markers(TA_CFG) + + +@pytest.mark.dependency() +def test_students_sanity_check(): + return_code = run_pytest_quietly([TA_CFG.STUDENT_TEST_FILE_PATH]) + + if return_code != pytest.ExitCode.OK: + pytest.skip( + f"Seus testes em {TA_CFG.STUDENT_TEST_FILE_PATH} " + "ainda não estão passando! " + "Verifique-os e tente novamente." + ) + + +@pytest.mark.dependency(depends=["test_students_sanity_check"]) +@pytest.mark.parametrize( + "broken_asset", + TA_CFG.BROKEN_ASSETS_LIST, +) +def test_assess_students_history_model(broken_asset: Callable): + with patch(TA_CFG.PATCH_TARGET, broken_asset): + return_code = run_pytest_quietly([TA_CFG.STUDENT_TEST_FILE_PATH]) + + if return_code == pytest.ExitCode.TESTS_FAILED: + assert_fails_with_broken_asset( + broken_asset, pytest.ExitCode.TESTS_FAILED, TA_CFG + ) + + assert_fails_with_broken_asset(broken_asset, return_code, TA_CFG) + + +@pytest.mark.dependency( + depends=["test_assess_students_history_model"], include_all_instances=True +) +def test_assess_students_history_model_final(): + pass diff --git a/tests/models/test_language_model.py b/tests/models/test_language_model.py new file mode 100644 index 0000000..a92ae18 --- /dev/null +++ b/tests/models/test_language_model.py @@ -0,0 +1,36 @@ +import pytest + +try: + from src.models.language_model import LanguageModel +except ImportError as error: + pytestmark = pytest.mark.skip(reason=error.msg) + + +def test_create_language() -> None: + language = LanguageModel({"name": "afrikaans", "acronym": "af"}) + saved_language = language.save() + + assert saved_language.id is not None + assert isinstance(saved_language.id, str) + + +def test_create_dict_language() -> None: + language = LanguageModel({"name": "afrikaans", "acronym": "af"}) + saved_language = language.save() + dict_language = saved_language.to_dict() + assert dict_language["name"] == "afrikaans" + assert dict_language["acronym"] == "af" + assert len(dict_language) == 2 + + +def test_create_list_languages() -> None: + list_of_languages = [ + {"name": "Afrikaans", "acronym": "af"}, + {"name": "english", "acronym": "en"}, + {"name": "Portugues", "acronym": "pt"}, + ] + + for language in list_of_languages: + LanguageModel(language).save() + + assert LanguageModel.list_dicts() == list_of_languages diff --git a/tests/test_db.py b/tests/test_db.py new file mode 100644 index 0000000..824c5d5 --- /dev/null +++ b/tests/test_db.py @@ -0,0 +1,13 @@ + + +from pymongo import MongoClient + + +def test_database_connection(): + try: + from src.database.db import db + except ImportError as error: + assert False, error + + assert db.name == "test_db_traduzo" + assert isinstance(db.client, MongoClient) diff --git a/thunder-tests/thunderActivity.json b/thunder-tests/thunderActivity.json new file mode 100644 index 0000000..7cdeee1 --- /dev/null +++ b/thunder-tests/thunderActivity.json @@ -0,0 +1,255 @@ +[ + { + "_id": "5da4fc3a-fb03-48cc-bfcb-47df08b650e5", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/history/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/admin/history/64a3d35c7e97610fff47b374", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:12:10.805Z", + "modified": "2023-07-04T08:12:49.672Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "de63d6dd-7ea1-41df-8074-c526230024c8", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/histsory/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/admin/histsory/64a3d35c7e97610fff47b374", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:13:23.156Z", + "modified": "2023-07-04T08:13:23.156Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "0b1db2d1-e7b8-4d8b-879a-c3a290e9ab61", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/histssory/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/admin/histssory/64a3d35c7e97610fff47b374", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:13:29.311Z", + "modified": "2023-07-04T08:13:29.311Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "067e32df-1c29-4298-afab-0480049449a7", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/admin/64a3d35c7e97610fff47b374", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:13:49.955Z", + "modified": "2023-07-04T08:13:49.955Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "1c724efa-1ec1-4aab-9ea3-41f8f7808109", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/history/", + "url": "127.0.0.1:9000/admin/history/", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:14:53.182Z", + "modified": "2023-07-04T08:14:53.182Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "3b7fd368-a1fa-4b8b-88e4-5dc1266354b1", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/history/2", + "url": "127.0.0.1:9000/admin/history/2", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:14:55.289Z", + "modified": "2023-07-04T08:14:55.289Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "49078f1c-6083-4ca7-b144-c03a9f95e2be", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin", + "url": "127.0.0.1:9000/admin", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:14:58.238Z", + "modified": "2023-07-04T08:14:58.238Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "4ec32673-7537-46dc-a74d-6248678a96e8", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/adminasd", + "url": "127.0.0.1:9000/adminasd", + "method": "GET", + "sortNum": 0, + "created": "2023-07-04T08:15:01.355Z", + "modified": "2023-07-04T08:15:01.355Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "cc9e753f-2501-46d4-b9ba-622c23555128", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/admin/history/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/admin/history/64a3d35c7e97610fff47b374", + "method": "DELETE", + "sortNum": 0, + "created": "2023-07-04T08:15:55.462Z", + "modified": "2023-07-04T08:15:55.462Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "cc395d96-caf5-40e8-bf10-0713ae149068", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:9000/history/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:9000/history/64a3d35c7e97610fff47b374", + "method": "DELETE", + "sortNum": 0, + "created": "2023-07-04T08:16:33.191Z", + "modified": "2023-07-04T08:16:33.191Z", + "headers": [ + { + "name": "name", + "value": "Vini" + } + ], + "params": [], + "auth": { + "type": "bearer", + "bearer": "soeusei" + }, + "tests": [] + }, + { + "_id": "dc142ee9-d876-4f36-abb1-cfe3d043e988", + "colId": "history", + "containerId": "", + "name": "127.0.0.1:8000/admin/history/64a3d35c7e97610fff47b374", + "url": "127.0.0.1:8000/admin/history/64a3d6640ba167294f94af10", + "method": "DELETE", + "sortNum": 0, + "created": "2023-07-04T08:16:47.538Z", + "modified": "2023-07-04T08:28:50.322Z", + "headers": [ + { + "name": "User", + "value": "Vini" + }, + { + "name": "Authorization", + "value": "soeusei" + } + ], + "params": [], + "tests": [] + } +] \ No newline at end of file