diff --git a/estimation/app/migrations/0002_githubuser_access_token_githubuser_avatar_url.py b/estimation/app/migrations/0002_githubuser_access_token_githubuser_avatar_url.py new file mode 100644 index 0000000..b33b6b8 --- /dev/null +++ b/estimation/app/migrations/0002_githubuser_access_token_githubuser_avatar_url.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.1 on 2024-09-18 06:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="githubuser", + name="access_token", + field=models.CharField( + blank=True, + db_comment="GitHub access token for OAuth authentication.", + max_length=255, + null=True, + ), + ), + migrations.AddField( + model_name="githubuser", + name="avatar_url", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/estimation/app/models.py b/estimation/app/models.py index c84ed3d..0776212 100644 --- a/estimation/app/models.py +++ b/estimation/app/models.py @@ -6,6 +6,20 @@ class GithubUser(models.Model): max_length=1000, db_comment="The github handle for the user, without the '@github' part.", ) + access_token = models.CharField( + max_length=255, + blank=True, + null=True, + db_comment="GitHub access token for OAuth authentication." + ) + + avatar_url = models.URLField(blank=True, null=True) + + +def __str__(self): + return self.handle + + class GithubIssue(models.Model): diff --git a/estimation/app/templates/base.html b/estimation/app/templates/base.html index a1f43d4..3b41e40 100644 --- a/estimation/app/templates/base.html +++ b/estimation/app/templates/base.html @@ -1,7 +1,7 @@ - {% load static %} - +{% load static %} + @@ -13,34 +13,31 @@ {% block title %} {{ title }} {% endblock title %} - - - -
-
- -

GitHub Async Estimation Tool

- {% if user.handle %} - Logout - - + + + +
+
+ {% if user.is_authenticated %} + Logout + + + {% else %} + Login with GitHub {% endif %} -
-
-
- -
+
+
+ +
{% block content %} {% endblock content %} -
-
-
-
DDS Hackathon 2024
-
-
- +
+ diff --git a/estimation/app/templates/dashboard.html b/estimation/app/templates/dashboard.html index e69de29..bf13d25 100644 --- a/estimation/app/templates/dashboard.html +++ b/estimation/app/templates/dashboard.html @@ -0,0 +1,5 @@ + +
+ Profile Picture + +
\ No newline at end of file diff --git a/estimation/app/urls.py b/estimation/app/urls.py index 3122f79..4c61ce9 100644 --- a/estimation/app/urls.py +++ b/estimation/app/urls.py @@ -1,4 +1,8 @@ from django.urls import path from . import views -urlpatterns = [path("", views.index, name="index")] +urlpatterns = [path("", views.index, name="index"), + path('github/login/', views.github_login, name='github_login'), + path('github/callback/', views.github_callback, name='github_callback'), + path('dashboard/', views.dashboard, name='dashboard'), + ] diff --git a/estimation/app/views.py b/estimation/app/views.py index 21af73c..e94b127 100644 --- a/estimation/app/views.py +++ b/estimation/app/views.py @@ -1,8 +1,165 @@ -from django.http import HttpRequest -from django.shortcuts import render - -# Create your views here. +import requests +from django.http import HttpRequest, JsonResponse +from django.shortcuts import render, redirect +from app.models import GithubUser +from estimation import settings +import re def index(request: HttpRequest): return render(request, "index.html", {}) + + +def dashboard(request): + avatar_url = request.session.get('avatar_url') + github_handle = request.session.get('github_handle') + + # Clear session variables if necessary + request.session.pop('avatar_url', None) + request.session.pop('github_handle', None) + + return render(request, 'dashboard.html', { + 'avatar_url': avatar_url, + 'github_handle': github_handle + }) + + +def github_login(request): + # GitHub OAuth authorization URL + github_auth_url = ( + 'https://github.com/login/oauth/authorize?' + f'client_id={settings.GITHUB_CLIENT_ID}&' + f'redirect_uri={settings.GITHUB_REDIRECT_URI}&' + 'scope=repo' + ) + return redirect(github_auth_url) + + +def github_callback(request): + code = request.GET.get('code') + if not code: + return redirect('index') # Redirect to the main page if no code is present + + # Exchange the authorization code for an access token + token_url = 'https://github.com/login/oauth/access_token' + payload = { + 'client_id': settings.GITHUB_CLIENT_ID, + 'client_secret': settings.GITHUB_CLIENT_SECRET, + 'code': code, + 'redirect_uri': settings.GITHUB_REDIRECT_URI, + } + headers = {'Accept': 'application/json'} + + response = requests.post(token_url, data=payload, headers=headers) + response_data = response.json() + + access_token = response_data.get('access_token') + if not access_token: + return redirect('index') # Handle the case where token is not retrieved + + # Get user information from GitHub + user_info_url = 'https://api.github.com/user' + headers = {'Authorization': f'token {access_token}'} + user_info_response = requests.get(user_info_url, headers=headers) + user_info = user_info_response.json() + + github_handle = user_info.get('login') + avatar_url = user_info.get('avatar_url') + + if github_handle: + # Check if the user already exists in the database + github_user, created = GithubUser.objects.get_or_create( + handle=github_handle, + defaults={'access_token': access_token} + ) + + if not created: + # If the user already exists, update the access token + github_user.access_token = access_token + github_user.avatar_url = avatar_url + github_user.save() + + print(user_info) # Optional: You can log user info for debugging + + # Example values for demonstration purposes + project_url = 'https://github.com/users/ayeshmcg/projects/1' + issue_url = 'https://github.com/ayeshmcg/testRepo/issues/1' + story_points = 5 + + result = update_story_points_for_issue_card(project_url, issue_url, story_points, access_token) + print(result) # Log the result for debugging + request.session['avatar_url'] = avatar_url + request.session['github_handle'] = github_handle + + return redirect('dashboard') + + +def update_story_points_for_issue_card(project_url, issue_url, story_points, access_token): + # Extract project ID from the URL + # project_match = re.match(r'https://github.com/orgs/[^/]+/projects/(\d+)', project_url) + # if not project_match: + # return {'error': 'Invalid project URL format'} + # + # project_id = project_match.group(1) + + # Extract issue number from the URL + # issue_match = re.match(r'https://github.com/[^/]+/[^/]+/issues/(\d+)', issue_url) + # if not issue_match: + # return {'error': 'Invalid issue URL format'} + # + # issue_number = issue_match.group(1) + + # Retrieve the project columns + columns_url = f'https://api.github.com/projects/1/columns' + headers = { + 'Authorization': f'token {access_token}', + 'Accept': 'application/vnd.github+json' + } + columns_response = requests.get(columns_url, headers=headers) + if columns_response.status_code != 200: + return {'error': f'Unable to retrieve columns: {columns_response.status_code}, {columns_response.text}'} + + columns = columns_response.json() + + # Find the card in a column + card_id = None + for column in columns: + cards_url = f'https://api.github.com/projects/columns/{column["id"]}/cards' + cards_response = requests.get(cards_url, headers=headers) + if cards_response.status_code != 200: + return {'error': f'Unable to retrieve cards: {cards_response.status_code}, {cards_response.text}'} + + cards = cards_response.json() + for card in cards: + if card.get('content_url') == issue_url: + card_id = card['id'] + break + if card_id: + break + + if not card_id: + return {'error': 'No card found for the specified issue URL'} + + # Update the card with Story Points + card_url = f'https://api.github.com/projects/columns/cards/{card_id}' + card_response = requests.get(card_url, headers=headers) + if card_response.status_code != 200: + return {'error': f'Unable to retrieve card details: {card_response.status_code}, {card_response.text}'} + + card_data = card_response.json() + + print('card_data', card_data) + print('story_points', story_points) + + # If the card is a note card, append the story points + # if 'note' in card_data: + # updated_note = f"{card_data['note']}\n\nStory Points: {story_points}" + # update_data = {'note': updated_note} + # else: + # return {'error': 'This card is not a note card and cannot be updated'} + # + # update_response = requests.patch(card_url, json=update_data, headers=headers) + # if update_response.status_code == 200: + # return {'success': 'Story Points added successfully'} + # else: + # return {'error': f'Failed to add Story Points: {update_response.status_code}'} diff --git a/estimation/estimation/settings.py b/estimation/estimation/settings.py index 27b5afc..42767bf 100644 --- a/estimation/estimation/settings.py +++ b/estimation/estimation/settings.py @@ -12,10 +12,13 @@ import os from pathlib import Path +from dotenv import load_dotenv # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv() + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -120,6 +123,13 @@ USE_TZ = True +#github details + +GITHUB_CLIENT_ID = os.environ.get('GITHUB_CLIENT_ID', '') +GITHUB_CLIENT_SECRET = os.environ.get('GITHUB_CLIENT_SECRET', '') +GITHUB_REDIRECT_URI = os.environ.get('GITHUB_REDIRECT_URI', 'http://localhost:8000/app/github/callback/') + + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ diff --git a/estimation/estimation/urls.py b/estimation/estimation/urls.py index d08aaf4..321e2a3 100644 --- a/estimation/estimation/urls.py +++ b/estimation/estimation/urls.py @@ -18,7 +18,10 @@ from django.contrib import admin from django.urls import include, path +from app import views + urlpatterns = [ path("app/", include("app.urls")), - path("admin/", admin.site.urls), + path("admin/", views.index), + # path("admin/", admin.site.urls), ] diff --git a/requirements.txt b/requirements.txt index ec53851..3aaf3b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ requests==2.32.3 sqlparse==0.5.1 typing_extensions==4.12.2 urllib3==2.2.3 +python-dotenv~=1.0.1 \ No newline at end of file