From e43598b845c181a3e00875bcfc411b218f3e9eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nestor=20Rodr=C3=ADguez?= Date: Sun, 16 Jan 2022 23:09:34 -0300 Subject: [PATCH 1/3] challange-project-ateliware --- .gitignore | 6 ++ .idea/.gitignore | 3 + .idea/ChallangeProject.iml | 8 ++ .../inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 10 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ Dockerfile | 11 ++ Procfile | 1 + README.md | 54 +++++++--- app.py | 12 +++ docker-compose.yml | 22 ++++ instance/flask.cfg | 36 +++++++ instance/flask_test.cfg | 42 ++++++++ project/__init__.py | 44 ++++++++ project/controllers/Core.py | 62 +++++++++++ project/controllers/__init__.py | 0 project/migrations/README | 1 + project/migrations/__init__.py | 0 project/migrations/alembic.ini | 50 +++++++++ project/migrations/env.py | 91 ++++++++++++++++ project/migrations/script.py.mako | 24 +++++ project/migrations/versions/6bafd3cebeab_.py | 34 ++++++ project/migrations/versions/76423362cc53_.py | 28 +++++ project/migrations/versions/__init__.py | 0 project/models/__init__.py | 0 project/models/database.py | 38 +++++++ project/models/repository.py | 7 ++ project/templates/base.html | 27 +++++ project/templates/renders/history.html | 38 +++++++ project/templates/renders/search.html | 101 ++++++++++++++++++ project/views/__init__.py | 0 project/views/history.py | 26 +++++ project/views/search.py | 28 +++++ requirements.txt | Bin 0 -> 1582 bytes runtime.txt | 1 + tests/conftest.py | 42 ++++++++ tests/functional/test_functional.py | 59 ++++++++++ tests/pytest.ini | 0 tests/unit/test_models.py | 51 +++++++++ uwsgi.ini | 7 ++ 41 files changed, 969 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/ChallangeProject.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Dockerfile create mode 100644 Procfile create mode 100644 app.py create mode 100644 docker-compose.yml create mode 100644 instance/flask.cfg create mode 100644 instance/flask_test.cfg create mode 100644 project/__init__.py create mode 100644 project/controllers/Core.py create mode 100644 project/controllers/__init__.py create mode 100644 project/migrations/README create mode 100644 project/migrations/__init__.py create mode 100644 project/migrations/alembic.ini create mode 100644 project/migrations/env.py create mode 100644 project/migrations/script.py.mako create mode 100644 project/migrations/versions/6bafd3cebeab_.py create mode 100644 project/migrations/versions/76423362cc53_.py create mode 100644 project/migrations/versions/__init__.py create mode 100644 project/models/__init__.py create mode 100644 project/models/database.py create mode 100644 project/models/repository.py create mode 100644 project/templates/base.html create mode 100644 project/templates/renders/history.html create mode 100644 project/templates/renders/search.html create mode 100644 project/views/__init__.py create mode 100644 project/views/history.py create mode 100644 project/views/search.py create mode 100644 requirements.txt create mode 100644 runtime.txt create mode 100644 tests/conftest.py create mode 100644 tests/functional/test_functional.py create mode 100644 tests/pytest.ini create mode 100644 tests/unit/test_models.py create mode 100644 uwsgi.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..3dc631e7b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +__pycache__/ +*.pyc +.env +.coverage +.pytest_cache diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/ChallangeProject.iml b/.idea/ChallangeProject.iml new file mode 100644 index 0000000000..d0876a78d0 --- /dev/null +++ b/.idea/ChallangeProject.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000..105ce2da2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..5771aab46d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..5e9caf3743 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..94a25f7f4c --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..e969f9875e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +CMD [ "python", "./app.py" ] \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..bef6317d62 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uwsgi uwsgi.ini \ No newline at end of file diff --git a/README.md b/README.md index 3f1e493650..6ece5fbde4 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,46 @@ -# Desafio técnico para desenvolvedores +# Ateliware project challange Construa uma nova aplicação, utilizando o framework de sua preferência (Ruby on Rails, Elixir Phoenix, Python Django ou Flask, NodeJS Sails, Java Spring, ASP.NET ou outro), a qual deverá conectar na API do GitHub e disponibilizar as seguintes funcionalidades: -- Botão para buscar e armazenar os repositórios destaques de 5 linguagens à sua escolha; -- Listar os repositórios encontrados; -- Visualizar os detalhes de cada repositório. +Botão para buscar e armazenar os repositórios destaques de 5 linguagens à sua escolha; +Listar os repositórios encontrados; +Visualizar os detalhes de cada repositório. -Alguns requisitos: -- Deve ser uma aplicação totalmente nova; -- A solução deve estar em um repositório público do GitHub; -- A aplicação deve armazenar as informações encontradas; -- Utilizar PostgreSQL, MySQL ou SQL Server; -- O deploy deve ser realizado, preferencialmente, no Heroku, AWS ou no Azure; -- A aplicação precisa ter testes automatizados; -- Preferenciamente dockerizar a aplicação; -- Por favor atualizar o readme da aplicação com passo a passo com instrução para subir o ambiente. +# About this project + +- Python 3.10 +- Postgres 14 +- Frontend Frameworks: Flask, Bootstrap +- Docker +- Heroku +- Unit Test + +# Deploy on Heroku +- https://ateliware-project-challange.herokuapp.com/ + +# Installation steps +- open project in terminal and run: + - $ docker-compose up --build + +- open browser on: http://127.0.0.1:8001 + +# Requirements +- Docker + +## Unit Tests +- Test new Repository (language, full_name=, html_url, stargazers_count, description) +- Test new History (email, fullname, language, url, description, date) +- Check register information found on database + +## Functional Tests +- Search: + - Test page response + - Test make a new search + - Test GET and POST methods + +- History: + - Test page response + -Quando terminar, faça um Pull Request neste repo e avise-nos por email. -**IMPORTANTE:** se você não conseguir finalizar o teste, por favor nos diga o motivo e descreva quais foram as suas dificuldades. Você pode também sugerir uma outra abordagem para avaliarmos seus skills técnicos, vender seu peixe, mostrar-nos do que é capaz. diff --git a/app.py b/app.py new file mode 100644 index 0000000000..ef3e94eab3 --- /dev/null +++ b/app.py @@ -0,0 +1,12 @@ +from project import create_app +from project.models.database import db + + +# Call the application factory function to construct a Flask application +# instance using the development configuration +app = create_app('flask.cfg') +app.app_context().push() +db.create_all() + +if __name__ == '__main__': + app.run(port=8001) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..95d11bddc6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.10" + +services: + webserver: + build: . + command: uwsgi --ini uwsgi.ini + volumes: + - .:/app + ports: + - 8001:8001 + depends_on: + - postgresdb + postgresdb: + image: postgres:14 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=QWERTY + volumes: + - postgres_data:/var/lib/postgresql/data/ + +volumes: + postgres_data: \ No newline at end of file diff --git a/instance/flask.cfg b/instance/flask.cfg new file mode 100644 index 0000000000..29e933cba5 --- /dev/null +++ b/instance/flask.cfg @@ -0,0 +1,36 @@ +########################################################## +# +# This is a sample flask.cfg for developing a Flask application +# +########################################################## +import os + + +# Get the folder of the top-level directory of this project +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + +# Update later by using a random number generator and moving +# the actual key outside of the source code under version control +SECRET_KEY = 'cd48e1c22de0961d5d1bfb14f8a66e006cfb1cfbf3f0c0f3' +WTF_CSRF_ENABLED = True +DEBUG = False + +# DB CONNECTION +DATABASE = { + 'engine': 'postgresql', + 'user': 'postgres', + 'pw': 'QWERTY', + 'db': 'postgres', + 'host': 'postgresdb', #postgresdb on docker + 'port': '5432', +} + +# Get database url from environment variables on Heroku else Docker +database_url = os.environ.get('DATABASE_URL', '%(engine)s://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % DATABASE) +if database_url and database_url.startswith("postgres://"): + database_url = database_url.replace("postgres://", "postgresql://", 1) +# rest of connection code using the connection string `database_url` + +# SQLAlchemy +SQLALCHEMY_DATABASE_URI = database_url +SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/instance/flask_test.cfg b/instance/flask_test.cfg new file mode 100644 index 0000000000..66e0b1c01d --- /dev/null +++ b/instance/flask_test.cfg @@ -0,0 +1,42 @@ +########################################################## +# +# flask_test.cfg is intended to be used for testing a Flask application +# +########################################################## +import os + + +# Get the folder of the top-level directory of this project +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + +# Update later by using a random number generator and moving +# the actual key outside of the source code under version control +SECRET_KEY = 'cd48e1c22de0961d5d1bfb14f8a66e006cfb1cfbf3f0c0f3' +DEBUG = True + +# DB CONNECTION +DATABASE = { + 'engine': 'postgresql', + 'user': 'postgres', + 'pw': 'QWERTY', + 'db': 'postgres', + 'host': 'localhost', #postgresdb on docker + 'port': '5432', +} + +# Get database url from environment variables on Heroku else Docker +database_url = os.environ.get('DATABASE_URL', '%(engine)s://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % DATABASE) +if database_url and database_url.startswith("postgres://"): + database_url = database_url.replace("postgres://", "postgresql://", 1) +# rest of connection code using the connection string `database_url` + +# SQLAlchemy +SQLALCHEMY_DATABASE_URI = database_url +SQLALCHEMY_TRACK_MODIFICATIONS = False + +# Enable the TESTING flag to disable the error catching during request handling +# so that you get better error reports when performing test requests against the application. +TESTING = True + +# Disable CSRF tokens in the Forms (only valid for testing purposes!) +WTF_CSRF_ENABLED = False diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000000..a0252f01e1 --- /dev/null +++ b/project/__init__.py @@ -0,0 +1,44 @@ +from flask import Flask +from flask_migrate import Migrate +from flask_bootstrap import Bootstrap5 +from project.models.database import db + + +####################### +#### Configuration #### +####################### + + +###################################### +#### Application Factory Function #### +###################################### + +def create_app(config_filename=None): + app = Flask(__name__, instance_relative_config=True) + app.config.from_pyfile(config_filename) + initialize_extensions(app) + register_blueprints(app) + bootstrap = Bootstrap5(app) + return app + + +########################## +#### Helper Functions #### +########################## + +def initialize_extensions(app): + # Since the application instance is now created, pass it to each Flask + # extension instance to bind it to the Flask application instance (app) + db.init_app(app) + migrate = Migrate() + migrate.init_app(app, db) + + +def register_blueprints(app): + # Since the application instance is now created, register each Blueprint + # with the Flask application instance (app) + from project.views.history import history_blueprints + from project.views.search import search_blueprints + + app.register_blueprint(history_blueprints, url_prefix="/history") + app.register_blueprint(search_blueprints, url_prefix="/") diff --git a/project/controllers/Core.py b/project/controllers/Core.py new file mode 100644 index 0000000000..98d4fd7c1e --- /dev/null +++ b/project/controllers/Core.py @@ -0,0 +1,62 @@ +from datetime import datetime +from project.models.database import db, History +import json +from types import SimpleNamespace +from urllib.parse import urlencode +import requests +from project.models.repository import Repo + + +# DATABASE +def handled_history(email, data): + # store the repositories found in the search + new_history = History(email=email, fullname=data.full_name, language=data.language, url=data.html_url, + description=data.description, date=datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + + db.session.add(new_history) + db.session.commit() + + +# GITHUB +def search_github(email, keywords): + # API URL EXAMPLE https://api.github.com/search/repositories?q=Python+language:Python&sort=stars&order=desc + # This request search for repositories with the word "Python" in the name, the description, or the README. + # Limiting the results to only find repositories where the primary language is Python. Sorting by stars + # in descending order, so that the most popular repositories appear first in the search results. + + results = [] + qty = 0 + keywords = [keyword.strip() for keyword in keywords.split(',')] + for keyword in keywords: + api_url = "https://api.github.com/search/repositories?" + sort, order = 'star', 'desc' + params = {'q': (keyword + 'language:' + keyword), 'sort': sort, 'order': order} + query_url = api_url + urlencode(params) + r = requests.get(query_url) + if r.status_code == 200: + data = json.loads(r.text, object_hook=lambda d: SimpleNamespace(**d)) + qty += len(data.items) + for i in data.items[:2]: + results.append(Repo(i)) + + for repo in results: + handled_history(email, repo) + + return results, qty + + +def get_languages(): + # Retrieve a list of languages from GitHub API https://api.github.com/languages + languages_list = [] + api_url = "https://api.github.com/languages" + r = requests.get(api_url) + if r.status_code == 200: + data = json.loads(r.text) + for lang in data: + languages_list.append(lang['name']) + return languages_list + + else: + # In case response 403 GitHub API Rate limit exceeded return an error + return {'error loading data'} + diff --git a/project/controllers/__init__.py b/project/controllers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/migrations/README b/project/migrations/README new file mode 100644 index 0000000000..0e04844159 --- /dev/null +++ b/project/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/project/migrations/__init__.py b/project/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/migrations/alembic.ini b/project/migrations/alembic.ini new file mode 100644 index 0000000000..ec9d45c26a --- /dev/null +++ b/project/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/project/migrations/env.py b/project/migrations/env.py new file mode 100644 index 0000000000..68feded2a0 --- /dev/null +++ b/project/migrations/env.py @@ -0,0 +1,91 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/project/migrations/script.py.mako b/project/migrations/script.py.mako new file mode 100644 index 0000000000..2c0156303a --- /dev/null +++ b/project/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/project/migrations/versions/6bafd3cebeab_.py b/project/migrations/versions/6bafd3cebeab_.py new file mode 100644 index 0000000000..976652c277 --- /dev/null +++ b/project/migrations/versions/6bafd3cebeab_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 6bafd3cebeab +Revises: +Create Date: 2022-01-12 15:52:28.245315 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6bafd3cebeab' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=160), nullable=False), + sa.Column('language', sa.String(length=160), nullable=False), + sa.Column('url', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('history') + # ### end Alembic commands ### diff --git a/project/migrations/versions/76423362cc53_.py b/project/migrations/versions/76423362cc53_.py new file mode 100644 index 0000000000..8c02e08aa0 --- /dev/null +++ b/project/migrations/versions/76423362cc53_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 76423362cc53 +Revises: 6bafd3cebeab +Create Date: 2022-01-12 16:09:57.289246 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '76423362cc53' +down_revision = '6bafd3cebeab' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('history', sa.Column('date', sa.String(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('history', 'date') + # ### end Alembic commands ### diff --git a/project/migrations/versions/__init__.py b/project/migrations/versions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/models/__init__.py b/project/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/models/database.py b/project/models/database.py new file mode 100644 index 0000000000..6cfa997011 --- /dev/null +++ b/project/models/database.py @@ -0,0 +1,38 @@ +from flask_sqlalchemy import SQLAlchemy + +# Create the instances of the Flask extensions (flask-sqlalchemy, flask-login, etc.) in +# the global scope, but without any arguments passed in. These instances are not attached +# to the application at this point. +db = SQLAlchemy() + + +# define your models classes hereafter + +class BaseModel(db.Model): + # base data model for all objects + __abstract__ = True + # define here __repr__ and json methods or any common method + # that you need for all your models + + +class History(BaseModel): + __tablename__ = 'history' + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(160), nullable=False) + fullname = db.Column(db.String(250), nullable=False) + language = db.Column(db.String(160)) + url = db.Column(db.String(), nullable=False) + description = db.Column(db.String()) + date = db.Column(db.String(), nullable=False) + + def __init__(self, email, fullname, language, url, description, date): + self.email = email + self.fullname = fullname + self.language = language + self.url = url + self.description = description + self.date = date + + def __repr__(self): + return f"History({self.email}{self.fullname}{self.language}{self.url}{self.description}{self.date})" diff --git a/project/models/repository.py b/project/models/repository.py new file mode 100644 index 0000000000..373946fcc4 --- /dev/null +++ b/project/models/repository.py @@ -0,0 +1,7 @@ +class Repo(object): + def __init__(self, data) -> None: + self.language = data.language + self.full_name = data.full_name + self.html_url = data.html_url + self.stargazers_count = data.stargazers_count + self.description = data.description diff --git a/project/templates/base.html b/project/templates/base.html new file mode 100644 index 0000000000..697a31659b --- /dev/null +++ b/project/templates/base.html @@ -0,0 +1,27 @@ + + + + {% block head %} + + + + + {% block styles %} + + {{ bootstrap.load_css() }} + + {% endblock %} + + GitHub Search + {% endblock %} + + + + {% block content %}{% endblock %} + + {% block scripts %} + + {{ bootstrap.load_js() }} + {% endblock %} + + \ No newline at end of file diff --git a/project/templates/renders/history.html b/project/templates/renders/history.html new file mode 100644 index 0000000000..1a07c34b15 --- /dev/null +++ b/project/templates/renders/history.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+
+

GitHub Search History

+
+
+ [x] close +
+
+
+
+
+
+ + + {% for result in results %} + + + + + + + + + + + {% endfor %} + +

#{{ loop.index }} - {{ result.email }}

{{result.date}}

{{result.url}}{{result.fullname}}{{result.description}}
+
+
+ +
+
+{% endblock %} diff --git a/project/templates/renders/search.html b/project/templates/renders/search.html new file mode 100644 index 0000000000..5b6d2e821f --- /dev/null +++ b/project/templates/renders/search.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+
+

GitHub Repositories Search

+
+ +
+
+
+
+
+ +
+
+ +
+
+ {% if qty %} +

About {{qty}} results found,
showing the two best ranked of each selected language

+
+ {% endif %} + + + {% for result in results | sort(attribute='stargazers_count', reverse = True) %} + + + + + + {% endfor %} + +
{{result.stargazers_count}}
{{result.html_url}}

{{result.language}} - {{result.full_name}}

{{result.description}}

+
+
+
+ +
+
+{% endblock %} diff --git a/project/views/__init__.py b/project/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/views/history.py b/project/views/history.py new file mode 100644 index 0000000000..f53acadef3 --- /dev/null +++ b/project/views/history.py @@ -0,0 +1,26 @@ +from flask import Blueprint, request, render_template +from project.models.database import History + +history_blueprints = Blueprint("history", __name__) + +view = { + "title": "History", + "name": "history", +} + + +@history_blueprints.route('/', methods=['GET']) +def index(): + if request.method == 'GET': + all_history = History.query.all() + results = [ + { + "email": history.email, + "language": history.language, + "fullname": history.fullname, + "url": history.url, + "description": history.description, + "date": history.date + } for history in all_history] + + return render_template('renders/history.html', view=view, results=results) diff --git a/project/views/search.py b/project/views/search.py new file mode 100644 index 0000000000..558f63eaa1 --- /dev/null +++ b/project/views/search.py @@ -0,0 +1,28 @@ +from flask import Blueprint, request, render_template +from project.controllers.Core import get_languages, search_github + +search_blueprints = Blueprint("search", __name__) + +view = { + "title": "Search", + "name": "search", +} + + +@search_blueprints.route('/') +def index(): + return render_template('renders/search.html', view=view, languages=get_languages()) + + +@search_blueprints.route('/search', methods=['POST']) +def search(): + if request.method == 'POST': + email = request.form['email'] + keywords = request.form['lang_one'] if request.form['lang_one'] else "" + keywords += ',' + request.form['lang_two'] if request.form['lang_two'] else "" + keywords += ',' + request.form['lang_three'] if request.form['lang_three'] else "" + keywords += ',' + request.form['lang_four'] if request.form['lang_four'] else "" + keywords += ',' + request.form['lang_five'] if request.form['lang_five'] else "" + + results, qty = search_github(email, keywords) + return render_template('renders/search.html', view=view, results=results, qty=qty, languages=get_languages()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed1aea91a7776b7de4032bb750645d2a089cb298 GIT binary patch literal 1582 zcmZ`(O>fgs5Zp5oKgAJt+ENZ2D5yvkMWBE<8`n)uo5VPAp!|4XX2<*NsI4r^*4y2g z*^jsX{+4!UwXN*CHCEYcJh{#6CEjCuZqKZ=oo!&J^_O+{?%=;oo=5O}kvw z(Rv1y4Pv)=JD|3f**UmNoZLEE;Yfj2fu)0QW7l}8s{oHsbNtQh)LARo!-L9m=pHd{ zfKNp=kTP3CL5EzJb7Oy;LCB0?tTL(GUJbfV9dBmyfo}sVaY{#GJY$*ZQS#V3c42U&U;nsIJX%Stco~&(cSMEXWtUx~ST!DjCUk_fPlSoqec=g0G+A_NY%F^|cjW~1X zd5DwQ0;<^K7RrK=*`2udDeA&L+Xs7tT2yue+?Cy@2zDU%2=dl=*6?Ewh^a0`JsRZC z97C6?%)B{q8+_T_aoE8HCP^^x3F+l*lqGit0q+pEwb-%z8q@A3XorYY@zwyb8X=vg`9*}NntyQ z%e{UP$vE$H zDLcR$TcFCU(_?;_eFbBK?8T~y;Wp46(_U0fpzr`KX%pJ|`I=ySGs3wt}KKIDgkX2M$=skA)7E^ero12$a`VTEe B-6sG5 literal 0 HcmV?d00001 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000..0c9f406313 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.1 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..f01c3e14a6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +from datetime import datetime +import pytest +from project import create_app +from project.models.database import db, History + + +@pytest.fixture(scope='module') +def test_search(): + flask_app = create_app('flask_test.cfg') + + # Create a test client using the Flask application configured for testing + with flask_app.test_client() as testing_client: + # Establish an application context + with flask_app.app_context(): + yield testing_client # this is where the testing happens! + + +@pytest.fixture(scope='module') +def init_database(test_client): + # Create the database and the database table + db.create_all() + + # Insert user data + history1 = History(email='tester01@gmail.com', fullname='FlaskIsAwesome', language='Python', + url='https://github.com/donnemartin', description='Learn how to design large-scale systems. ' + 'Prep for the system design interview.', + date=datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + + history2 = History(email='tester02@gmail.com', fullname='Postgres SQL', language='SQL', + url='https://github.com/sqlpostgres', description='Automatic SQL injection and database ' + 'takeover tool ', + date=datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + + db.session.add(history1) + db.session.add(history2) + + # Commit the changes for the users + db.session.commit() + + yield # this is where the testing happens! + + db.drop_all() diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py new file mode 100644 index 0000000000..8a60d2d512 --- /dev/null +++ b/tests/functional/test_functional.py @@ -0,0 +1,59 @@ +""" +This file contains the functional tests for the pages blueprint. + +These tests use GETs and POSTs to different URLs to check for the proper behavior +of the pages blueprint. +""" +from project import create_app + + +def test_search_page(): + """ + GIVEN a Flask application configured for testing + WHEN the '/' page is requested (GET) + THEN check that the response is valid + """ + flask_app = create_app('flask_test.cfg') + + # Create a test search using the Flask application configured for testing + with flask_app.test_client() as test_client: + response = test_client.get('/') + assert response.status_code == 200 + assert b'email' in response.data + assert b'lang_one' in response.data + assert b'lang_two' in response.data + assert b'lang_three' in response.data + assert b'lang_four' in response.data + assert b'lang_five' in response.data + assert b'submit' in response.data + + +def test_search_post(): + """ + GIVEN a Flask application configured for testing + WHEN the '/search' page is posted to (POST) + THEN check that the response is valid + """ + flask_app = create_app('flask_test.cfg') + + # Create a test search using the Flask application configured for testing + with flask_app.test_client() as test_client: + response = test_client.post('/search', data=dict(email='nrodriguez02@gmail.com', lang_one='Python', + lang_two='PHP', lang_three='C', lang_four='C#', + lang_five='SQL')) + assert response.status_code == 200 + assert b"showing the two best ranked" in response.data + + +def test_history_page(): + """ + GIVEN a Flask application configured for testing + WHEN the '/' page is requested (GET) + THEN check that the response is valid + """ + flask_app = create_app('flask_test.cfg') + + # Create a test history using the Flask application configured for testing + with flask_app.test_client() as test_client: + response = test_client.get('/history/') + assert response.status_code == 200 diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 0000000000..3cd38aade5 --- /dev/null +++ b/tests/unit/test_models.py @@ -0,0 +1,51 @@ +""" +This file (test_models.py) contains the unit tests for the models.py file. +""" +import json +from datetime import datetime +from types import SimpleNamespace + +import requests + +from project.models.repository import Repo +from project.models.database import History + + +def test_repository(): + """ + GIVEN a Repo model + WHEN a new Repo is created + THEN check the fields are defined correctly + """ + sample = dict(language='Python', full_name='FlaskIsAwesome', html_url='https://github.com/donnemartin', + stargazers_count='123450', description='Learn how to design large-scale systems. ' + 'Prep for the system design interview.') + + data = json.dumps(sample, indent=5) + data = json.loads(data, object_hook=lambda d: SimpleNamespace(**d)) + repo = Repo(data) + assert repo.language == 'Python' + assert repo.full_name == 'FlaskIsAwesome' + assert repo.html_url == 'https://github.com/donnemartin' + assert repo.stargazers_count == '123450' + assert repo.description == 'Learn how to design large-scale systems. Prep for the system design interview.' + + +def test_history(): + """ + GIVEN a History model + WHEN a new History is created + THEN check the fields are defined correctly + """ + + history = History(email='tester01@gmail.com', fullname='FlaskIsAwesome', language='Python', + url='https://github.com/donnemartin', description='Learn how to design large-scale systems. ' + 'Prep for the system design interview.', + date=datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + + assert history.email == 'tester01@gmail.com' + assert history.fullname == 'FlaskIsAwesome' + assert history.language == 'Python' + assert history.url == 'https://github.com/donnemartin' + assert history.description == 'Learn how to design large-scale systems. Prep for the system design interview.' + assert history.date == datetime.now().strftime("%d/%m/%Y %H:%M:%S") diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 0000000000..55197164e3 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,7 @@ +[uwsgi] +#http-socket = :$(PORT) # on Heroku +master = true +die-on-term = true +module = app:app +http = 0.0.0.0:8001 # on Docker +memory-report = true \ No newline at end of file From e644c3059f829a70ed9d934dbfba4fd13f1c92ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nestor=20Rodr=C3=ADguez?= Date: Sun, 16 Jan 2022 23:36:06 -0300 Subject: [PATCH 2/3] adding .idea to gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3dc631e7b4..9c1e2176f6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ .env .coverage .pytest_cache +.idea From 05c06b98d070dc83f1450510c46f8b68fc0c9479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nestor=20Rodr=C3=ADguez?= Date: Mon, 17 Jan 2022 15:09:18 -0300 Subject: [PATCH 3/3] changing readme.md file adding PyTest commands to run the test --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6ece5fbde4..af79112869 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Visualizar os detalhes de cada repositório. - Frontend Frameworks: Flask, Bootstrap - Docker - Heroku -- Unit Test +- PyTest # Deploy on Heroku - https://ateliware-project-challange.herokuapp.com/ @@ -41,6 +41,7 @@ Visualizar os detalhes de cada repositório. - History: - Test page response - - - + +- Run test: + - python -m pytest -v + - python -m pytest --setup-show --cov=project