From 7c0607687fc422dfb566d5d44edbb930ab8610b2 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Fri, 29 Nov 2024 17:46:17 +0000 Subject: [PATCH 01/27] New educational status added --- ...065_alter_cohortuser_educational_status.py | 32 +++++++++++++++++++ breathecode/admissions/models.py | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 breathecode/admissions/migrations/0065_alter_cohortuser_educational_status.py diff --git a/breathecode/admissions/migrations/0065_alter_cohortuser_educational_status.py b/breathecode/admissions/migrations/0065_alter_cohortuser_educational_status.py new file mode 100644 index 000000000..811520fee --- /dev/null +++ b/breathecode/admissions/migrations/0065_alter_cohortuser_educational_status.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.2 on 2024-11-29 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("admissions", "0064_academy_legal_name"), + ] + + operations = [ + migrations.AlterField( + model_name="cohortuser", + name="educational_status", + field=models.CharField( + blank=True, + choices=[ + ("ACTIVE", "Active"), + ("POSTPONED", "Postponed"), + ("GRADUATED", "Graduated"), + ("SUSPENDED", "Suspended"), + ("DROPPED", "Dropped"), + ("NOT_COMPLETING", "Not Completing"), + ], + db_index=True, + default="ACTIVE", + max_length=15, + null=True, + ), + ), + ] diff --git a/breathecode/admissions/models.py b/breathecode/admissions/models.py index faa6b8cea..1e5cf7ad6 100644 --- a/breathecode/admissions/models.py +++ b/breathecode/admissions/models.py @@ -446,12 +446,14 @@ def __str__(self): SUSPENDED = "SUSPENDED" GRADUATED = "GRADUATED" DROPPED = "DROPPED" +NOT_COMPLETING = "NOT_COMPLETING" EDU_STATUS = ( (ACTIVE, "Active"), (POSTPONED, "Postponed"), (GRADUATED, "Graduated"), (SUSPENDED, "Suspended"), (DROPPED, "Dropped"), + (NOT_COMPLETING, "Not Completing"), ) From f2c15c0d6371aea365cd3779d8a9a536ac376af9 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:22:10 -0500 Subject: [PATCH 02/27] Update views.py --- breathecode/media/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/media/views.py b/breathecode/media/views.py index ecfa64df0..d9aa082a9 100644 --- a/breathecode/media/views.py +++ b/breathecode/media/views.py @@ -52,6 +52,8 @@ "image/jpg", "application/octet-stream", "application/x-pka", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ] From dc4c9e5f555d93e3923c481635d94a852683c801 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:24:12 -0500 Subject: [PATCH 03/27] Update settings.py --- breathecode/media/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/media/settings.py b/breathecode/media/settings.py index 4dc6650b8..bda46e3bc 100644 --- a/breathecode/media/settings.py +++ b/breathecode/media/settings.py @@ -41,6 +41,8 @@ class MediaSettings(TypedDict): "image/jpg", "application/octet-stream", "application/x-pka", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ] PROOF_OF_PAYMENT_MIME_ALLOWED = [ From 83a5de59aac4a364e57bfe5a71d64f0a5064312a Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:33:07 -0500 Subject: [PATCH 04/27] Update views.py --- breathecode/assignments/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index 408a50662..75d9afcf4 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -64,6 +64,8 @@ "application/octet-stream", "application/json", "text/plain", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ] IMAGES_MIME_ALLOW = ["image/png", "image/svg+xml", "image/jpeg", "image/jpg"] From 2da39fa2a6b3229b2c19ed87ca3f23e96ece6206 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Fri, 29 Nov 2024 20:17:10 +0000 Subject: [PATCH 05/27] Add more characters to mime model --- .../0020_alter_userattachment_mime.py | 18 ++++++++++++++++++ breathecode/assignments/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 breathecode/assignments/migrations/0020_alter_userattachment_mime.py diff --git a/breathecode/assignments/migrations/0020_alter_userattachment_mime.py b/breathecode/assignments/migrations/0020_alter_userattachment_mime.py new file mode 100644 index 000000000..39b44323f --- /dev/null +++ b/breathecode/assignments/migrations/0020_alter_userattachment_mime.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2024-11-29 20:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignments", "0019_repositorydeletionorder_starts_transferring_at"), + ] + + operations = [ + migrations.AlterField( + model_name="userattachment", + name="mime", + field=models.CharField(max_length=120), + ), + ] diff --git a/breathecode/assignments/models.py b/breathecode/assignments/models.py index ff88c22ca..2a66a33b9 100644 --- a/breathecode/assignments/models.py +++ b/breathecode/assignments/models.py @@ -13,7 +13,7 @@ class UserAttachment(models.Model): slug = models.SlugField(max_length=150, unique=True) name = models.CharField(max_length=150) - mime = models.CharField(max_length=60) + mime = models.CharField(max_length=120) url = models.URLField(max_length=255) hash = models.CharField(max_length=64) From c1a6ee334464651c73f94238fb730c015532d0d0 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 2 Dec 2024 17:13:13 +0000 Subject: [PATCH 06/27] added support for asset versionin when searching for an asser by repo --- breathecode/registry/models.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index c6c46d54e..92f93c6b7 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -865,6 +865,20 @@ def get_by_slug(asset_slug, request=None, asset_type=None): @staticmethod def get_by_github_url(github_url): + """ + Retrieve an Asset object based on a GitHub URL. + This method parses the provided GitHub URL to extract the organization name, + repository name, and optionally the branch name. It then searches for an Asset + object whose `readme_url` contains the relevant GitHub URL components. + Args: + github_url (str): The GitHub URL to parse and search for. + Returns: + Asset: The first Asset object that matches the parsed GitHub URL components, + or None if no matching Asset is found. + Raises: + ValueError: If the provided URL is not a valid GitHub URL or does not contain + the necessary components. + """ parsed_url = urlparse(github_url) if parsed_url.netloc != "github.com": raise ValueError("Invalid GitHub URL") @@ -874,7 +888,21 @@ def get_by_github_url(github_url): raise ValueError("Invalid GitHub URL") org_name, repo_name = path_parts[:2] - asset = Asset.objects.filter(readme_url__icontains=f"github.com/{org_name}/{repo_name}").first() + branch_name = None + + # 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] + + if branch_name: + asset = Asset.objects.filter( + readme_url__icontains=f"github.com/{org_name}/{repo_name}/blob/{branch_name}" + ).first() + else: + asset = Asset.objects.filter(readme_url__icontains=f"github.com/{org_name}/{repo_name}").first() return asset From 2f3bfacc9a34ac66ba7a11a4672ffe23db9976e9 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 2 Dec 2024 18:07:24 +0000 Subject: [PATCH 07/27] added support for asset versionin when searching for an asser by repo --- breathecode/registry/models.py | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 92f93c6b7..92a960d47 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -870,6 +870,10 @@ def get_by_github_url(github_url): This method parses the provided GitHub URL to extract the organization name, repository name, and optionally the branch name. It then searches for an Asset object whose `readme_url` contains the relevant GitHub URL components. + + If a branch name is specified, it will return the Asset with the highest version + number in the branch. + Args: github_url (str): The GitHub URL to parse and search for. Returns: @@ -898,9 +902,47 @@ def get_by_github_url(github_url): branch_name = path_parts[blob_index + 1] if branch_name: - asset = Asset.objects.filter( - readme_url__icontains=f"github.com/{org_name}/{repo_name}/blob/{branch_name}" - ).first() + + def compare_versions(version1, version2): + v1_parts = list(map(int, version1.split("."))) + v2_parts = list(map(int, version2.split("."))) + + # Compare each part of the version + for v1, v2 in zip(v1_parts, v2_parts): + if v1 > v2: + return 1 + elif v1 < v2: + return -1 + + # If all parts are equal, compare lengths + if len(v1_parts) > len(v2_parts): + return 1 + elif len(v1_parts) < len(v2_parts): + return -1 + + return 0 + + original_version = branch_name + original_major_version = int(original_version.split(".")[0]) + + assets = Asset.objects.filter(readme_url__icontains=f"github.com/{org_name}/{repo_name}/blob/") + + # Extract and compare versions to find the highest one + highest_version = None + highest_asset = None + version_pattern = re.compile(r"/blob/(\d+(\.\d+)*)/README.md") + + for asset in assets: + match = version_pattern.search(asset.readme_url or "") + if match: + version = match.group(1) + major_version = int(version.split(".")[0]) + if major_version == original_major_version: + if highest_version is None or compare_versions(version, highest_version) > 0: + highest_version = version + highest_asset = asset + + return highest_asset else: asset = Asset.objects.filter(readme_url__icontains=f"github.com/{org_name}/{repo_name}").first() return asset From 47bac485ec043faa65ed63d9107b6af07a319645 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 2 Dec 2024 14:45:42 -0500 Subject: [PATCH 08/27] Update models.py --- breathecode/registry/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 92a960d47..1e701c91f 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -922,7 +922,11 @@ def compare_versions(version1, version2): return 0 - original_version = branch_name + pattern = r"^v\d+\.\d+$" + if not bool(re.match(branch_name, string)): + 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]) assets = Asset.objects.filter(readme_url__icontains=f"github.com/{org_name}/{repo_name}/blob/") From ce089b8591ff8229020aaa88d8297e3986a878a1 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 2 Dec 2024 21:01:49 +0000 Subject: [PATCH 09/27] 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 47ba086364b3a99e4806ef02810f97786b648f9a Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 4 Dec 2024 15:21:31 -0500 Subject: [PATCH 10/27] Update tasks.py --- breathecode/registry/tasks.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 7011e06ef..162fac918 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -614,47 +614,45 @@ def async_build_asset_context(asset_id): lang = asset.lang or asset.category.lang lang_name = LANG_MAP.get(lang, lang) - context = f"This {asset.asset_type} about {asset.title} is written in {lang_name}. " + context = f"This {asset.asset_type} called '{asset.title}' is written in {lang_name}. " translations = ", ".join([x.title for x in asset.all_translations.all()]) if translations: context = context[:-2] - context += f", and it has the following translations: {translations}. " + context += f"It has the following translation languages: {translations}. " if asset.solution_url: context = context[:-2] - context += f", and it has a solution code this link is: {asset.solution_url}. " + context += f"It has a solution published on this link: {asset.solution_url}. " if asset.solution_video_url: context = context[:-2] - context += f", and it has a video solution this link is {asset.solution_video_url}. " - - context += f"It's category related is (what type of skills the student will get) {asset.category.title}. " + context += f"It also has a video solution, published on this link is {asset.solution_video_url}. " technologies = ", ".join([x.title for x in asset.technologies.filter(Q(lang=lang) | Q(lang=None))]) if technologies: - context += f"This asset is about the following technologies: {technologies}. " + context += f"This {asset.asset_type} is about the following technologies: {technologies}. " if asset.external: - context += "This asset is external, which means it opens outside 4geeks. " + context += f"This {asset.asset_type} opens outside of 4Geeks.com. " if asset.interactive: - context += "This asset opens on LearnPack so it has a step-by-step of the exercises that you should follow. " + context += f"This {asset.asset_type} uses LearnPack with an interactive step-by-step tutorial the student must follow. " if asset.gitpod: context += ( - f"This {asset.asset_type} can be opened both locally or with click and code (This " - "way you don't have to install anything and it will open automatically on gitpod or github codespaces). " + f"This {asset.asset_type} can be opened both locally on your computer or in the cloud using our 'click and learn' technology (This " + "way you don't have to install anything, and it will open automatically in the cloud). " ) if asset.interactive == True and asset.with_video == True: - context += f"This {asset.asset_type} has videos on each step. " + context += f"This {asset.asset_type} has video solutions and/or explanations on each step. " if asset.interactive == True and asset.with_solutions == True: - context += f"This {asset.asset_type} has a code solution on each step. " + context += f"This {asset.asset_type} has a model solution for each coding challenge on each step. " - if asset.duration: - context += f"This {asset.asset_type} will last {asset.duration} hours. " + if asset.duration and asset.duration > 0: + context += f"This {asset.asset_type} will takes {asset.duration} hours to complete (in average). " if asset.difficulty: context += f"Its difficulty is considered as {asset.difficulty}. " @@ -667,7 +665,7 @@ def async_build_asset_context(asset_id): if asset.asset_type == "PROJECT" and asset.delivery_instructions and asset.delivery_formats: context += ( - f"This project should be delivered by adding a file of one of these types: {asset.delivery_formats}. " + f"This project should be delivered by adding at least one file of one of these types: {asset.delivery_formats}. " ) if asset.asset_type == "PROJECT" and asset.delivery_regex_url: From 0dec2d32133c97181c347eb2e68dd0b1642b1c5d Mon Sep 17 00:00:00 2001 From: Jeferson De Freitas Pinto Date: Thu, 5 Dec 2024 10:18:56 -0500 Subject: [PATCH 11/27] Security update --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 32905d6e0..b3e06402b 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.12.7 +python-3.12.8 From 45e9e160dd18da7bd61d69346160d0e08dad998d Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Mon, 9 Dec 2024 16:46:26 +0000 Subject: [PATCH 12/27] add academy to cohort user hook serializer --- breathecode/admissions/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/admissions/serializers.py b/breathecode/admissions/serializers.py index e322a2b64..9cd471454 100644 --- a/breathecode/admissions/serializers.py +++ b/breathecode/admissions/serializers.py @@ -452,6 +452,7 @@ class CohortUserHookSerializer(serpy.Serializer): id = serpy.Field() user = UserSerializer() cohort = GetSmallCohortSerializer() + academy = GetSmallAcademySerializer() role = serpy.Field() finantial_status = serpy.Field() educational_status = serpy.Field() From c6b3293b4ff1e4b022616029a5c1ae5f978c3d2e Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:27:43 -0400 Subject: [PATCH 13/27] Revert "add academy to cohort user hook serializer" --- breathecode/admissions/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/breathecode/admissions/serializers.py b/breathecode/admissions/serializers.py index 9cd471454..e322a2b64 100644 --- a/breathecode/admissions/serializers.py +++ b/breathecode/admissions/serializers.py @@ -452,7 +452,6 @@ class CohortUserHookSerializer(serpy.Serializer): id = serpy.Field() user = UserSerializer() cohort = GetSmallCohortSerializer() - academy = GetSmallAcademySerializer() role = serpy.Field() finantial_status = serpy.Field() educational_status = serpy.Field() From dc81b9ebc9b6bd01275bad797be0efa50ed74424 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 9 Dec 2024 15:59:37 -0500 Subject: [PATCH 14/27] add migration --- ...ydeletionorder_repository_name_and_more.py | 33 +++++++++++++++++++ breathecode/assignments/models.py | 8 ++--- 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 breathecode/assignments/migrations/0021_alter_repositorydeletionorder_repository_name_and_more.py diff --git a/breathecode/assignments/migrations/0021_alter_repositorydeletionorder_repository_name_and_more.py b/breathecode/assignments/migrations/0021_alter_repositorydeletionorder_repository_name_and_more.py new file mode 100644 index 000000000..4605bc8f5 --- /dev/null +++ b/breathecode/assignments/migrations/0021_alter_repositorydeletionorder_repository_name_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2024-12-09 20:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignments", "0020_alter_userattachment_mime"), + ] + + operations = [ + migrations.AlterField( + model_name="repositorydeletionorder", + name="repository_name", + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name="repositorydeletionorder", + name="repository_user", + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name="repositorywhitelist", + name="repository_name", + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name="repositorywhitelist", + name="repository_user", + field=models.CharField(max_length=256), + ), + ] diff --git a/breathecode/assignments/models.py b/breathecode/assignments/models.py index 2a66a33b9..142588d82 100644 --- a/breathecode/assignments/models.py +++ b/breathecode/assignments/models.py @@ -268,8 +268,8 @@ def __init__(self, *args, **kwargs): status = models.CharField(max_length=15, choices=Status, default=Status.PENDING) status_text = models.TextField(default=None, null=True, blank=True) - repository_user = models.CharField(max_length=100) - repository_name = models.CharField(max_length=100) + repository_user = models.CharField(max_length=256) + repository_name = models.CharField(max_length=256) starts_transferring_at = models.DateTimeField(default=None, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) @@ -296,8 +296,8 @@ class RepositoryWhiteList(models.Model): Provider = Provider provider = models.CharField(max_length=15, choices=Provider, default=Provider.GITHUB) - repository_user = models.CharField(max_length=100) - repository_name = models.CharField(max_length=100) + repository_user = models.CharField(max_length=256) + repository_name = models.CharField(max_length=256) created_at = models.DateTimeField(auto_now_add=True, editable=False) From 14e29cb9aec4b31aa86f4b9af5bb3e50500fa958 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Dec 2024 16:18:57 -0500 Subject: [PATCH 15/27] Update tasks.py --- breathecode/registry/tasks.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 162fac918..e29ea0b88 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -83,7 +83,13 @@ def async_pull_project_dependencies(asset_slug): target_asset = asset if asset.template_url is not None and asset.template_url != "": - target_asset = Asset.get_by_github_url(asset.template_url) + + # To avoid legacy issues we have to mark assets.template_url as "self" when no template is needed + if asset.template_url == "self": + target_asset = asset + else: + target_asset = Asset.get_by_github_url(asset.template_url) + if target_asset is None: raise Exception( f"Asset {asset_slug} template {asset.template_url} not found in the database as another asset" From f82e57d74a763b9e0a24280072428e63b15dba5a Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 9 Dec 2024 16:26:57 -0500 Subject: [PATCH 16/27] update deletion schedule --- .../management/commands/schedule_repository_deletions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assignments/management/commands/schedule_repository_deletions.py b/breathecode/assignments/management/commands/schedule_repository_deletions.py index 7d981fe66..2351e8cb2 100644 --- a/breathecode/assignments/management/commands/schedule_repository_deletions.py +++ b/breathecode/assignments/management/commands/schedule_repository_deletions.py @@ -17,7 +17,7 @@ class Command(BaseCommand): help = "Clean data from marketing module" - github_url_pattern = re.compile(r"https?:\/\/github\.com\/(?P[^\/]+)\/(?P[^\/\s]+)\/?") + github_url_pattern = re.compile(r"https?://github\.com/(?P[^/\s]+)/(?P[^/\s]+)/?") def handle(self, *args, **options): self.fill_whitelist() From e79564034a499fe386111354b1504cd7037a2130 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 9 Dec 2024 16:53:05 -0500 Subject: [PATCH 17/27] change timeout --- breathecode/services/github.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/services/github.py b/breathecode/services/github.py index 0f6e6199f..dce5b2482 100644 --- a/breathecode/services/github.py +++ b/breathecode/services/github.py @@ -59,7 +59,7 @@ def _call(self, method_name, action_name, params=None, json=None): } url = self.HOST + action_name - resp = requests.request(method=method_name, url=url, headers=self.headers, params=params, json=json, timeout=2) + resp = requests.request(method=method_name, url=url, headers=self.headers, params=params, json=json, timeout=20) if resp.status_code >= 200 and resp.status_code < 300: if method_name in ["DELETE", "HEAD"]: From 499f71ff815a0ca86ee6bf62dad360792f204ddf Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Dec 2024 23:34:42 +0000 Subject: [PATCH 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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",