From 648dfa596796c9d9968ef9a6a08d3da1449b49f8 Mon Sep 17 00:00:00 2001 From: Kursat Aktas Date: Thu, 6 Mar 2025 15:43:03 +0300 Subject: [PATCH] Create Guru Form - Cloud (#168) Create Guru Form --------- Co-authored-by: aralyekta --- .../backend/backend/settings.py | 3 + src/gurubase-backend/backend/backend/urls.py | 1 + src/gurubase-backend/backend/core/admin.py | 12 +- .../backend/core/exceptions.py | 4 + .../core/migrations/0058_gurucreationform.py | 29 +++ .../0059_gurucreationform_source.py | 19 ++ src/gurubase-backend/backend/core/models.py | 18 ++ .../backend/core/requester.py | 33 +++ src/gurubase-backend/backend/core/signals.py | 28 ++- src/gurubase-backend/backend/core/views.py | 41 +++- src/gurubase-frontend/src/app/actions.js | 55 +++++ .../src/app/guru/create/page.js | 67 +++++ .../src/components/CommonContentLayout.js | 2 +- .../src/components/GuruEditPageSidebar.jsx | 28 ++- .../src/components/GuruForm/GuruForm.jsx | 229 ++++++++++++++++++ .../components/GuruForm/HeaderFooterWrap.js | 15 ++ .../src/components/GuruList/index.js | 12 +- .../src/components/OtherGurus/index.js | 71 +++++- .../src/components/ui/button.jsx | 2 + 19 files changed, 653 insertions(+), 16 deletions(-) create mode 100644 src/gurubase-backend/backend/core/migrations/0058_gurucreationform.py create mode 100644 src/gurubase-backend/backend/core/migrations/0059_gurucreationform_source.py create mode 100644 src/gurubase-frontend/src/app/guru/create/page.js create mode 100644 src/gurubase-frontend/src/components/GuruForm/GuruForm.jsx create mode 100644 src/gurubase-frontend/src/components/GuruForm/HeaderFooterWrap.js diff --git a/src/gurubase-backend/backend/backend/settings.py b/src/gurubase-backend/backend/backend/settings.py index 73d2c299..421b0408 100644 --- a/src/gurubase-backend/backend/backend/settings.py +++ b/src/gurubase-backend/backend/backend/settings.py @@ -443,4 +443,7 @@ WEBSHARE_TOKEN = config('WEBSHARE_TOKEN', default='') GITHUB_FILE_BATCH_SIZE = config('GITHUB_FILE_BATCH_SIZE', default=100, cast=int) CRAWL_INACTIVE_THRESHOLD_SECONDS = config('CRAWL_INACTIVE_THRESHOLD_SECONDS', default=7, cast=int) +ADMIN_EMAIL = config('ADMIN_EMAIL', default='') +MAILGUN_API_KEY = config('MAILGUN_API_KEY', default='') + diff --git a/src/gurubase-backend/backend/backend/urls.py b/src/gurubase-backend/backend/backend/urls.py index 7f706d9b..b1e99c99 100644 --- a/src/gurubase-backend/backend/backend/urls.py +++ b/src/gurubase-backend/backend/backend/urls.py @@ -98,6 +98,7 @@ from django.views.decorators.cache import cache_page sitemaps = get_sitemaps() urlpatterns += [ + path('guru_types/submit_form/', core_views.submit_guru_creation_form, name='submit_guru_creation_form'), path( "sitemap.xml", cache_page(3600)(views.index), diff --git a/src/gurubase-backend/backend/core/admin.py b/src/gurubase-backend/backend/core/admin.py index eb5f53bb..0410618d 100644 --- a/src/gurubase-backend/backend/core/admin.py +++ b/src/gurubase-backend/backend/core/admin.py @@ -17,10 +17,11 @@ Summarization, SummaryQuestionGeneration, Settings, - LLMEvalResult, Thread, + LLMEvalResult, + Thread, WidgetId, GithubFile, - ) + GuruCreationForm) from django.utils.html import format_html import logging from django.contrib.admin import SimpleListFilter @@ -458,3 +459,10 @@ class CrawlStateAdmin(admin.ModelAdmin): list_filter = ('status',) search_fields = ['id', 'url'] ordering = ('-id',) + +@admin.register(GuruCreationForm) +class GuruCreationFormAdmin(admin.ModelAdmin): + list_display = ['id', 'notified', 'source', 'email', 'github_repo', 'docs_url', 'date_created', 'date_updated'] + search_fields = ['id', 'email', 'github_repo', 'docs_url', 'use_case'] + list_filter = ('notified', 'source') + ordering = ('-id',) diff --git a/src/gurubase-backend/backend/core/exceptions.py b/src/gurubase-backend/backend/core/exceptions.py index 9754f9f4..737dac6c 100644 --- a/src/gurubase-backend/backend/core/exceptions.py +++ b/src/gurubase-backend/backend/core/exceptions.py @@ -53,3 +53,7 @@ class PermissionError(Exception): class NotFoundError(Exception): """Exception raised for not found errors.""" pass + +class ThrottlingException(Exception): + """Exception raised for throttling errors.""" + pass \ No newline at end of file diff --git a/src/gurubase-backend/backend/core/migrations/0058_gurucreationform.py b/src/gurubase-backend/backend/core/migrations/0058_gurucreationform.py new file mode 100644 index 00000000..268f1531 --- /dev/null +++ b/src/gurubase-backend/backend/core/migrations/0058_gurucreationform.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.18 on 2025-03-05 13:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0057_widgetid_is_wildcard'), + ] + + operations = [ + migrations.CreateModel( + name='GuruCreationForm', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('github_repo', models.URLField(max_length=2000)), + ('docs_url', models.URLField(max_length=2000)), + ('use_case', models.TextField(blank=True, null=True)), + ('notified', models.BooleanField(default=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['-date_created'], + }, + ), + ] diff --git a/src/gurubase-backend/backend/core/migrations/0059_gurucreationform_source.py b/src/gurubase-backend/backend/core/migrations/0059_gurucreationform_source.py new file mode 100644 index 00000000..01bedfe9 --- /dev/null +++ b/src/gurubase-backend/backend/core/migrations/0059_gurucreationform_source.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.18 on 2025-03-05 13:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0058_gurucreationform'), + ] + + operations = [ + migrations.AddField( + model_name='gurucreationform', + name='source', + field=models.CharField(default='/', max_length=50), + preserve_default=False, + ), + ] diff --git a/src/gurubase-backend/backend/core/models.py b/src/gurubase-backend/backend/core/models.py index 2dee75a6..8497bb4c 100644 --- a/src/gurubase-backend/backend/core/models.py +++ b/src/gurubase-backend/backend/core/models.py @@ -1656,3 +1656,21 @@ class Source(models.TextChoices): def __str__(self): return f"Crawl {self.id} - {self.url} ({self.status}) - {self.guru_type.name} - {self.user.email if self.user else 'selfhosted'}" + +class GuruCreationForm(models.Model): + + email = models.EmailField() + github_repo = models.URLField(max_length=2000) + docs_url = models.URLField(max_length=2000) + use_case = models.TextField(blank=True, null=True) + notified = models.BooleanField(default=False) + source = models.CharField(max_length=50) + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.email} - {self.github_repo}" + + class Meta: + ordering = ['-date_created'] + diff --git a/src/gurubase-backend/backend/core/requester.py b/src/gurubase-backend/backend/core/requester.py index cce89335..44a94bda 100644 --- a/src/gurubase-backend/backend/core/requester.py +++ b/src/gurubase-backend/backend/core/requester.py @@ -19,6 +19,7 @@ from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator logging.getLogger("openai").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) +from core.exceptions import ThrottlingException logger = logging.getLogger(__name__) @@ -816,3 +817,35 @@ def get_proxies(self): return [] return response.json() + + +class MailgunRequester(): + def __init__(self): + self.base_url = "https://api.mailgun.net/v3" + self.api_key = settings.MAILGUN_API_KEY + + def send_email(self, to, subject, body): + data = { + "from": "Anteon (formerly Ddosify) ", + "to": to, + "subject": subject, + "text": body + } + try: + email_response = requests.post(url="https://api.mailgun.net/v3/mail.getanteon.com/messages", + auth=("api", self.api_key), + data=data) + + if not email_response.ok: + if email_response.status_code >= 400: + raise ThrottlingException(f"Email send error: {email_response.text} - Status Code: {email_response.status_code}") + raise Exception(f"Email send error: {email_response.text} - Status Code: {email_response.status_code}") + + logger.info(f"Email sent to: {to}. Subject: {subject}") + except ThrottlingException as e: + exception_code = "E-101" + logger.fatal(f"Can not send email. Email: {to}. Subject: {subject} Code: {exception_code}") + raise + except Exception as e: + exception_code = "E-100" + logger.fatal(f"Can not send email. Email: {to}. Subject: {subject} Code: {exception_code}") diff --git a/src/gurubase-backend/backend/core/signals.py b/src/gurubase-backend/backend/core/signals.py index a6870035..677ac9dc 100644 --- a/src/gurubase-backend/backend/core/signals.py +++ b/src/gurubase-backend/backend/core/signals.py @@ -14,7 +14,8 @@ from django.core.validators import URLValidator from urllib.parse import urlparse import secrets -from .models import Integration, APIKey +from .models import Integration, APIKey, GuruCreationForm +from .requester import MailgunRequester logger = logging.getLogger(__name__) @@ -925,4 +926,27 @@ def leave_guild(): # Continue with deletion even if token revocation fails if instance.api_key: - instance.api_key.delete() \ No newline at end of file + instance.api_key.delete() + +@receiver(post_save, sender=GuruCreationForm) +def notify_admin_on_guru_creation_form_submission(sender, instance, **kwargs): + if instance.notified: + return + + # Send email notification + subject = 'New Guru Creation Request' + message = f""" +A new guru creation request has been submitted: + +Email: {instance.email} +Documentation URL: {instance.docs_url} +GitHub Repository: {instance.github_repo} +Use Case: {instance.use_case} +Source: {instance.source} + +View this request in the admin panel. +""" + + MailgunRequester().send_email(settings.ADMIN_EMAIL, subject, message) + instance.notified = True + instance.save() \ No newline at end of file diff --git a/src/gurubase-backend/backend/core/views.py b/src/gurubase-backend/backend/core/views.py index 38555245..f8b8ea35 100644 --- a/src/gurubase-backend/backend/core/views.py +++ b/src/gurubase-backend/backend/core/views.py @@ -24,7 +24,7 @@ from core.serializers import WidgetIdSerializer, BingeSerializer, DataSourceSerializer, GuruTypeSerializer, GuruTypeInternalSerializer, QuestionCopySerializer, FeaturedDataSourceSerializer, APIKeySerializer, DataSourceAPISerializer, SettingsSerializer from core.auth import auth, follow_up_examples_auth, jwt_auth, combined_auth, stream_combined_auth, api_key_auth from core.gcp import replace_media_root_with_localhost, replace_media_root_with_nginx_base_url -from core.models import CrawlState, FeaturedDataSource, Question, ContentPageStatistics, WidgetId, Binge, DataSource, GuruType, Integration, Thread, APIKey +from core.models import CrawlState, FeaturedDataSource, Question, ContentPageStatistics, WidgetId, Binge, DataSource, GuruType, Integration, Thread, APIKey, GuruCreationForm from accounts.models import User from core.utils import ( # Authentication & validation @@ -2850,4 +2850,41 @@ def get_crawl_status_api(request, guru_slug, crawl_id): except Exception as e: return Response({'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST) - return Response(data, status=return_status) \ No newline at end of file + return Response(data, status=return_status) + +@api_view(['POST']) +@combined_auth +def submit_guru_creation_form(request): + """ + Handle submission of guru creation forms. + """ + try: + email = request.data.get('email') + github_repo = request.data.get('github_repo') + docs_url = request.data.get('docs_url') + use_case = request.data.get('use_case') + source = request.data.get('source', 'unknown') + + if not all([email, docs_url]): + return Response({ + 'error': 'Missing required fields. Please provide email, and documentation root url.' + }, status=status.HTTP_400_BAD_REQUEST) + + # Create form submission + GuruCreationForm.objects.create( + email=email, + github_repo=github_repo, + docs_url=docs_url, + use_case=use_case, + source=source + ) + + return Response({ + 'message': 'Your guru creation request has been submitted successfully.' + }, status=status.HTTP_201_CREATED) + + except Exception as e: + logger.error(f'Error processing guru creation form: {e}', exc_info=True) + return Response({ + 'error': 'An error occurred while processing your request.' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/src/gurubase-frontend/src/app/actions.js b/src/gurubase-frontend/src/app/actions.js index 8035a714..ca0d929b 100644 --- a/src/gurubase-frontend/src/app/actions.js +++ b/src/gurubase-frontend/src/app/actions.js @@ -1191,3 +1191,58 @@ export async function getCrawlStatus(crawlId, guruSlug) { }); } } + +export async function submitGuruCreationForm(formData) { + try { + const session = await getUserSession(); + const payload = { + email: formData.get("email"), + github_repo: formData.get("github_repo"), + docs_url: formData.get("docs_url"), + use_case: formData.get("use_case"), + source: formData.get("source") + }; + + if (session?.user) { + // Authenticated request + const response = await makeAuthenticatedRequest( + `${process.env.NEXT_PUBLIC_BACKEND_FETCH_URL}/guru_types/submit_form/`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + } + ); + + if (!response) return null; + return await response.json(); + } else { + // Public request + const response = await makePublicRequest( + `${process.env.NEXT_PUBLIC_BACKEND_FETCH_URL}/guru_types/submit_form/`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + } + ); + + if (!response) return null; + return await response.json(); + } + } catch (error) { + return handleRequestError(error, { + context: "submitGuruCreationForm" + }); + } +} + +export async function getCurrentUserEmail() { + try { + const session = await getUserSession(); + return session?.user?.email || ""; + } catch (error) { + console.error("Error fetching user email:", error); + return ""; + } +} diff --git a/src/gurubase-frontend/src/app/guru/create/page.js b/src/gurubase-frontend/src/app/guru/create/page.js new file mode 100644 index 00000000..2fa94993 --- /dev/null +++ b/src/gurubase-frontend/src/app/guru/create/page.js @@ -0,0 +1,67 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { submitGuruCreationForm, getCurrentUserEmail } from "@/app/actions"; +import GuruForm from "@/components/GuruForm/GuruForm"; +import HeaderFooterWrap from "@/components/GuruForm/HeaderFooterWrap"; +import { redirect } from "next/navigation"; +import { useEffect, useState } from "react"; + +export default function UserInfoPage() { + const searchParams = useSearchParams(); + const source = searchParams.get("source") || "unknown"; + const [userEmail, setUserEmail] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const isSelfHosted = process.env.NEXT_PUBLIC_NODE_ENV === "selfhosted"; + + useEffect(() => { + const fetchUserEmail = async () => { + try { + const email = await getCurrentUserEmail(); + setUserEmail(email); + } finally { + setIsLoading(false); + } + }; + + fetchUserEmail(); + }, []); + + // Mock function to handle form submission + const handleFormSubmit = async (data) => { + try { + const formData = new FormData(); + formData.append("email", data.email); + formData.append("github_repo", data.githubLink); + formData.append("docs_url", data.docsRootUrl); + formData.append("use_case", data.useCase); + formData.append("source", data.source); + + const response = await submitGuruCreationForm(formData); + + if (response?.error) { + throw new Error(response.message); + } + + return response; + } catch (error) { + // console.error("Error submitting form:", error); + throw error; + } + }; + + if (isSelfHosted) { + redirect("not-found"); + } + + return ( + + + + ); +} diff --git a/src/gurubase-frontend/src/components/CommonContentLayout.js b/src/gurubase-frontend/src/components/CommonContentLayout.js index 900596f0..c045d854 100644 --- a/src/gurubase-frontend/src/components/CommonContentLayout.js +++ b/src/gurubase-frontend/src/components/CommonContentLayout.js @@ -3,7 +3,7 @@ import React from "react"; const CommonContentLayout = ({ children, sidebar }) => { return ( -
+