Skip to content

Commit

Permalink
Merge pull request #1462 from breatheco-de/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
tommygonzaleza committed Sep 18, 2024
2 parents 6a4d279 + 8ddd16d commit 7620a34
Show file tree
Hide file tree
Showing 37 changed files with 2,551 additions and 752 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,5 @@ google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
capy-core = {extras = ["django"], version = "*"}
google-api-python-client = "*"
python-dotenv = "*"
uvicorn-worker = "*"
1,202 changes: 621 additions & 581 deletions Pipfile.lock

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions breathecode/admissions/receivers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# from django.db.models.signals import post_save
import logging
import re
from typing import Any, Type

from django.dispatch import receiver
Expand All @@ -15,6 +16,7 @@

# add your receives here
logger = logging.getLogger(__name__)
GITHUB_URL_PATTERN = re.compile(r"https?:\/\/github\.com\/(?P<user>[^\/]+)\/(?P<repo>[^\/\s]+)\/?")


@receiver(cohort_log_saved, sender=Cohort)
Expand Down Expand Up @@ -43,6 +45,32 @@ async def new_cohort_user(sender: Type[Cohort], instance: Cohort, **kwargs: Any)
)


@receiver(revision_status_updated, sender=Task, weak=False)
def schedule_repository_deletion(sender: Type[Task], instance: Task, **kwargs: Any):
logger.info("Scheduling repository deletion for task: " + str(instance.id))

if instance.revision_status != Task.RevisionStatus.PENDING and instance.github_url:
match = GITHUB_URL_PATTERN.match(instance.github_url)
if match:
user = match.group("user")
repo = match.group("repo")
from breathecode.assignments.models import RepositoryDeletionOrder

order, created = RepositoryDeletionOrder.objects.get_or_create(
provider=RepositoryDeletionOrder.Provider.GITHUB,
repository_user=user,
repository_name=repo,
defaults={"status": RepositoryDeletionOrder.Status.PENDING},
)

if not created and order.status in [
RepositoryDeletionOrder.Status.NO_STARTED,
RepositoryDeletionOrder.Status.ERROR,
]:
order.status = RepositoryDeletionOrder.Status.PENDING
order.save()


@receiver(revision_status_updated, sender=Task, weak=False)
def mark_saas_student_as_graduated(sender: Type[Task], instance: Task, **kwargs: Any):
logger.info("Processing available as saas student's tasks and marking as GRADUATED if it is")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import random

import pytest

from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode
import capyc.pytest as capy


@pytest.mark.parametrize(
"revision_status, github_url",
[
("PENDING", None),
("PENDING", ""),
("PENDING", "https://github.com/breathecode-test/test"),
("REJECTED", None),
("REJECTED", ""),
("IGNORED", None),
("IGNORED", ""),
("APPROVED", None),
("APPROVED", ""),
],
)
def test_nothing_happens(
database: capy.Database, format: capy.Format, signals: capy.Signals, revision_status: str, github_url: str
):
signals.enable("breathecode.assignments.signals.revision_status_updated")

model = database.create(task={"revision_status": "PENDING", "github_url": github_url})

if revision_status != "PENDING":
model.task.revision_status = revision_status
model.task.save()

assert database.list_of("assignments.Task") == [
format.to_obj_repr(model.task),
]

assert database.list_of("assignments.RepositoryDeletionOrder") == []


@pytest.mark.parametrize("revision_status", ["REJECTED", "IGNORED", "APPROVED"])
@pytest.mark.parametrize(
"github_url, username, repo",
[
("https://github.com/user1/repo1", "user1", "repo1"),
("https://github.com/user2/repo2", "user2", "repo2"),
("https://github.com/user3/repo3", "user3", "repo3"),
],
)
def test_schedule_repository_deletion(
database: capy.Database,
format: capy.Format,
signals: capy.Signals,
revision_status: str,
github_url: str,
username: str,
repo: str,
):
signals.enable("breathecode.assignments.signals.revision_status_updated")

model = database.create(task={"revision_status": "PENDING", "github_url": github_url})

if revision_status != "PENDING":
model.task.revision_status = revision_status
model.task.save()

assert database.list_of("assignments.Task") == [
format.to_obj_repr(model.task),
]

assert database.list_of("assignments.RepositoryDeletionOrder") == [
{
"id": 1,
"provider": "GITHUB",
"repository_name": repo,
"repository_user": username,
"status": "PENDING",
"status_text": None,
},
]
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import re
from datetime import datetime
from typing import Optional
from typing import Any, Optional

from dateutil import parser
from dateutil.relativedelta import relativedelta
from django.core.management.base import BaseCommand
from django.utils import timezone

from breathecode.assignments.models import RepositoryDeletionOrder, RepositoryWhiteList
from breathecode.assignments.models import RepositoryDeletionOrder, RepositoryWhiteList, Task
from breathecode.authenticate.models import AcademyAuthSettings
from breathecode.monitoring.models import RepositorySubscription
from breathecode.registry.models import Asset
Expand All @@ -16,7 +16,7 @@

class Command(BaseCommand):
help = "Clean data from marketing module"
github_url_pattern = re.compile(r"https:\/\/github\.com\/(?P<user>[^\/]+)\/(?P<repo>[^\/\s]+)\/?")
github_url_pattern = re.compile(r"https?:\/\/github\.com\/(?P<user>[^\/]+)\/(?P<repo>[^\/\s]+)\/?")

def handle(self, *args, **options):
self.fill_whitelist()
Expand Down Expand Up @@ -48,11 +48,83 @@ def github(self):
last_check = last.created_at

self.schedule_github_deletions(settings.github_username, last_check)
self.collect_transferred_orders()
self.transfer_ownership()
self.delete_github_repositories()

def check_path(self, obj: dict, *indexes: str) -> bool:
try:
value = obj
for index in indexes:
value = value[index]
return True
except Exception:
return False

def how_many_added_members(self, events: list[dict[str, Any]]) -> int:
return len(
[
event
for event in events
if self.check_path(event, "type")
and self.check_path(event, "payload", "action")
and event["type"] == "MemberEvent"
and event["payload"]["action"] == "added"
]
)

def get_username(self, owner: str, repo: str) -> Optional[str]:
r = repo
repo = repo.lower()
index = -1
for events in self.github_client.get_repo_events(owner, r):
index += 1
for event in events:
if self.check_path(event, "type") is False:
continue

if (
index == 0
and event["type"] == "MemberEvent"
and len(events) < 30
and self.check_path(event, "payload", "action")
and self.how_many_added_members(events) == 1
and self.check_path(event, "payload", "member", "login")
and event["payload"]["action"] == "added"
):
return event["payload"]["member"]["login"]

if (
event["type"] == "watchEvent"
and self.check_path(event, "actor", "login")
and event["actor"]["login"].replace("-", "").lower() in repo
):
return event["actor"]["login"]

if (
event["type"] == "MemberEvent"
and self.check_path(event, "payload", "member", "login")
and event["payload"]["member"]["login"].replace("-", "").lower() in repo
):
return event["payload"]["member"]["login"]

if (
event["type"] == "IssuesEvent"
and self.check_path(event, "payload", "assignee", "login")
and event["payload"]["assignee"]["login"].replace("-", "").lower() in repo
):
return event["payload"]["assignee"]["login"]

if (
self.check_path(event, "actor", "login")
and event["actor"]["login"].replace("-", "").lower() in repo
):
return event["actor"]["login"]

def purge_deletion_orders(self):

page = 0
to_delete = []
while True:
qs = RepositoryDeletionOrder.objects.filter(
status=RepositoryDeletionOrder.Status.PENDING,
Expand All @@ -67,16 +139,18 @@ def purge_deletion_orders(self):
repository_user__iexact=deletion_order.repository_user,
repository_name__iexact=deletion_order.repository_name,
).exists():
deletion_order.delete()
to_delete.append(deletion_order.id)

page += 1

RepositoryDeletionOrder.objects.filter(id__in=to_delete).delete()

def delete_github_repositories(self):

while True:
qs = RepositoryDeletionOrder.objects.filter(
provider=RepositoryDeletionOrder.Provider.GITHUB,
status=RepositoryDeletionOrder.Status.PENDING,
status__in=[RepositoryDeletionOrder.Status.PENDING, RepositoryDeletionOrder.Status.TRANSFERRING],
created_at__lte=timezone.now() - relativedelta(months=2),
)[:100]

Expand All @@ -85,11 +159,23 @@ def delete_github_repositories(self):

for deletion_order in qs:
try:
self.github_client.delete_org_repo(
if self.github_client.repo_exists(
owner=deletion_order.repository_user, repo=deletion_order.repository_name
)
deletion_order.status = RepositoryDeletionOrder.Status.DELETED
deletion_order.save()
):
self.github_client.delete_org_repo(
owner=deletion_order.repository_user, repo=deletion_order.repository_name
)
deletion_order.status = RepositoryDeletionOrder.Status.DELETED
deletion_order.save()

elif deletion_order.status == RepositoryDeletionOrder.Status.TRANSFERRING:
deletion_order.status = RepositoryDeletionOrder.Status.TRANSFERRED
deletion_order.save()

else:
raise Exception(
f"Repository does not exist: {deletion_order.repository_user}/{deletion_order.repository_name}"
)

except Exception as e:
deletion_order.status = RepositoryDeletionOrder.Status.ERROR
Expand Down Expand Up @@ -172,6 +258,84 @@ def schedule_github_deletion(self, provider: str, user: str, repo_name: str):
).exists():
return

RepositoryDeletionOrder.objects.get_or_create(
provider=provider, repository_user=user, repository_name=repo_name
status = RepositoryDeletionOrder.Status.PENDING
if (
Task.objects.filter(github_url__icontains=f"github.com/{user}/{repo_name}")
.exclude(revision_status=Task.RevisionStatus.PENDING)
.exists()
):
status = RepositoryDeletionOrder.Status.NO_STARTED

order, _ = RepositoryDeletionOrder.objects.get_or_create(
provider=provider,
repository_user=user,
repository_name=repo_name,
defaults={"status": status},
)

if order.status != status:
order.status = status
order.save()

def collect_transferred_orders(self):

ids = []

while True:
qs = RepositoryDeletionOrder.objects.filter(
provider=RepositoryDeletionOrder.Provider.GITHUB,
status=RepositoryDeletionOrder.Status.TRANSFERRING,
created_at__gt=timezone.now(),
).exclude(id__in=ids)[:100]

if qs.count() == 0:
break

for deletion_order in qs:
try:
ids.append(deletion_order.id)
if (
self.github_client.repo_exists(
owner=deletion_order.repository_user, repo=deletion_order.repository_name
)
is False
):
deletion_order.status = RepositoryDeletionOrder.Status.TRANSFERRED
deletion_order.save()

except Exception as e:
deletion_order.status = RepositoryDeletionOrder.Status.ERROR
deletion_order.status_text = str(e)
deletion_order.save()

def transfer_ownership(self):
ids = []

while True:
qs = RepositoryDeletionOrder.objects.filter(
provider=RepositoryDeletionOrder.Provider.GITHUB,
status=RepositoryDeletionOrder.Status.PENDING,
created_at__gt=timezone.now(),
).exclude(id__in=ids)[:100]

if qs.count() == 0:
break

for deletion_order in qs:
ids.append(deletion_order.id)
try:
if self.github_client.repo_exists(
owner=deletion_order.repository_user, repo=deletion_order.repository_name
):
new_owner = self.get_username(deletion_order.repository_user, deletion_order.repository_name)
if not new_owner:
continue

self.github_client.transfer_repo(repo=deletion_order.repository_name, new_owner=new_owner)
deletion_order.status = RepositoryDeletionOrder.Status.TRANSFERRING
deletion_order.save()

except Exception as e:
deletion_order.status = RepositoryDeletionOrder.Status.ERROR
deletion_order.status_text = str(e)
deletion_order.save()
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.6 on 2024-07-02 08:25
# Generated by Django 5.1.1 on 2024-09-08 00:40

from django.db import migrations, models

Expand All @@ -22,6 +22,9 @@ class Migration(migrations.Migration):
("PENDING", "Pending"),
("ERROR", "Error"),
("DELETED", "Deleted"),
("TRANSFERRED", "Transferred"),
("NO_STARTED", "No started"),
("TRANSFERRING", "Transferring"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
Expand Down
Loading

0 comments on commit 7620a34

Please sign in to comment.