diff --git a/Procfile b/Procfile index 31970cd..083cf53 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ +release: python manage.py migrate web: gunicorn cdoc.wsgi \ No newline at end of file diff --git a/app/migrations/0002_auto_20220329_1319.py b/app/migrations/0002_auto_20220329_1319.py index 6289c5e..6b50d7b 100644 --- a/app/migrations/0002_auto_20220329_1319.py +++ b/app/migrations/0002_auto_20220329_1319.py @@ -6,13 +6,13 @@ def update_permissions(schema, group): - call_command('update_permissions') + call_command("update_permissions") def apply_migration(apps, schema_editor): # Add, change, delete, view - Group = apps.get_model('auth', 'Group') - Permission = apps.get_model('auth', 'Permission') + Group = apps.get_model("auth", "Group") + Permission = apps.get_model("auth", "Permission") required_permissions = [ "view_githubappinstallation", "change_githubappinstallation", @@ -39,12 +39,13 @@ def revert_migration(*args, **kwargs): class Migration(migrations.Migration): dependencies = [ - ('app', '0001_initial'), - ('auth', "0012_alter_user_first_name_max_length"), + ("app", "0001_initial"), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ - migrations.RunPython(update_permissions, - reverse_code=migrations.RunPython.noop), - migrations.RunPython(apply_migration, revert_migration) + migrations.RunPython( + update_permissions, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython(apply_migration, revert_migration), ] diff --git a/app/migrations/0012_alter_githubpullrequest_pr_closed_at_and_more.py b/app/migrations/0012_alter_githubpullrequest_pr_closed_at_and_more.py new file mode 100644 index 0000000..d7d58c1 --- /dev/null +++ b/app/migrations/0012_alter_githubpullrequest_pr_closed_at_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.3 on 2022-04-10 11:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0011_githubpullrequest_pr_owner_username'), + ] + + operations = [ + migrations.AlterField( + model_name='githubpullrequest', + name='pr_closed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='githubpullrequest', + name='pr_head_modified_on', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='githubpullrequest', + name='pr_merged_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/app/migrations/0013_alter_monitoredpullrequest_pull_request_status_and_more.py b/app/migrations/0013_alter_monitoredpullrequest_pull_request_status_and_more.py new file mode 100644 index 0000000..192244c --- /dev/null +++ b/app/migrations/0013_alter_monitoredpullrequest_pull_request_status_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 4.0.3 on 2022-04-12 06:44 + +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_admin_users(apps, schema_editor): + # Add, change, delete, view + GithubAppUser = apps.get_model("app", "GithubAppUser") + GithubAppInstallation = apps.get_model("app", "GithubAppInstallation") + for gai_instance in GithubAppInstallation.objects.all(): + GithubAppUser.objects.update_or_create( + github_user=gai_instance.creator, installation=gai_instance + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0012_alter_githubpullrequest_pr_closed_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="monitoredpullrequest", + name="pull_request_status", + field=models.CharField( + choices=[ + ("NOT_CONNECTED", "Not Connected"), + ("APPROVAL_PENDING", "Approval Pending"), + ("STALE_CODE", "Stale Code"), + ("STALE_APPROVAL", "Stale Approval"), + ("APPROVED", "Approved"), + ("MANUALLY_APPROVED", "Manual Approval"), + ], + default="NOT_CONNECTED", + max_length=20, + ), + ), + migrations.CreateModel( + name="GithubAppUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "github_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="app.githubuser" + ), + ), + ( + "installation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="app.githubappinstallation", + ), + ), + ], + options={ + "unique_together": {("installation", "github_user")}, + }, + ), + migrations.RunPython(migrate_admin_users, migrations.RunPython.noop), + ] diff --git a/app/migrations/0014_monitoredpullrequest_integration_and_more.py b/app/migrations/0014_monitoredpullrequest_integration_and_more.py new file mode 100644 index 0000000..20bba15 --- /dev/null +++ b/app/migrations/0014_monitoredpullrequest_integration_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.0.3 on 2022-04-12 07:15 + +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_pr_integrations(apps, schema_editor): + # Add, change, delete, view + # Group = apps.get_model("auth", "Group") + MonitoredPullRequest = apps.get_model("app", "MonitoredPullRequest") + # GithubAppInstallation = apps.get_model("app", "GithubAppInstallation") + GithubRepoMap = apps.get_model("app", "GithubRepoMap") + for monitored_pr_instance in MonitoredPullRequest.objects.all(): + code_pull_request = monitored_pr_instance.code_pull_request + # documentation_pull_request = monitored_pr_instance.documentation_pull_request + grm = GithubRepoMap.objects.filter( + code_repo=code_pull_request.repository, + # documentation_repo=documentation_pull_request.repository, + ).last() + if grm is None: + assert False, "Exception: grm is None" + monitored_pr_instance.integration = grm.integration + monitored_pr_instance.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0013_alter_monitoredpullrequest_pull_request_status_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="monitoredpullrequest", + name="integration", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="app.githubappinstallation", + ), + preserve_default=False, + ), + migrations.AlterUniqueTogether( + name="githubrepomap", + unique_together={("integration", "code_repo", "documentation_repo")}, + ), + migrations.AlterUniqueTogether( + name="monitoredpullrequest", + unique_together={("code_pull_request", "documentation_pull_request")}, + ), + migrations.RunPython(migrate_pr_integrations, migrations.RunPython.noop), + migrations.AlterField( + model_name="monitoredpullrequest", + name="integration", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="app.githubappinstallation", + ), + ), + ] diff --git a/app/migrations/0015_githubloginstate_githubappuser_created_at_and_more.py b/app/migrations/0015_githubloginstate_githubappuser_created_at_and_more.py new file mode 100644 index 0000000..c772042 --- /dev/null +++ b/app/migrations/0015_githubloginstate_githubappuser_created_at_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.3 on 2022-04-12 08:37 + +import datetime +from django.db import migrations, models +import django.utils.timezone +from django.utils.timezone import utc +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0014_monitoredpullrequest_integration_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='GithubLoginState', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('state', models.UUIDField(default=uuid.uuid4)), + ('redirect_url', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.AddField( + model_name='githubappuser', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='githubrepomap', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='githubrepository', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/app/models.py b/app/models.py index b9d8c04..f19f943 100644 --- a/app/models.py +++ b/app/models.py @@ -161,6 +161,19 @@ def update_token(self): )[1] +class GithubAppUser(models.Model): + installation = models.ForeignKey(GithubAppInstallation, on_delete=models.CASCADE) + github_user = models.ForeignKey(GithubUser, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("installation", "github_user") + + def __str__(self) -> str: + return f"{self.github_user.account_name}[{self.installation.installation_id}]" + return super().__str__() + + class GithubInstallationToken(Token): github_app_installation = models.ForeignKey( GithubAppInstallation, on_delete=models.CASCADE, related_name="tokens" @@ -172,6 +185,7 @@ class GithubRepository(models.Model): repo_name = models.CharField(max_length=150) repo_full_name = models.CharField(max_length=200, unique=True) owner = models.ForeignKey(GithubAppInstallation, on_delete=models.PROTECT) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self) -> str: return self.repo_full_name @@ -195,6 +209,10 @@ class IntegrationType(models.TextChoices): GithubRepository, on_delete=models.PROTECT, related_name="documentation_repos" ) integration_type = models.CharField(max_length=20, choices=IntegrationType.choices) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("integration", "code_repo", "documentation_repo") class GithubPullRequest(models.Model): @@ -239,6 +257,7 @@ class PullRequestStatus(models.TextChoices): STALE_CODE = "STALE_CODE", "Stale Code" STALE_APPROVAL = "STALE_APPROVAL", "Stale Approval" APPROVED = "APPROVED", "Approved" + MANUAL_APPROVAL = "MANUALLY_APPROVED", "Manual Approval" code_pull_request = models.OneToOneField( GithubPullRequest, @@ -257,6 +276,10 @@ class PullRequestStatus(models.TextChoices): choices=PullRequestStatus.choices, default=PullRequestStatus.NOT_CONNECTED, ) + integration = models.ForeignKey(GithubAppInstallation, on_delete=models.CASCADE) + + class Meta: + unique_together = ("code_pull_request", "documentation_pull_request") def __str__(self) -> str: return f"{self.code_pull_request.repository.repo_full_name}[{self.code_pull_request.pr_number}]" @@ -270,7 +293,11 @@ def save(self, *args, **kwargs): self.pull_request_status = ( MonitoredPullRequest.PullRequestStatus.APPROVAL_PENDING ) - elif self.documentation_pull_request is None: + elif ( + self.documentation_pull_request is None + and self.pull_request_status + != MonitoredPullRequest.PullRequestStatus.MANUAL_APPROVAL + ): self.pull_request_status = ( MonitoredPullRequest.PullRequestStatus.NOT_CONNECTED ) @@ -278,7 +305,6 @@ def save(self, *args, **kwargs): def get_display_name(self): return f"{self.code_pull_request.repository.repo_full_name}/#{self.code_pull_request.pr_number}: {self.code_pull_request.pr_title}" - pass class GithubCheckRun(models.Model): @@ -326,3 +352,17 @@ def save(self, *args, **kwargs): ) self.run_id = check_run_instance.id super().save(*args, **kwargs) + + +class GithubLoginState(models.Model): + """ + This model is used to store the state of the login process. Along with it, + it also stores the redirect URL for the state. + """ + + state = models.UUIDField(default=uuid.uuid4) + redirect_url = models.URLField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return self.state.__str__() diff --git a/app/signals.py b/app/signals.py index df438af..3692905 100644 --- a/app/signals.py +++ b/app/signals.py @@ -117,7 +117,7 @@ def synchronize_github_check(sender, instance, **kwargs): "external_id": instance.unique_id.__str__(), "status": "completed", "conclusion": "action_required", - "details_url": f"{settings.WEBSITE_HOST}/{github_repo.full_name}/pull/{instance.ref_pull_request.code_pull_request.pr_number}", + "details_url": f"{settings.WEBSITE_HOST}/{github_repo.full_name}/pulls/", } if ( instance.ref_pull_request.pull_request_status @@ -134,10 +134,10 @@ def synchronize_github_check(sender, instance, **kwargs): }, } ) - elif ( - instance.ref_pull_request.pull_request_status - == app_models.MonitoredPullRequest.PullRequestStatus.APPROVED - ): + elif instance.ref_pull_request.pull_request_status in [ + app_models.MonitoredPullRequest.PullRequestStatus.APPROVED, + app_models.MonitoredPullRequest.PullRequestStatus.MANUAL_APPROVAL, + ]: data.update( { "conclusion": "success", diff --git a/app/templates/all_installations.html b/app/templates/all_installations.html new file mode 100644 index 0000000..cede3ad --- /dev/null +++ b/app/templates/all_installations.html @@ -0,0 +1,3 @@ +{% for installation in all_installations %} +{{installation.account_name}} +{% endfor %} \ No newline at end of file diff --git a/app/templates/all_repos.html b/app/templates/all_repos.html new file mode 100644 index 0000000..c350748 --- /dev/null +++ b/app/templates/all_repos.html @@ -0,0 +1,3 @@ +{% for repo in all_repos %} +{{repo.repo_full_name}} +{% endfor %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..6a16243 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,44 @@ + + + +{% load static %} + + + + {% block title %}Cdoc - Continuous Documentation{% endblock %} + + + + + + + + + + + +
+ + +
+ {% block content %} {% endblock content %} + + + +{% block postcontent %}{% endblock postcontent %} + + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..383c6ef --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,545 @@ +{% extends 'base.html' %} +{% block content %} + +
+
+ + + + + + + +
+ + {% for monitored_pr in open_prs %} + {% with code_pr_number=monitored_pr.code_pull_request.pr_number|stringformat:"s" documentation_pr_number=monitored_pr.documentation_pull_request.pr_number|stringformat:"s" %} + {% with code_pr_name="#"|add:code_pr_number|add:" "|add:monitored_pr.code_pull_request.pr_title %} +
+ {% if monitored_pr.pull_request_status == "NOT_CONNECTED" %} +
+
+ {{ code_pr_name }} + {{monitored_pr.pull_request_status}} +
+
+
+ Connect +
+ {% elif monitored_pr.pull_request_status == "MANUALLY_APPROVED" %} +
+
+ {{ code_pr_name }} + {{monitored_pr.get_pull_request_status_display}} +
+
+
+ Approved +
+ by @sharmashobit 20 min ago +
+
+ {% else %} + + {% with documentation_pr_name="#"|add:documentation_pr_number|add:" "|add:monitored_pr.documentation_pull_request.pr_title %} + {% if monitored_pr.pull_request_status == "APPROVED" %} + +
+ Approved +
+ by @sharmashobit 20 min ago +
+
+ {% else %} + {% comment %}Expected states `APPROVAL_PENDING`, `STALE_CODE` or `STALE_APPROVAL`{% endcomment %} + +
+ Approve +
+ {% endif %} + {% endwith %} + {% endif %} +
+ {% endwith %} + {% endwith %} + {% endfor %} + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+{% endblock %} + +{% block postcontent %} + + +{% endblock postcontent %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..54ff53e --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,5 @@ + + + Login with GitHub + + \ No newline at end of file diff --git a/app/views.py b/app/views.py index aa20dc5..33ac3c5 100644 --- a/app/views.py +++ b/app/views.py @@ -4,7 +4,7 @@ import logging import uuid from urllib.parse import parse_qs - +from typing import Any, Dict import github import lib from . import lib as app_lib @@ -12,14 +12,19 @@ from django.conf import settings from django.contrib.auth import get_user_model, login from django.db import transaction -from django.http import HttpResponseRedirect, JsonResponse +from django.http import Http404, HttpResponseRedirect, JsonResponse from django.shortcuts import render from django.utils import timezone from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required from django.contrib.auth import models as auth_models -from django.views.generic import FormView +from django.views.generic import FormView, TemplateView +from django.contrib.auth import views as auth_views +from requests.models import PreparedRequest +from django.shortcuts import get_object_or_404 + from . import models as app_models from . import forms as app_forms @@ -38,9 +43,8 @@ def get(self, request, *args, **kwargs): - setup_action: str """ code: str = request.GET["code"] - installation_id: int = request.GET["installation_id"] - setup_action: str = request.GET["setup_action"] - logger.info(request.GET) + + # logger.info(request.GET) # github_app_auth_user = app_models.GithubAppAuth.objects.create( # code=code, # installation_id=installation_id, @@ -61,6 +65,7 @@ def get(self, request, *args, **kwargs): resp = requests.post( "https://github.com/login/oauth/access_token", json=payload ) + redirect_url = None if resp.ok: response = parse_qs(resp.text) if "error" in response: @@ -78,20 +83,8 @@ def get(self, request, *args, **kwargs): ) github_instance = github.Github(access_token) user_instance = github_instance.get_user() - github_installation_manager = lib.GithubInstallationManager( - installation_id=installation_id, user_token=access_token - ) - installation_details = ( - github_installation_manager.get_installation_details() - ) - account_details = installation_details["account"] - account_name = account_details["login"] - account_id = account_details["id"] - account_type = account_details["type"] - avatar_url = account_details["avatar_url"] - - access_group = auth_models.Group.objects.get(name="github_user") with transaction.atomic(): + access_group = auth_models.Group.objects.get(name="github_user") (auth_user_instance, _) = get_user_model().objects.get_or_create( username=user_instance.login, defaults={ @@ -119,26 +112,71 @@ def get(self, request, *args, **kwargs): expires_at=refresh_token_expires_at, github_user=github_user, ) - ( - installation_instance, - _, - ) = app_models.GithubAppInstallation.objects.update_or_create( - installation_id=installation_id, - account_id=account_id, - defaults={ - "account_name": account_name, - "state": app_models.GithubAppInstallation.InstallationState.INSTALLED, - "account_type": account_type, - "avatar_url": avatar_url, - "creator": github_user, - }, - ) login(request, auth_user_instance) - # installation_instance.save() - installation_instance.update_token() + if "installation_id" in request.GET: + installation_id: int = request.GET["installation_id"] + setup_action: str = request.GET["setup_action"] + + github_installation_manager = lib.GithubInstallationManager( + installation_id=installation_id, user_token=access_token + ) + installation_details = ( + github_installation_manager.get_installation_details() + ) + account_details = installation_details["account"] + account_name = account_details["login"] + account_id = account_details["id"] + account_type = account_details["type"] + avatar_url = account_details["avatar_url"] + + with transaction.atomic(): + + ( + installation_instance, + _, + ) = app_models.GithubAppInstallation.objects.update_or_create( + installation_id=installation_id, + account_id=account_id, + defaults={ + "account_name": account_name, + "state": app_models.GithubAppInstallation.InstallationState.INSTALLED, + "account_type": account_type, + "avatar_url": avatar_url, + "creator": github_user, + }, + ) + # installation_instance.save() + installation_instance.update_token() + if redirect_url is None: + redirect_url = ( + f"/{installation_instance.account_name}/repositories/" + ) + for installation in user_instance.get_installations(): + if installation.app_id == settings.GITHUB_CREDS["app_id"]: + installation_instance = ( + app_models.GithubAppInstallation.objects.get( + account_id=installation.target_id, + installation_id=installation.id, + ) + ) + app_models.GithubAppUser.objects.update_or_create( + github_user=github_user, + installation=installation_instance, + ) + if redirect_url is None: + redirect_url = ( + f"/{installation_instance.account_name}/repositories/" + ) else: logger.error(resp.text) - return HttpResponseRedirect("/admin/") + return Http404("Something went wrong") + if "state" in request.GET: + next_url = app_models.GithubLoginState.objects.get( + state=request.GET["state"] + ).redirect_url + if next_url is not None or next_url != "": + redirect_url = next_url + return HttpResponseRedirect(redirect_url or "/installations/") @method_decorator(csrf_exempt, name="dispatch") @@ -245,8 +283,45 @@ def get(self, request): assert False, request -class PRView(FormView): - template_name = "app/pr_approval.html" +@method_decorator(login_required, name="dispatch") +class AllPRView(TemplateView): + template_name = "index.html" + + def get(self, request, *args: Any, **kwargs: Any): + github_user = request.user.github_user + context = self.get_context_data(**kwargs) + + if github_user is None: + return Http404("User not found") + # elif app_models.GithubAppUser.objects.filter(): + # pass + get_object_or_404( + app_models.GithubAppUser, + github_user=github_user, + installation=context["repo_mapping"].integration, + ) + return self.render_to_response(context) + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + matches = self.request.resolver_match.kwargs + context["repo_mapping"] = app_models.GithubRepoMap.objects.get( + code_repo__repo_full_name__iexact="{}/{}".format( + matches["account_name"], matches["repo_name"] + ) + ) + context["open_prs"] = app_models.MonitoredPullRequest.objects.filter( + code_pull_request__repository=context["repo_mapping"].code_repo + ) + context["all_documentation_prs"] = app_models.GithubPullRequest.objects.filter( + repository=context["repo_mapping"].documentation_repo + ) + return context + + +@method_decorator(login_required, name="dispatch") +class PRView(View): + template_name = "index.html" form_class = app_forms.GithubPRApprovalForm success_url = "." @@ -261,23 +336,108 @@ def get_instance(self): ) return app_models.MonitoredPullRequest.objects.get(code_pull_request=pr) - def get_form(self, form_class=None): - if form_class is None: - form_class = self.get_form_class() - return form_class(instance=self.get_instance(), **self.get_form_kwargs()) - - def form_valid(self, form): - # form.instance = self.get_instance() - # form.save() - clean_data = form.cleaned_data + def post(self, request, *args, **kwargs): + payload = json.loads(request.body) instance = self.get_instance() - if "documentation_pull_request" in clean_data: - instance.documentation_pull_request = clean_data[ + get_object_or_404( + app_models.GithubAppUser, + github_user=request.user.github_user, + installation=instance.integration, + ) + action = payload["action"] + success = True + if ( + action == "connect_documentation_pr" + and "documentation_pull_request" in payload + ): + instance.documentation_pull_request_id = payload[ "documentation_pull_request" ] - else: + elif ( + action == "approve_pr" + and "approve_pull_request" in payload + and payload["approve_pull_request"] == "APPROVED" + ): + assert ( + payload["github_username"] == request.user.github_user.account_name + ), "User mismatch" instance.pull_request_status = ( app_models.MonitoredPullRequest.PullRequestStatus.APPROVED ) + elif action == "unlink": + instance.documentation_pull_request = None + elif action == "manual_pr_approval": + assert ( + payload["github_username"] == request.user.github_user.account_name + ), "User mismatch" + instance.pull_request_status = ( + app_models.MonitoredPullRequest.PullRequestStatus.MANUAL_APPROVAL + ) + else: + success = False instance.save() - return super().form_valid(form) + return JsonResponse({"status": success}) + + +class LoginView(auth_views.LoginView): + template_name = "login.html" + + def get(self, request, *args: Any, **kwargs: Any): + if request.user.is_authenticated: + return HttpResponseRedirect("/installations/") + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + login_state_instance = app_models.GithubLoginState() + if self.request.GET.get("next"): + login_state_instance.redirect_url = self.request.GET.get("next") + login_state_instance.save() + url = "https://github.com/login/oauth/authorize" + params = { + "client_id": settings.GITHUB_CREDS["client_id"], + "allow_signup": False, + "state": login_state_instance.state.__str__(), + } + req = PreparedRequest() + req.prepare_url(url, params) + context["github_login_url"] = req.url + return context + + +class AllInstallationsView(TemplateView): + template_name = "all_installations.html" + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + context["all_installations"] = app_models.GithubAppInstallation.objects.filter( + id__in=app_models.GithubAppUser.objects.filter( + github_user=self.request.user.github_user + ).values_list("installation", flat=True) + ) + return context + + +class ListInstallationRepos(TemplateView): + template_name = "all_repos.html" + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + matches = self.request.resolver_match.kwargs + + context = super().get_context_data(**kwargs) + current_installation = app_models.GithubAppInstallation.objects.get( + id__in=app_models.GithubAppUser.objects.filter( + github_user=self.request.user.github_user + ).values_list("installation", flat=True), + **matches, + ) + + context["current_installation"] = current_installation + context["all_repos"] = app_models.GithubRepository.objects.filter( + id__in=app_models.MonitoredPullRequest.objects.filter( + integration=current_installation + ) + .values_list("code_pull_request__repository", flat=True) + .distinct() + ) + return context diff --git a/cdoc/settings.py b/cdoc/settings.py index 3123378..1595343 100644 --- a/cdoc/settings.py +++ b/cdoc/settings.py @@ -49,6 +49,7 @@ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", + "whitenoise.runserver_nostatic", "django.contrib.staticfiles", "django_extensions", "app", diff --git a/cdoc/urls.py b/cdoc/urls.py index 33cb485..796206d 100644 --- a/cdoc/urls.py +++ b/cdoc/urls.py @@ -17,11 +17,35 @@ from django.urls import path, include from app import views as app_views +from django.contrib.auth import views as auth_views + + urlpatterns = [ + path( + "accounts/login/", + app_views.LoginView.as_view(), + ), + path( + "accounts/logout/", + auth_views.LogoutView.as_view(next_page="/"), + ), + path( + "installations/", + app_views.AllInstallationsView.as_view(), + ), + path( + "/repos/", + app_views.ListInstallationRepos.as_view(), + ), path( "//pull//", app_views.PRView.as_view(), ), + path( + "//pulls/", + app_views.AllPRView.as_view(), + ), + # path("app/", include("app.urls")), path("admin/", admin.site.urls), ] diff --git a/staticfiles/css/base.css b/staticfiles/css/base.css new file mode 100644 index 0000000..fce7bad --- /dev/null +++ b/staticfiles/css/base.css @@ -0,0 +1,575 @@ +*{ + padding: 0; + margin: 0; +} +a{ + text-decoration: none; +} +a:hover{ + text-decoration: underline; +} + +input:focus, textarea:focus{ + outline: none !important; +} + +body { + width: 100%; + margin: 0 auto; + font: .9em/1.2 'IBM Plex Sans', sans-serif; + background-color: #18181B; +} + +.parent-container{ + max-width: 1200px; + width: 1200px; + margin: 0 auto; +} +.container { + display: grid; + grid-template-columns: repeat(12, minmax(0,1fr)); + grid-column-gap: 32px; + position: relative; +} + + +aside { + border-right: 1px solid #999; +} + +header { + display: grid; + grid-template-columns: repeat(12, minmax(0,1fr)); + grid-gap: 32px; + justify-items: end; + padding: 24px 32px 24px 32px; + background-color: rgba(45, 212, 143, 0.05); +} +header > div.logo{ + grid-column: 1 / span 2; + justify-self: start; +} + +header > div.menu{ + grid-column-start: 6; + grid-column-end: 13; + justify-self: end; +} + +header > div.menu .menu-item{ + margin-right: 32px; +} + +header > div.menu .menu-item .iconify { + font-size: 24px; + color: #FFFFFF; +} +header > div.menu .menu-item:last-child{ + margin-right: 0; +} +.breadcrumb{ + grid-column: 2 / span 12; + margin-top: 34px; + margin-bottom: 34px; + display: flex; +} + +.breadcrumb a{ + vertical-align: middle; + line-height: 36px; + font-size: 24px; +} + +.breadcrumb .iconify{ + font-size: 16px; + margin-right: 5px; +} + +.breadcrumb > .repo{ + margin-left: 5px; + margin-right: 10px; + background: url(../images/square-braket.svg) no-repeat; + background-size: 16px 23px; + background-position: left 8.5px; + padding-left: 24px; + position: relative; +} +.breadcrumb > .repo .iconify{ + position: absolute; + left: -2px; + top: 12.5px; + font-size: 15px; + color: #4fb2df; +} + +.breadcrumb > .repo > .repo-item{ + line-height: 20px; + font-size: 14px; + font-weight: 700; + display: block; +} +.breadcrumb > .repo > .repo-item:first-child{ + margin-bottom: 4px; + line-height: 16px; +} + + +.search{ + grid-column: span 10; + grid-column-start: 2; + grid-column-end: 10; + margin-bottom: 17px; +} + +.search > input{ + padding: 14px 28px; + border: 1px solid #919192; + color: #A8A29E; + font-size: 20px; + line-height: 24px; + background-color: transparent; + width: 100%; + border-radius: 8px; + box-sizing: border-box; +} +.search > input:focus{ + border: 1px solid #919192; +} + + +.doc-history{ + grid-column: span 12; + grid-column-start: 2; + grid-column-end: 12; + margin-bottom: 17px; + border: 1px solid #434547; + box-sizing: border-box; + border-radius: 8px; + margin: 0; +} +.doc-history .doc-history-row { + padding: 0px 30px; + border-bottom: 1px solid #434547; + display: grid; + grid-template-columns: repeat(12, minmax(0,1fr)); + grid-gap: 32px; +} +.dialog-history-row { + display: grid; + grid-template-columns: repeat(12, minmax(0,1fr)); + grid-gap: 32px; +} + +.doc-history .doc-history-row:last-child{ + border-bottom: 0px solid #434547; +} +.doc-history-row > .row-left, +.dialog-history-row > .row-left{ + grid-column-start: 1; + grid-column-end: 8; + justify-self: start; + padding: 20px 0; +} +.dialog-history-row > .row-left, +.dialog-history-row > .row-right{ + padding: 0px 0 !important; +} +.cell-group { + background: url(../images/square-braket.svg) no-repeat; + background-size: 16px 23px; + background-position: left 9.5px; + padding-left: 18px; + position: relative; +} +.cell-group.no-bracket{ + padding-left: 0px; + background: none; + min-height: auto !important; +} + +.cell-group > .iconify{ + position: absolute; + left: -1px; + top: 14px; +} +.cell-group a{ + display: block; + font-size: 16px; + line-height: 20px; +} + +.cell-group a.item-top{ + font-size: 14px; + line-height: 16px; +} + +.cell-group a:last-child{ + line-height: 20px; +} + +.cell-group .cell-item .iconify{ + margin-right: 5px; + font-size: 16px; + vertical-align: middle; +} +.cell-group.no-bracket .cell-item .iconify{ + margin-right: 16px; + font-size: 16px; + vertical-align: middle; + line-height: 20; +} +.cell-group.large a{ + display: block; + font-size: 20px; + line-height: 24px; +} + +.cell-group.large a.item-top{ + font-size: 20px; + line-height: 24px; +} + +.cell-group.large a:last-child{ + line-height: 24px; +} + +.cell-group.large .cell-item .iconify{ + margin-right: 5px; + font-size: 16px; + line-height: 24px; + vertical-align: middle; +} + +.doc-history-row > .row-right, +.dialog-history-row > .row-right{ + grid-column-start: 8; + grid-column-end: 13; + justify-self: end; + text-align: right; + padding: 20px 0; +} + + + + +.doc-history-row > .row-right .approved{ + font-size: 20px; + line-height: 24px; +} + +.doc-history-row > .row-right .approved .iconify{ + margin-right: 5px; + font-size: 16px; + vertical-align: middle; +} + + +.doc-history-row > .row-right .user{ + font-size: 16px; + line-height: 20px; +} + +.doc-history-row > .row-right .username{ + font-weight: 700; +} + + +/* modal */ + + +.modal{ + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + display: none; +} +.modal .modal-parent{ + max-width: 1200px; + width: 1200px; + margin: 0 auto; +} +.modal .modal-container{ + display: grid; + grid-template-columns: repeat(12, minmax(0,1fr)); + grid-column-gap: 32px; + +} +.modal.opened,.modal.opened:before{ + display: block; +} +.modal:before{ + content: ""; + display: none; + background: rgba(24, 24, 27, 0.7); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; +} + +.modal .modal-dialog{ + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 8px; + min-width: 614px; + min-height: 100px; + grid-column-start: 1; + grid-column-end: 8; + justify-self: start; + z-index: 100; + padding: 12px 0; +} +.modal .modal-dialog.large-dialog{ + min-width: 961px; +} +.modal.warning .modal-dialog{ + background: #141414; + border: 1px solid #966220; + box-sizing: border-box; + box-shadow: 3px 4px 4px #966220; + text-align: center; +} +.modal.warning .modal-dialog:before{ + content: ""; + width: 100%; + background: #966220; + border-radius: 8px; + height: 12px; + position: absolute; + left: 0; + top: -4px; +} +.modal.regular .modal-dialog{ + background: #141414; + border: 1px solid #2DD4BF; + box-sizing: border-box; + box-shadow: 3px 4px 4px rgba(165, 243, 252, 0.2); + border-radius: 8px; +} + +.modal.regular .modal-dialog .modal-title{ + margin-left: 35px; +} +.modal .modal-dialog .modal-title{ + font-size: 32px; + line-height: 44px; +} + +.modal .modal-dialog .modal-title .close{ + position: absolute; + right: 14px; + top: 17px; + font-size: 24px; + line-height: 24px; + cursor: pointer; +} +.modal .modal-dialog .modal-title .close:hover{ + color: #FFFFFF; +} + +.modal .modal-dialog .modal-title .goback{ + position: absolute; + left: 14px; + top: 17px; + font-size: 24px; + line-height: 24px; + cursor: pointer; +} +.modal .modal-dialog .modal-title .goback:hover{ + color: #FFFFFF; +} +.modal .modal-body { + padding: 30px 0 33px 0; +} + +.modal .modal-body .modal-item{ + display: inline-block; + text-align:left; +} + +.modal .modal-body { + margin: auto 70px; +} + +.modal.regular .modal-body { + margin: auto 35px !important; +} +.modal .body-row{ + font-weight: 400; + font-size: 16px; + line-height: 24px; +} + +.modal .body-row strong{ + font-weight: 700; +} + +.modal .modal-body .modal-msg{ + margin: 41px auto 0 auto; +} + +.modal .body-row.bordered-list{ + padding: 20px 0; + border-bottom: 1px solid #434547; + min-height: 32px; +} + +.modal .body-row.bordered-list > .connect{ + display: none; +} + +.modal .body-row.bordered-list:hover .connect{ + display: block; +} + + +.modal .group-list > .body-row:last-child{ + border-bottom: 0px solid #434547; +} + +.modal .modal-body .userinput{ + margin: 19px auto 0 auto; + width: auto; +} +.modal .modal-body .userinput input{ + width: 99.8%; + padding: 14px 0 12px 0; + font-size: 20px; + line-height: 24px; + text-align: center; + background: transparent; + border: 1px solid #919192; + box-sizing: border-box; + border-radius: 8px; + color: #A8A29E; +} + +.modal .modal-body .cta-row{ + margin-top: 25px; +} + +.modal .modal-body .caption{ + margin-top: 34px; + font-size: 14px; + line-height: 16px; +} + +/* colors */ +.main-text{ + color: #A8A29E; +} +.main-text-strong{ + color: #FFFFFF; +} +.main-error-text{ + color: #C62A21; +} +.main-warning-base{ + color: #FBEFBA; +} +.main-cta-secondary-base{ + color: #4FB2DF; +} +.main-cta-primary-base{ + color: #2DD4BF; +} +.main-success-text{ + color: #72D23C; +} +.main-cta-primary-base{ + color: #2DD4BF +} + + + +/* actionables */ +.cta{ + padding: 8px 16px; + border-radius: 8px; + font-size: 16px; + line-height: 16px; + font-weight: 700; + display: inline-block; + cursor: pointer; +} + +.cta-primary{ + background-color: #2DD4BF; + color: #434547; +} +.cta-primary:hover{ + background-color: #2c9f90; + text-decoration: none; +} + + +.cta-warning{ + background-color: #966220; + color: #FBEFBA; +} +.cta-warning:hover{ + background-color: #8b5b1d; + text-decoration: none; +} + + +/* typo */ +.mt31{ + margin-top: 31px !important; +} +.mt11{ + margin-top: 11px !important; +} + +.mt12{ + margin-top: 12px !important; +} + +.pt0{ + padding-top: 0px !important; +} +.pb22{ + padding-bottom: 22px !important; +} +.pb0{ + padding-bottom: 0px !important; +} +.mhz40{ + margin-left: 40px !important; + margin-right: 40px !important; +} +.pt12{ + padding-top: 12px !important; +} +.mt27{ + margin-top: 27px !important; +} +.mt70{ + margin-top: 70px !important; +} +.tc{ + text-align: center !important; +} +.lh16{ + line-height: 16px !important; +} +.text-to-bottom{ + display: inline-block; + border-bottom: 1px solid #966220; + padding-bottom: 6px; + +} + +.visible{ + display: block; +} +.hidden{ + display: none; +} \ No newline at end of file diff --git a/staticfiles/images/fifthtry-logo.png b/staticfiles/images/fifthtry-logo.png new file mode 100644 index 0000000..80c185f Binary files /dev/null and b/staticfiles/images/fifthtry-logo.png differ diff --git a/staticfiles/images/square-braket.svg b/staticfiles/images/square-braket.svg new file mode 100644 index 0000000..e503b6d --- /dev/null +++ b/staticfiles/images/square-braket.svg @@ -0,0 +1,4 @@ + + + +