Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/breatheco-de/apiv2
Browse files Browse the repository at this point in the history
  • Loading branch information
jefer94 committed Dec 16, 2024
2 parents 47f1c3a + ed2bf49 commit 7f00621
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 17 deletions.
33 changes: 29 additions & 4 deletions breathecode/authenticate/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def clear_user_password(modeladmin, request, queryset):

@admin.register(UserProxy)
class UserAdmin(UserAdmin):
list_display = ("username", "email", "first_name", "last_name", "is_staff", "github_login")
list_display = ("username", "email", "first_name", "last_name", "is_staff", "github_login", "google_login")
actions = [clean_all_tokens, clean_expired_tokens, send_reset_password, clear_user_password]

def get_queryset(self, request):
Expand All @@ -176,6 +176,11 @@ def github_login(self, obj):
f"<a rel='noopener noreferrer' target='_blank' href='/v1/auth/github/?user={obj.id}&url={self.github_callback}'>connect github</a>"
)

def google_login(self, obj):
return format_html(
"<a rel='noopener noreferrer' target='_blank' href='/v1/auth/academy/google'>connect google</a>"
)


@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -473,12 +478,21 @@ def clean_errors(modeladmin, request, queryset):

@admin.register(AcademyAuthSettings)
class AcademyAuthSettingsAdmin(admin.ModelAdmin):
list_display = ("academy", "github_is_sync", "github_errors", "github_username", "github_owner", "authenticate")
list_display = (
"academy",
"github_is_sync",
"github_errors",
"github_username",
"github_owner",
"authenticate_github",
"authenticate_google",
)
search_fields = ["academy__slug", "academy__name", "github__username", "academy__id"]
actions = (clean_errors, activate_github_sync, deactivate_github_sync, sync_github_members)
raw_id_fields = ["github_owner", "google_cloud_owner"]

def get_queryset(self, request):
self.admin_request = request

self.github_callback = "https://4geeks.com"
self.github_callback = str(base64.urlsafe_b64encode(self.github_callback.encode("utf-8")), "utf-8")
Expand All @@ -490,16 +504,27 @@ def github_errors(self, obj):
else:
return format_html("<span class='badge bg-success'>No errors</span>")

def authenticate(self, obj):
def authenticate_github(self, obj):
settings = AcademyAuthSettings.objects.get(id=obj.id)
if settings.github_owner is None:
return format_html("no owner")

scopes = str(base64.urlsafe_b64encode(b"user repo admin:org"), "utf-8")
return format_html(
f"<a href='/v1/auth/github?user={obj.github_owner.id}&url={self.github_callback}&scope={scopes}'>connect owner</a>"
f"<a href='/v1/auth/github?user={obj.github_owner.id}&url={self.github_callback}&scope={scopes}'>connect github</a>"
)

def authenticate_google(self, obj):
settings = AcademyAuthSettings.objects.get(id=obj.id)
if settings.google_cloud_owner is None:
return format_html("no google cloud owner")

request = getattr(self, "admin_request", None)
current_url = f"{request.scheme}://{request.get_host()}{request.get_full_path()}"
current_url = str(base64.urlsafe_b64encode(current_url.encode("utf-8")), "utf-8")

return format_html(f"<a href='/v1/auth/academy/google?url={current_url}'>connect google</a>")


@admin.register(GoogleWebhook)
class GoogleWebhookAdmin(admin.ModelAdmin):
Expand Down
2 changes: 1 addition & 1 deletion breathecode/authenticate/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from capyc.core.managers import feature

flags = feature.flags
flags = feature._flags


@feature.availability("authenticate.set_google_credentials")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
{"slug": "read_mentorship_bill", "description": "Read all mentroship bills from one academy"},
{"slug": "read_asset", "description": "Read all academy registry assets"},
{"slug": "crud_asset", "description": "Update, create and delete registry assets"},
{"slug": "read_asset_error", "description": "Update, create and delete asset errors"},
{"slug": "crud_asset_error", "description": "Update, create and delete asset errors"},
{"slug": "read_content_variables", "description": "Read all academy content variables used in the asset markdowns"},
{
"slug": "crud_content_variables",
Expand Down Expand Up @@ -361,6 +363,8 @@ def extend_roles(roles: list[RoleType]) -> None:
"read_my_academy",
"read_asset",
"crud_asset",
"read_asset_error",
"crud_asset_error",
"read_category",
"crud_category",
"read_content_variables",
Expand Down
4 changes: 3 additions & 1 deletion breathecode/authenticate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
LoginView,
LogoutView,
MeInviteView,
MeProfileAcademyInvite,
MemberView,
MeProfileAcademyInvite,
PasswordResetView,
ProfileInviteMeView,
ProfileMePictureView,
Expand All @@ -63,6 +63,7 @@
pick_password,
receive_google_webhook,
render_academy_invite,
render_google_connect,
render_invite,
render_user_invite,
reset_password_view,
Expand Down Expand Up @@ -154,6 +155,7 @@
# google authentication oath2.0
path("google/callback", save_google_token, name="google_callback"),
path("google/<str:token>", get_google_token, name="google_token"),
path("academy/google", render_google_connect, name="academy_google_token"),
path("gitpod/sync", sync_gitpod_users_view, name="sync_gitpod_users"),
# sync with gitHUB
path("academy/github/user", GithubUserView.as_view(), name="github_user"),
Expand Down
24 changes: 23 additions & 1 deletion breathecode/authenticate/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import re
import urllib.parse
from datetime import timedelta
from urllib.parse import parse_qs, urlencode
from urllib.parse import parse_qs, urlencode, urlparse

import aiohttp
import requests
Expand Down Expand Up @@ -2251,6 +2251,28 @@ async def async_iter(iterable: list):
raise APIException("Error from google credentials")


@private_view()
def render_google_connect(request, token):
callback_url = request.GET.get("url", None)

if not callback_url:
# Fallback to HTTP_REFERER if 'url' is not in the query string
referrer = request.META.get("HTTP_REFERER", "")
# Optionally, parse query parameters from the referrer if needed
if referrer:
parsed_referrer = urlparse(referrer)
query_params = parse_qs(parsed_referrer.query)
callback_url = str(base64.urlsafe_b64encode(query_params.get("url", [None])[0].encode("utf-8")), "utf-8")

if callback_url is None:
raise ValidationException("Callback URL specified", slug="no-callback")

token, created = Token.get_or_create(user=request.user, token_type="one_time")

url = f"/v1/auth/google/{token}?url={callback_url}"
return HttpResponseRedirect(redirect_to=url)


@api_view(["POST"])
@permission_classes([AllowAny])
def receive_google_webhook(request):
Expand Down
24 changes: 24 additions & 0 deletions breathecode/marketing/migrations/0092_coursetranslation_heading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2024-12-12 14:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("marketing", "0091_course_cohorts_order"),
]

operations = [
migrations.AddField(
model_name="coursetranslation",
name="heading",
field=models.CharField(
blank=True,
default=None,
help_text="Heading that will be used in the landing page",
max_length=160,
null=True,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.2 on 2024-12-12 14:48

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("marketing", "0092_coursetranslation_heading"),
]

operations = [
migrations.AlterField(
model_name="coursetranslation",
name="heading",
field=models.TextField(
blank=True,
default=None,
help_text="Heading that will be used in the landing page",
max_length=400,
null=True,
),
),
]
3 changes: 3 additions & 0 deletions breathecode/marketing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,9 @@ class CourseTranslation(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
lang = models.CharField(max_length=5, validators=[validate_language_code])
title = models.CharField(max_length=60)
heading = models.TextField(
max_length=400, help_text="Heading that will be used in the landing page", default=None, null=True, blank=True
)
description = models.TextField(max_length=400)
short_description = models.CharField(max_length=120, null=True, default=None, blank=True)
video_url = models.URLField(
Expand Down
3 changes: 2 additions & 1 deletion breathecode/marketing/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
from datetime import timedelta

from capyc.rest_framework.exceptions import ValidationException
from django.db.models.query_utils import Q
from django.utils import timezone
from rest_framework import serializers
Expand All @@ -11,7 +12,6 @@
from breathecode.services.activecampaign.client import acp_ids
from breathecode.utils import serpy
from breathecode.utils.integer_to_base import to_base
from capyc.rest_framework.exceptions import ValidationException

from .models import AcademyAlias, ActiveCampaignAcademy, Automation, CourseTranslation, FormEntry, ShortLink, Tag

Expand Down Expand Up @@ -408,6 +408,7 @@ class GetCourseTranslationSerializer(serpy.Serializer):
landing_variables = serpy.Field()
landing_url = serpy.Field()
video_url = serpy.Field()
heading = serpy.Field()


class GetCourseSmallSerializer(serpy.Serializer):
Expand Down
2 changes: 1 addition & 1 deletion breathecode/payments/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from breathecode.registry.models import Asset
from breathecode.utils.decorators.consume import ServiceContext

flags = feature.flags
flags = feature._flags


@feature.availability("payments.bypass_consumption")
Expand Down
24 changes: 24 additions & 0 deletions breathecode/registry/migrations/0053_alter_asset_template_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.1 on 2024-12-09 23:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("registry", "0052_assettechnology_marketing_information"),
]

operations = [
migrations.AlterField(
model_name="asset",
name="template_url",
field=models.CharField(
blank=True,
default=None,
help_text="This template will be used to open the asset (only applied for projects). If project has no template it should state 'self' as template url",
max_length=500,
null=True,
),
),
]
14 changes: 8 additions & 6 deletions breathecode/registry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,12 @@ def __init__(self, *args, **kwargs):
help_text="Only applies to LearnPack tutorials that have been published in the LearnPack cloud",
)

template_url = models.URLField(
template_url = models.CharField(
null=True,
blank=True,
default=None,
help_text="This template will be used to open the asset (only applied for projects)",
max_length=500,
help_text="This template will be used to open the asset (only applied for projects). If project has no template it should state 'self' as template url",
)
dependencies = models.CharField(
max_length=50,
Expand Down Expand Up @@ -896,10 +897,11 @@ def get_by_github_url(github_url):

# if branch is specified in the URL, we will use it to find the asset
# For example: https://github.com/4GeeksAcademy/react-hello/blob/1.0/README.md
if "blob" in path_parts:
blob_index = path_parts.index("blob")
if len(path_parts) > blob_index + 1:
branch_name = path_parts[blob_index + 1]
branch_pattern = "blob" if "blob" in path_parts else "tree" if "tree" in path_parts else None
if branch_pattern is not None:
branch_index = path_parts.index(branch_pattern)
if len(path_parts) > branch_index + 1:
branch_name = path_parts[branch_index + 1]

if branch_name:

Expand Down
61 changes: 61 additions & 0 deletions breathecode/registry/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
AssetAlias,
AssetCategory,
AssetComment,
AssetErrorLog,
AssetKeyword,
AssetTechnology,
ContentVariable,
Expand Down Expand Up @@ -195,6 +196,18 @@ class AcademyCommentSerializer(serpy.Serializer):
created_at = serpy.Field()


class AcademyErrorSerializer(serpy.Serializer):
id = serpy.Field()
asset_type = serpy.Field()
slug = serpy.Field()
status = serpy.Field()
path = serpy.Field()
status_text = serpy.Field()
asset = SmallAsset(required=False)
user = UserSerializer(required=False)
created_at = serpy.Field()


class AssetHookSerializer(serpy.Serializer):
id = serpy.Field()
slug = serpy.Field()
Expand Down Expand Up @@ -824,6 +837,54 @@ def validate(self, data):
return validated_data


class AssetErrorListSerializer(serializers.ListSerializer):

def update(self, instances, validated_data):

instance_hash = {index: instance for index, instance in enumerate(instances)}

result = [self.child.update(instance_hash[index], attrs) for index, attrs in enumerate(validated_data)]

return result


class PutAssetErrorSerializer(serializers.ModelSerializer):

class Meta:
model = AssetErrorLog
exclude = ("asset_type", "slug", "path", "user", "created_at")
list_serializer_class = AssetErrorListSerializer

def validate(self, data):

validated_data = super().validate(data)

updating_status = (
True if "status" in validated_data and validated_data["status"] != self.instance.status else False
)
updating_asset = True if "asset" in validated_data and validated_data["asset"] != self.instance.asset else False

if updating_asset:
if updating_status:
raise ValidationException(
"You cannot update the status and the asset of the error at the same time",
slug="update-status-along",
)

return validated_data

def update(self, instance, validated_data):

if "status" in validated_data and validated_data["status"] != instance.status:
AssetErrorLog.objects.filter(
slug=instance.slug, asset_type=instance.asset_type, path=instance.path, asset=instance.asset
).update(status=validated_data["status"])
return instance

else:
return super().update(instance, validated_data)


class AssetListSerializer(serializers.ListSerializer):

def update(self, instances, validated_data):
Expand Down
Loading

0 comments on commit 7f00621

Please sign in to comment.