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" %}
+
+
+ {% elif monitored_pr.pull_request_status == "MANUALLY_APPROVED" %}
+
+
+ {% 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" %}
+
+
+ {% else %}
+ {% comment %}Expected states `APPROVAL_PENDING`, `STALE_CODE` or `STALE_APPROVAL`{% endcomment %}
+
+
+ {% endif %}
+ {% endwith %}
+ {% endif %}
+
+ {% endwith %}
+ {% endwith %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ Approve Changes
+
+
+
+
+
+
+
+
+ You hereby declare that you’ve verified that the implemented code
+ exactly matches the documentation, and is ready for merge.
+
You take full responsibility for this approval
+
+
+
+
+
+
+ Note: Please ask the Dev/Documentor to come in sync if this is not the case
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure?
+
+
+
+
+
+
+ You are declaring that the following pull request
+
+
+
+
+ does not need any associated documentation.
+
This action should be used sparingly. It may be reviewed by your
+ Manager, and will affect your organizations’ dev statistics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure?
+
+
+
+
+
+
+ You are declaring that the following pull request
+
+
+
+
+ does not need any associated documentation.
+
Use this only to do Documentation for deployed code, or improve existing
+ documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attach Code PR
+
+
+
+
+
+ A healthy code always carry good Documentation
+
+ Attach one for:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attach Documentation
+
+
+
+
+
+ A healthy code always carry good Documentation
+
+ Attach one for:
+
+
+
+
+
+
+ {% for doc_pr in all_documentation_prs %}
+
+ {% 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 @@
+