From 463eb78380ef8d97f45ef370c9b907641a6f7e75 Mon Sep 17 00:00:00 2001 From: Salman Ashraf Date: Tue, 23 Jul 2024 15:29:04 +0000 Subject: [PATCH] Log into github and create new contributor --- .gitignore | 3 ++ api/migrations/0001_initial.py | 85 ++++++++++------------------------ api/models/contributor.py | 2 +- api/views/contributor.py | 75 +++++++++++++++++++++++++++++- api/views/contributor_test.py | 49 ++++++++++++++++++++ api/views/login.txt | 1 + settings.py | 9 +++- 7 files changed, 161 insertions(+), 63 deletions(-) create mode 100644 api/views/login.txt diff --git a/.gitignore b/.gitignore index 15a614b..cc05f54 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,6 @@ cython_debug/ # Django static/ + +# 0Auth app +.env diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 465aa48..50f8ac7 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.25 on 2024-07-22 14:17 +# Generated by Django 3.2.25 on 2024-07-23 13:03 import django.core.validators from django.db import migrations, models @@ -6,79 +6,44 @@ class Migration(migrations.Migration): + initial = True - dependencies = [] + dependencies = [ + ] operations = [ migrations.CreateModel( - name="Contributor", + name='Contributor', fields=[ - ( - "id", - models.IntegerField( - help_text="The contributor's GitHub user-ID.", - primary_key=True, - serialize=False, - ), - ), - ( - "email", - models.EmailField(max_length=254, verbose_name="email"), - ), - ("name", models.TextField(verbose_name="name")), - ("location", models.TextField(verbose_name="location")), - ("html_url", models.TextField(verbose_name="html url")), - ("avatar_url", models.TextField(verbose_name="avatar url")), + ('id', models.IntegerField(help_text="The contributor's GitHub user-ID.", primary_key=True, serialize=False)), + ('email', models.EmailField(max_length=254, verbose_name='email')), + ('name', models.TextField(verbose_name='name')), + ('location', models.TextField(null=True, verbose_name='location')), + ('html_url', models.TextField(verbose_name='html url')), + ('avatar_url', models.TextField(verbose_name='avatar url')), ], options={ - "verbose_name": "contributor", - "verbose_name_plural": "contributors", + 'verbose_name': 'contributor', + 'verbose_name_plural': 'contributors', }, ), migrations.CreateModel( - name="Repository", + name='Repository', fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "gh_id", - models.IntegerField( - help_text="Github ID of the repo a contributor has contributed to.", - verbose_name="GitHub ID", - ), - ), - ( - "points", - models.IntegerField( - default=0, - help_text="Story points the contributor closed for this repository.", - verbose_name="points", - ), - ), - ( - "contributor", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="api.contributor", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('gh_id', models.IntegerField(help_text='Github ID of the repo a contributor has contributed to.', verbose_name='GitHub ID')), + ('points', models.IntegerField(default=0, help_text='Story points the contributor closed for this repository.', verbose_name='points')), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.contributor')), ], options={ - "verbose_name": "repository", - "verbose_name_plural": "repositories", - "unique_together": {("contributor", "gh_id")}, + 'verbose_name': 'repository', + 'verbose_name_plural': 'repositories', + 'unique_together': {('contributor', 'gh_id')}, }, ), migrations.CreateModel( - name="AgreementSignature", + name='AgreementSignature', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('agreement_id', models.CharField(help_text='Commit ID of the contribution agreement in workspace.', max_length=40, validators=[django.core.validators.MinLengthValidator(40)], verbose_name='agreement id')), @@ -86,9 +51,9 @@ class Migration(migrations.Migration): ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.contributor')), ], options={ - "verbose_name": "agreement signature", - "verbose_name_plural": "agreement signatures", - "unique_together": {("contributor", "agreement_id")}, + 'verbose_name': 'agreement signature', + 'verbose_name_plural': 'agreement signatures', + 'unique_together': {('contributor', 'agreement_id')}, }, ), ] diff --git a/api/models/contributor.py b/api/models/contributor.py index 7cb1797..c03e385 100644 --- a/api/models/contributor.py +++ b/api/models/contributor.py @@ -23,7 +23,7 @@ class Contributor(models.Model): ) email = models.EmailField(_("email")) name = models.TextField(_("name")) - location = models.TextField(_("location")) + location = models.TextField(_("location"), null=True) html_url = models.TextField(_("html url")) avatar_url = models.TextField(_("avatar url")) diff --git a/api/views/contributor.py b/api/views/contributor.py index 2af17d3..fdcf4d4 100644 --- a/api/views/contributor.py +++ b/api/views/contributor.py @@ -3,9 +3,15 @@ Created on 16/07/2024 at 11:03:09(+01:00). """ +import requests from codeforlife.permissions import AllowAny +from codeforlife.request import Request +from codeforlife.response import Response from codeforlife.user.models import User -from codeforlife.views import ModelViewSet +from codeforlife.views import ModelViewSet, action +from rest_framework import status + +import settings from ..models import Contributor from ..serializers import ContributorSerializer @@ -17,3 +23,70 @@ class ContributorViewSet(ModelViewSet[User, Contributor]): permission_classes = [AllowAny] serializer_class = ContributorSerializer queryset = Contributor.objects.all() + + @action(detail=False, methods=["get"]) + def log_into_github(self, request: Request): + """Users can login using their existing github account""" + # Get user access Token + access_token_request = requests.post( + url="https://github.com/login/oauth/access_token", + headers={"Accept": "application/json"}, + params={ + "client_id": settings.GITHUB_CLIENT_ID, + "client_secret": settings.GITHUB_CLIENT_SECRET, + "code": request.GET.get("code"), + }, + timeout=5, + ) + if not access_token_request.ok: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + auth_data = access_token_request.json() + + # Code expired + if "access_token" not in auth_data: + return Response( + status=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS + ) + + access_token = auth_data["access_token"] + token_type = auth_data["token_type"] + + # Get user's information + user_data_request = requests.get( + url="https://api.github.com/user", + headers={ + "Accept": "application/json", + "Authorization": f"{token_type} {access_token}", + }, + timeout=5, + ) + + user_data = user_data_request.json() + if not user_data["email"]: + return Response( + data="Email null", + status=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS, + ) + + # Check if user is already a contributor + gh_id = user_data["id"] + if Contributor.objects.filter(pk=gh_id): + return Response(status=status.HTTP_409_CONFLICT) + + # Create a new contributor + data = { + "id": gh_id, + "email": user_data["email"], + "name": user_data["name"], + "location": user_data["location"], + "html_url": user_data["html_url"], + "avatar_url": user_data["avatar_url"], + } + + serializer = ContributorSerializer(data=data) + if serializer.is_valid(): + serializer.save() + return Response(status=status.HTTP_200_OK) + return Response( + status=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS, + ) diff --git a/api/views/contributor_test.py b/api/views/contributor_test.py index 5e710f7..1d9bca3 100644 --- a/api/views/contributor_test.py +++ b/api/views/contributor_test.py @@ -3,8 +3,15 @@ Created on 16/07/2024 at 14:54:09(+01:00). """ +from unittest.mock import patch + +import requests +from codeforlife.request import Request from codeforlife.tests import ModelViewSetTestCase from codeforlife.user.models import User +from rest_framework import status + +import settings from ..models import Contributor from .contributor import ContributorViewSet @@ -41,3 +48,45 @@ def test_create(self): "avatar_url": "https://contributortest.github.io/", } ) + + # def test_log_into_github__no_code(self): + # """Login API call does not return a code.""" + # code = "3e074f3e12656707cf7f" + # request = Request + # # request.GET= {"code": code} + + # with patch.object(Request, "GET", return_value=request): + # self.client.get( + # self.reverse_action( + # "log_into_github", + # ), + # status_code_assertion=status.HTTP_500_INTERNAL_SERVER_ERROR, + # ) + + # def test_log_into_github__no_access_token(self): + # """POST API call did not return an access token""" + # code = "3e074f3e12656707cf7f" + # response = requests.Response() + # response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + # request = Request + # request.GET = {"code": code} + # with patch.object( + # requests, "post", return_value=response + # ) as requests_get: + # self.client.get( + # self.reverse_action( + # "log_into_github", + # ), + # status_code_assertion=status.HTTP_500_INTERNAL_SERVER_ERROR, + # ) + + # requests_get.assert_called_once_with( + # url="https://github.com/login/oauth/access_token", + # headers={"Accept": "application/json"}, + # params={ + # "client_id": settings.GITHUB_CLIENT_ID, + # "client_secret": settings.GITHUB_CLIENT_SECRET, + # "code": code, + # }, + # timeout=5, + # ) diff --git a/api/views/login.txt b/api/views/login.txt new file mode 100644 index 0000000..73fd118 --- /dev/null +++ b/api/views/login.txt @@ -0,0 +1 @@ +https://github.com/login/oauth/authorize?client_id=Ov23liBErSabQFqROeMg \ No newline at end of file diff --git a/settings.py b/settings.py index 834d91c..118ae9d 100644 --- a/settings.py +++ b/settings.py @@ -13,16 +13,23 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ +import os from pathlib import Path # pylint: disable-next=wildcard-import,unused-wildcard-import from codeforlife.settings import * +from dotenv import load_dotenv # Github GH_ORG = "ocadotechnology" -GH_REPO = "codeforlife-workspace" +GH_REPO = "codeforlife-workspace" GH_FILE = "CONTRIBUTING.md" +# 0Auth +load_dotenv() +GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID") +GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET") + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent