Skip to content
This repository was archived by the owner on Nov 7, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 443 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn backend.wsgi
93 changes: 78 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,85 @@
# Desafio técnico para desenvolvedores

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:
## Tecnologias no backend
* [Python3](https://www.python.org/)
* [Django](https://www.djangoproject.com/)
* [PostgreSQL](https://www.postgresql.org/)

- 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.
## Tecnologias do frontend
* [Next com typescript](https://nextjs.org/)
* [tailwindcss](https://tailwindcss.com/)

Alguns requisitos:
## Exemplo

- 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.
Segue um exemplo da [aplicação](https://whispering-savannah-48854.herokuapp.com/) rodando no heroku

Quando terminar, faça um Pull Request neste repo e avise-nos por email.
## Funcionalidades
* Autenticação JWT
* Refresh token automático
* Integração com API Github para trazer os repositórios
* Paginação mostrando no máximo 10 páginas de resultado
* Possibilidade de favoritar um repositório
* Ação só possível se estiver logado na aplicação
* Possibilidade de listar repositórios favoritados
* Ação só possível se estiver logado na aplicação
* Sistema com design guide retro (estilo windows classic)

**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.
## Lista de requisitos solicitados/Status de realizado

- Deve ser uma aplicação totalmente nova; [DONE]
- A solução deve estar em um repositório público do GitHub; [DONE]
- A aplicação deve armazenar as informações encontradas; [DONE]
- Utilizar PostgreSQL, MySQL ou SQL Server; [DONE]
- O deploy deve ser realizado, preferencialmente, no Heroku, AWS ou no Azure; [DONE]
- A aplicação precisa ter testes automatizados; [NO]
- Preferenciamente dockerizar a aplicação; [NO]
- Por favor atualizar o readme da aplicação com passo a passo com instrução para subir o ambiente. [DONE]

# Configurando a aplicação

## Frontend

- Build
1. Setar váriavel de ambiente NEXT_PUBLIC_BACKEND_API que aponta para o backend;
2. Fazer build da aplicação

npm run build
3. Iniciar servidor

npm start

- Dev
1. Setar váriavel de ambiente NEXT_PUBLIC_BACKEND_API que aponta para o backend;
2. Iniciar servidor

npm run dev

## Backend

- Build
1. Para deploy no heroku já existe um Procfile configurado no repositório. É necessário apenas criar o dyno e dar push da aplicação.

heroku create
git push heroku master
2. Setar váriaveis de ambiente (mesmo passos para dev)

- Dev
1. Criar venv

python3 -m venv
2. Adicione as váriaveis de ambiente ao final do arquivo "env/bin/activate". (No caso de heroku utilizar o comando heroku config:set para configurar as variáveis)

DATABASE_URL : Url de conexão com a base de dados
DEV_HIRING_CHALLENGE_ALLOWED_HOST : Urls separadas por ", " que serão permitidos ser feito requisições
DEV_HIRING_CHALLENGE_FRONTEND_HOST : Urls separadas por ", " de domínios front que devem liberar os CORS
DEV_HIRING_CHALLENGE_GITHUB_TOKEN : Token de credencial para consultar a api do github
DEV_HIRING_CHALLENGE_GITHUB_USERNAME : Nome de usuário usado como credencial para consultar a api do github

3. Configurar banco de dados

source env/bin/activate
python manage.py migrate

4. Subir aplicação

python manage.py runserver 0:8000
23 changes: 23 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# ROADMAP

## [ TODO ]

* {2}[BACK] configure OAuth;
* {3}[BACK] configure github api;
* {4}[BACK] create GET endpoint to retrieve github repos;
* {5}[BACK] create POST endpoint to bookmark github repo;
* {6}[BACK] create GET endpoint to retrieve bookmarked github repos;
* {7}[BACK] create DELETE endpoint to remove github repo as bookmarked;
* {8}[FRONT] configure a simple React app;
* {9}[FRONT] create login page;
* {10}[FRONT] create search page;
* {11}[FRONT] create bookmark page;

## [ DOING ]

* {1}[BACK] configure a simple Django rest api;

## [ DONE ]

--

Empty file added apps/authentication/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/authentication/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.apps import AppConfig

class AuthenticationConfig(AppConfig):
name = 'apps.authentication'
31 changes: 31 additions & 0 deletions apps/authentication/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from django.contrib.auth.password_validation import validate_password

class AuthenticationSerializer(serializers.ModelSerializer):
username = serializers.EmailField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all())]
)

password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)

class Meta:
model = User
fields = ('username', 'password', 'password2')

def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs

def create(self, validated_data):
del validated_data['password2']
user = User.objects.create(**validated_data)

user.set_password(validated_data['password'])
user.save()

return user
9 changes: 9 additions & 0 deletions apps/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path
from .views import AuthenticationView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('register/', AuthenticationView.as_view(), name='register_user'),
]
9 changes: 9 additions & 0 deletions apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib.auth.models import User
from rest_framework.generics import CreateAPIView
from rest_framework.permissions import AllowAny
from .serializers import AuthenticationSerializer

class AuthenticationView(CreateAPIView):
queryset = User.objects.all()
permission_classes = (AllowAny,)
serializer_class = AuthenticationSerializer
Empty file added apps/github/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/github/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.apps import AppConfig

class GithubConfig(AppConfig):
name = 'apps.github'
1 change: 1 addition & 0 deletions apps/github/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GITHUB_API = 'https://api.github.com/search/repositories'
13 changes: 13 additions & 0 deletions apps/github/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import requests
import os
from apps.github.consts import GITHUB_API

def mount_api_string(label, language, page):
if label == None or language == None:
return GITHUB_API
return GITHUB_API + '?q=label:' + label + '+language:' + language + '&page=' + page + '&per_page=5'

def request_github_api(label, language, page):
gitUsername = os.environ.get('DEV_HIRING_CHALLENGE_GITHUB_USERNAME', '')
gitToken = os.environ.get('DEV_HIRING_CHALLENGE_GITHUB_TOKEN', '')
return requests.get(mount_api_string(label, language, page), auth=(gitUsername, gitToken))
6 changes: 6 additions & 0 deletions apps/github/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.urls import path
from . import views

urlpatterns = [
path('', views.get_repositories)
]
23 changes: 23 additions & 0 deletions apps/github/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view
from apps.vocabulary.messages import github_not_working
from apps.github.helpers import request_github_api

"""
Fetch repositories from github with query and language param
It is mandatory to pass 2 params or it will search without params
@param language :: programming language to search
@param label :: query string to search
@param page :: page number of the results to fetch
"""
@api_view(['GET'])
def get_repositories(request):
try:
label = request.query_params.get('label')
language = request.query_params.get('language')
page = request.query_params.get('page')
return Response(request_github_api(label, language, page).json())
except:
response = Response({'detail': github_not_working})
response.status_code = 502
return response
Empty file added apps/home/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions apps/home/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path
from apps.home.views import home


urlpatterns = [
path('', home),
]
6 changes: 6 additions & 0 deletions apps/home/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from rest_framework.decorators import api_view
from django.shortcuts import render

@api_view(['GET'])
def home(request):
return render(request, 'index.html')
Empty file added apps/repositories/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/repositories/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.apps import AppConfig

class RepositoriesConfig(AppConfig):
name = 'apps.repositories'
27 changes: 27 additions & 0 deletions apps/repositories/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.0.5 on 2022-06-19 07:22

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Repositorie',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_name', models.CharField(max_length=100)),
('html_url', models.CharField(max_length=100)),
('description', models.CharField(max_length=255)),
('created_at', models.CharField(max_length=100)),
('users', models.ManyToManyField(related_name='repositories', to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
12 changes: 12 additions & 0 deletions apps/repositories/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.auth.models import User
from django.db import models

class Repositorie(models.Model):
full_name = models.CharField(max_length=100)
html_url = models.CharField(max_length=100)
description = models.CharField(max_length=255)
created_at = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name="repositories")

def __str__(self):
return self.html_url
13 changes: 13 additions & 0 deletions apps/repositories/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import Repositorie

class RepositorieSerializer(serializers.ModelSerializer):
class Meta:
model = Repositorie
fields = ('id', 'full_name', 'html_url', 'description', 'created_at', 'users')

class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'repositories')
6 changes: 6 additions & 0 deletions apps/repositories/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.urls import path
from . import views

urlpatterns = [
path('', views.repositories, name='get_post_repositories')
]
43 changes: 43 additions & 0 deletions apps/repositories/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from apps.repositories.models import Repositorie
from apps.vocabulary.messages import could_not_perform_repo_creation
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import RepositorieSerializer

@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def repositories(request):
try:
if request.method == 'POST':
return post_handler(request)

if request.method == 'GET':
return get_handler(request)
except Exception as ex:
template = "An exception of type {0} occurred. Arguments:\n{1!r}"
message = template.format(type(ex).__name__, ex.args)
print(message)
return error_handler()

def post_handler(request):
defaults = request.data.copy()
del defaults['id']

repositorie, _ = Repositorie.objects.update_or_create(
id=request.data['id'],
defaults=defaults
)

repositorie.users.add(request.user.id)
return Response(RepositorieSerializer(repositorie).data)

def get_handler(request):
repositories = request.user.repositories
serializer = RepositorieSerializer(repositories, many=True)
return Response(serializer.data)

def error_handler():
response = Response({'detail': could_not_perform_repo_creation})
response.status_code = 502
return response
Empty file added apps/vocabulary/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions apps/vocabulary/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.apps import AppConfig

class VocabularyConfig(AppConfig):
name = 'apps.vocabulary'
4 changes: 4 additions & 0 deletions apps/vocabulary/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

github_not_working = "github is not responding or not configured"

could_not_perform_repo_creation = "couldn't perform repositorie creation"
1 change: 1 addition & 0 deletions backend/.dev_hiring_challenge_pgpass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
localhost:5432:dev_hirigin_challenge:postgres:1
Empty file added backend/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions backend/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for backend project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')

application = get_asgi_application()
Loading