From ce089b8591ff8229020aaa88d8297e3986a878a1 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 2 Dec 2024 21:01:49 +0000 Subject: [PATCH 01/14] added support for asset versionin when searching for an asser by repo --- breathecode/registry/models.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 1e701c91f..73faab4bd 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -896,10 +896,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: @@ -923,9 +924,9 @@ def compare_versions(version1, version2): return 0 pattern = r"^v\d+\.\d+$" - if not bool(re.match(branch_name, string)): + if not bool(re.match(pattern, branch_name)): raise ValueError("Version name must follow the format vX.X, for example: v1.0") - + original_version = branch_name.replace("v", "") original_major_version = int(original_version.split(".")[0]) From 499f71ff815a0ca86ee6bf62dad360792f204ddf Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Dec 2024 23:34:42 +0000 Subject: [PATCH 02/14] converted urlfield to string in asset.template_url to allow to "self" --- breathecode/registry/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 73faab4bd..ef4ed4896 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -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, From e1039fc148e12bb3a0c0b4346d00dd71906dd5ea Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Dec 2024 23:45:23 +0000 Subject: [PATCH 03/14] converted urlfield to string in asset.template_url to allow to "self" --- breathecode/authenticate/flags.py | 2 +- breathecode/payments/flags.py | 2 +- .../0053_alter_asset_template_url.py | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 breathecode/registry/migrations/0053_alter_asset_template_url.py diff --git a/breathecode/authenticate/flags.py b/breathecode/authenticate/flags.py index a83082775..40a122249 100644 --- a/breathecode/authenticate/flags.py +++ b/breathecode/authenticate/flags.py @@ -2,7 +2,7 @@ from capyc.core.managers import feature -flags = feature.flags +flags = feature._flags @feature.availability("authenticate.set_google_credentials") diff --git a/breathecode/payments/flags.py b/breathecode/payments/flags.py index d54ce2483..1e5ee5f31 100644 --- a/breathecode/payments/flags.py +++ b/breathecode/payments/flags.py @@ -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") diff --git a/breathecode/registry/migrations/0053_alter_asset_template_url.py b/breathecode/registry/migrations/0053_alter_asset_template_url.py new file mode 100644 index 000000000..1d074a5b0 --- /dev/null +++ b/breathecode/registry/migrations/0053_alter_asset_template_url.py @@ -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, + ), + ), + ] From d8f6a69e17bd58d1f159adba86ee4a2897108ace Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Dec 2024 18:52:24 -0500 Subject: [PATCH 04/14] fixed flags = feature._flags issue --- breathecode/authenticate/flags.py | 2 +- breathecode/payments/flags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/authenticate/flags.py b/breathecode/authenticate/flags.py index 40a122249..a83082775 100644 --- a/breathecode/authenticate/flags.py +++ b/breathecode/authenticate/flags.py @@ -2,7 +2,7 @@ from capyc.core.managers import feature -flags = feature._flags +flags = feature.flags @feature.availability("authenticate.set_google_credentials") diff --git a/breathecode/payments/flags.py b/breathecode/payments/flags.py index 1e5ee5f31..d54ce2483 100644 --- a/breathecode/payments/flags.py +++ b/breathecode/payments/flags.py @@ -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") From 86280bf1e59aab885cfec4e6a43832d4e869b5a8 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Dec 2024 20:50:31 +0000 Subject: [PATCH 05/14] added link to connect academy to google --- breathecode/authenticate/admin.py | 33 +++++++++++++++++++++++++++---- breathecode/authenticate/urls.py | 4 +++- breathecode/authenticate/views.py | 24 +++++++++++++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/breathecode/authenticate/admin.py b/breathecode/authenticate/admin.py index d5491a409..76ea94c57 100644 --- a/breathecode/authenticate/admin.py +++ b/breathecode/authenticate/admin.py @@ -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): @@ -176,6 +176,11 @@ def github_login(self, obj): f"connect github" ) + def google_login(self, obj): + return format_html( + f"connect google" + ) + @admin.register(Role) class RoleAdmin(admin.ModelAdmin): @@ -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") @@ -490,16 +504,27 @@ def github_errors(self, obj): else: return format_html("No errors") - 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"connect owner" + f"connect github" ) + 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"connect google") + @admin.register(GoogleWebhook) class GoogleWebhookAdmin(admin.ModelAdmin): diff --git a/breathecode/authenticate/urls.py b/breathecode/authenticate/urls.py index 5e7c1a3c7..0a024578f 100644 --- a/breathecode/authenticate/urls.py +++ b/breathecode/authenticate/urls.py @@ -37,8 +37,8 @@ LoginView, LogoutView, MeInviteView, - MeProfileAcademyInvite, MemberView, + MeProfileAcademyInvite, PasswordResetView, ProfileInviteMeView, ProfileMePictureView, @@ -63,6 +63,7 @@ pick_password, receive_google_webhook, render_academy_invite, + render_google_connect, render_invite, render_user_invite, reset_password_view, @@ -154,6 +155,7 @@ # google authentication oath2.0 path("google/callback", save_google_token, name="google_callback"), path("google/", 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"), diff --git a/breathecode/authenticate/views.py b/breathecode/authenticate/views.py index ee50504cd..3419382c9 100644 --- a/breathecode/authenticate/views.py +++ b/breathecode/authenticate/views.py @@ -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 @@ -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): From 674304d15e5c86b1da0bdb7bc8060b4b58d85a26 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Dec 2024 20:52:33 +0000 Subject: [PATCH 06/14] added link to connect academy to google --- breathecode/authenticate/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/authenticate/admin.py b/breathecode/authenticate/admin.py index 76ea94c57..e8e44d731 100644 --- a/breathecode/authenticate/admin.py +++ b/breathecode/authenticate/admin.py @@ -178,7 +178,7 @@ def github_login(self, obj): def google_login(self, obj): return format_html( - f"connect google" + "connect google" ) From e0ce7e87fc651746a4aec0c4737563e40e7b07fd Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Thu, 12 Dec 2024 14:21:54 +0000 Subject: [PATCH 07/14] Added heading field to coursetranslation --- .../0092_coursetranslation_heading.py | 24 +++++++++++++++++++ breathecode/marketing/models.py | 3 +++ breathecode/marketing/serializers.py | 3 ++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 breathecode/marketing/migrations/0092_coursetranslation_heading.py diff --git a/breathecode/marketing/migrations/0092_coursetranslation_heading.py b/breathecode/marketing/migrations/0092_coursetranslation_heading.py new file mode 100644 index 000000000..9ae958395 --- /dev/null +++ b/breathecode/marketing/migrations/0092_coursetranslation_heading.py @@ -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, + ), + ), + ] diff --git a/breathecode/marketing/models.py b/breathecode/marketing/models.py index 5370aa91a..afea9145f 100644 --- a/breathecode/marketing/models.py +++ b/breathecode/marketing/models.py @@ -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.CharField( + max_length=160, 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( diff --git a/breathecode/marketing/serializers.py b/breathecode/marketing/serializers.py index ff0bafa83..bcbe41d8f 100644 --- a/breathecode/marketing/serializers.py +++ b/breathecode/marketing/serializers.py @@ -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 @@ -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 @@ -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): From 7e37299901f916b653d17c2fa5aadc3f036041db Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Thu, 12 Dec 2024 14:50:58 +0000 Subject: [PATCH 08/14] Changed field to TextField git push --- .../0093_alter_coursetranslation_heading.py | 24 +++++++++++++++++++ breathecode/marketing/models.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py diff --git a/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py b/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py new file mode 100644 index 000000000..8afe99a0c --- /dev/null +++ b/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py @@ -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=160, + null=True, + ), + ), + ] diff --git a/breathecode/marketing/models.py b/breathecode/marketing/models.py index afea9145f..e52a29201 100644 --- a/breathecode/marketing/models.py +++ b/breathecode/marketing/models.py @@ -867,7 +867,7 @@ 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.CharField( + heading = models.TextField( max_length=160, help_text="Heading that will be used in the landing page", default=None, null=True, blank=True ) description = models.TextField(max_length=400) From 95071d35cd4d9ba7762a2f6984c171b9fac96e60 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Thu, 12 Dec 2024 15:03:41 +0000 Subject: [PATCH 09/14] Added greater length --- .../migrations/0093_alter_coursetranslation_heading.py | 2 +- breathecode/marketing/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py b/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py index 8afe99a0c..e39a22fd0 100644 --- a/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py +++ b/breathecode/marketing/migrations/0093_alter_coursetranslation_heading.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): blank=True, default=None, help_text="Heading that will be used in the landing page", - max_length=160, + max_length=400, null=True, ), ), diff --git a/breathecode/marketing/models.py b/breathecode/marketing/models.py index e52a29201..773ec88ff 100644 --- a/breathecode/marketing/models.py +++ b/breathecode/marketing/models.py @@ -868,7 +868,7 @@ class CourseTranslation(models.Model): lang = models.CharField(max_length=5, validators=[validate_language_code]) title = models.CharField(max_length=60) heading = models.TextField( - max_length=160, help_text="Heading that will be used in the landing page", default=None, null=True, blank=True + 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) From cb893c6ddfbc6cb6c98f06d1a6a2a0d84d642597 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 13 Dec 2024 18:48:49 +0000 Subject: [PATCH 10/14] read and manage asset errors --- breathecode/authenticate/flags.py | 2 +- .../commands/create_academy_roles.py | 4 + breathecode/payments/flags.py | 2 +- breathecode/registry/serializers.py | 61 ++++++++++++ breathecode/registry/urls/v1.py | 3 + breathecode/registry/views.py | 99 +++++++++++++++++++ 6 files changed, 169 insertions(+), 2 deletions(-) diff --git a/breathecode/authenticate/flags.py b/breathecode/authenticate/flags.py index a83082775..40a122249 100644 --- a/breathecode/authenticate/flags.py +++ b/breathecode/authenticate/flags.py @@ -2,7 +2,7 @@ from capyc.core.managers import feature -flags = feature.flags +flags = feature._flags @feature.availability("authenticate.set_google_credentials") diff --git a/breathecode/authenticate/management/commands/create_academy_roles.py b/breathecode/authenticate/management/commands/create_academy_roles.py index 571c50b40..be54489c9 100644 --- a/breathecode/authenticate/management/commands/create_academy_roles.py +++ b/breathecode/authenticate/management/commands/create_academy_roles.py @@ -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", @@ -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", diff --git a/breathecode/payments/flags.py b/breathecode/payments/flags.py index d54ce2483..1e5ee5f31 100644 --- a/breathecode/payments/flags.py +++ b/breathecode/payments/flags.py @@ -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") diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 1b93d26a3..3e7d6305b 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -15,6 +15,7 @@ AssetAlias, AssetCategory, AssetComment, + AssetErrorLog, AssetKeyword, AssetTechnology, ContentVariable, @@ -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() @@ -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): diff --git a/breathecode/registry/urls/v1.py b/breathecode/registry/urls/v1.py index ad8ca7350..7182755f2 100644 --- a/breathecode/registry/urls/v1.py +++ b/breathecode/registry/urls/v1.py @@ -4,6 +4,7 @@ AcademyAssetActionView, AcademyAssetAliasView, AcademyAssetCommentView, + AcademyAssetErrorView, AcademyAssetOriginalityView, AcademyAssetSEOReportView, AcademyAssetView, @@ -47,6 +48,8 @@ path("academy/asset/image", AssetImageView.as_view()), path("academy/asset/comment", AcademyAssetCommentView.as_view()), path("academy/asset/comment/", AcademyAssetCommentView.as_view()), + path("academy/asset/error", AcademyAssetErrorView.as_view()), + path("academy/asset/error/", AcademyAssetErrorView.as_view()), path("academy/asset/action/", AcademyAssetActionView.as_view()), path("academy/asset/alias", AcademyAssetAliasView.as_view()), path("academy/asset/alias/", AcademyAssetAliasView.as_view()), diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index bd7f63675..37c99198c 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -56,6 +56,7 @@ from .serializers import ( AcademyAssetSerializer, AcademyCommentSerializer, + AcademyErrorSerializer, AssetAliasSerializer, AssetBigAndTechnologyPublishedSerializer, AssetBigSerializer, @@ -81,6 +82,7 @@ PostKeywordClusterSerializer, PostKeywordSerializer, PutAssetCommentSerializer, + PutAssetErrorSerializer, PUTCategorySerializer, PUTKeywordSerializer, SEOReportSerializer, @@ -1412,6 +1414,103 @@ def delete(self, request, comment_id=None, academy_id=None): return Response(None, status=status.HTTP_204_NO_CONTENT) +class AcademyAssetErrorView(APIView, GenerateLookupsMixin): + """ + List all snippets, or create a new snippet. + """ + + extensions = APIViewExtensions(sort="-created_at", paginate=True) + + @capable_of("read_asset_error") + def get(self, request, academy_id=None): + + handler = self.extensions(request) + # cache = handler.cache.get() + # if cache is not None: + # return cache + + items = AssetErrorLog.objects.filter(Q(asset__academy__id=academy_id) | Q(asset__isnull=True)) + lookup = {} + + if "asset" in self.request.GET: + param = self.request.GET.get("asset") + lookup["asset__slug__in"] = [p.lower() for p in param.split(",")] + + if "slug" in self.request.GET: + param = self.request.GET.get("slug") + lookup["slug__in"] = [p.lower() for p in param.split(",")] + + if "status" in self.request.GET: + param = self.request.GET.get("status") + lookup["status__in"] = [p.upper() for p in param.split(",")] + + if "asset_type" in self.request.GET: + param = self.request.GET.get("asset_type") + lookup["asset_type__in"] = [p.upper() for p in param.split(",")] + + items = items.filter(**lookup) + items = handler.queryset(items) + + serializer = AcademyErrorSerializer(items, many=True) + return handler.response(serializer.data) + + @capable_of("crud_asset_error") + def put(self, request, academy_id=None): + + data_list = request.data + if not isinstance(request.data, list): + data_list = [request.data] + + all_errors = [] + for data in data_list: + error_id = data.get("id") + if not error_id: + raise ValidationException("Missing error id") + + error = AssetErrorLog.objects.filter( + Q(id=error_id) & (Q(asset__academy__id=academy_id) | Q(asset__isnull=True)) + ).first() + if error is None: + raise ValidationException(f"This error with id {error_id} does not exist for this academy", 404) + + serializer = PutAssetErrorSerializer(error, data=data, context={"request": request, "academy": academy_id}) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + all_errors.append(serializer) + + for serializer in all_errors: + serializer.save() + + return Response([serializer.data for serializer in all_errors], status=status.HTTP_200_OK) + + @capable_of("crud_asset_error") + def delete(self, request, error_id=None, academy_id=None): + + lookups = self.generate_lookups(request, many_fields=["id"]) + if not lookups and not error_id: + raise ValidationException("provide arguments in the url", code=400, slug="without-lookups-and-error-id") + + if lookups and error_id: + raise ValidationException( + "error_id in url " "in bulk mode request, use querystring style instead", + code=400, + slug="lookups-and-error-id-together", + ) + + if error_id: + error = AssetErrorLog.objects.filter(id=error_id, asset__academy__id=academy_id).first() + if error is None: + raise ValidationException("This error does not exist", 404) + + error.delete() + + if lookups: + items = AssetErrorLog.objects.filter(**lookups) + items.delete() + + return Response(None, status=status.HTTP_204_NO_CONTENT) + + class AcademyAssetAliasView(APIView, GenerateLookupsMixin): """ List all snippets, or create a new snippet. From f57245166d5a05354ecf018c984af458b51b0d3f Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 13 Dec 2024 15:37:46 -0500 Subject: [PATCH 11/14] Update sql_keywords.json --- breathecode/sql_keywords.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/breathecode/sql_keywords.json b/breathecode/sql_keywords.json index 8fbb49c7e..8897ff4fb 100644 --- a/breathecode/sql_keywords.json +++ b/breathecode/sql_keywords.json @@ -379,7 +379,6 @@ "NOINHERIT", "DERIVED", "VALUES", - "WITH", "DESCRIPTOR", "STDDEV_SAMP", "COLLATION_CATALOG", @@ -452,7 +451,6 @@ "SUBLIST", "CONTAINS", "WRITE", - "END", "ROUTINE_SCHEMA", "ATTRIBUTES", "OIDS", From 12183ce0418d55a3214b85241e8cd6679aaca236 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 16 Dec 2024 12:23:10 -0500 Subject: [PATCH 12/14] Update serializers.py --- breathecode/registry/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 3e7d6305b..fcbd1c4ee 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -307,6 +307,7 @@ class AcademyAssetSerializer(AssetSerializer): published_at = serpy.Field() readme_updated_at = serpy.Field() authors_username = serpy.Field() + sort_priority = serpy.Field() requirements = serpy.Field() From 5ed5951e01175089dd86a73d91cd45866f589eea Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 16 Dec 2024 12:24:06 -0500 Subject: [PATCH 13/14] Update serializers.py --- breathecode/registry/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index fcbd1c4ee..3e7d6305b 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -307,7 +307,6 @@ class AcademyAssetSerializer(AssetSerializer): published_at = serpy.Field() readme_updated_at = serpy.Field() authors_username = serpy.Field() - sort_priority = serpy.Field() requirements = serpy.Field() From ed2bf4954d13e284f356d5e59ff218c178d71980 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 16 Dec 2024 16:55:31 -0500 Subject: [PATCH 14/14] Update views.py --- breathecode/registry/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 37c99198c..0e5fbd009 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -1449,6 +1449,11 @@ def get(self, request, academy_id=None): lookup["asset_type__in"] = [p.upper() for p in param.split(",")] items = items.filter(**lookup) + + like = request.GET.get("like", None) + if like is not None and like != "undefined" and like != "": + items = items.filter(Q(slug__icontains=slugify(like)) | Q(path__icontains=like)) + items = handler.queryset(items) serializer = AcademyErrorSerializer(items, many=True)