Skip to content

Commit

Permalink
Create Guru Form - Cloud (#168)
Browse files Browse the repository at this point in the history
Create Guru Form

---------

Co-authored-by: aralyekta <[email protected]>
  • Loading branch information
kursataktas and aralyekta authored Mar 6, 2025
1 parent acf92da commit 648dfa5
Show file tree
Hide file tree
Showing 19 changed files with 653 additions and 16 deletions.
3 changes: 3 additions & 0 deletions src/gurubase-backend/backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='')


1 change: 1 addition & 0 deletions src/gurubase-backend/backend/backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
12 changes: 10 additions & 2 deletions src/gurubase-backend/backend/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',)
4 changes: 4 additions & 0 deletions src/gurubase-backend/backend/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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'],
},
),
]
Original file line number Diff line number Diff line change
@@ -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,
),
]
18 changes: 18 additions & 0 deletions src/gurubase-backend/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

33 changes: 33 additions & 0 deletions src/gurubase-backend/backend/core/requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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) <[email protected]>",
"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}")
28 changes: 26 additions & 2 deletions src/gurubase-backend/backend/core/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -925,4 +926,27 @@ def leave_guild():
# Continue with deletion even if token revocation fails

if instance.api_key:
instance.api_key.delete()
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()
41 changes: 39 additions & 2 deletions src/gurubase-backend/backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
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)
55 changes: 55 additions & 0 deletions src/gurubase-frontend/src/app/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
}
67 changes: 67 additions & 0 deletions src/gurubase-frontend/src/app/guru/create/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<HeaderFooterWrap>
<GuruForm
source={source}
onSubmit={handleFormSubmit}
defaultEmail={userEmail}
isLoading={isLoading}
/>
</HeaderFooterWrap>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from "react";

const CommonContentLayout = ({ children, sidebar }) => {
return (
<main className="z-10 flex justify-center w-full flex-grow guru-sm:px-0 px-8">
<main className="z-10 flex justify-center w-full flex-grow guru-sm:px-0 px-8 polygon-fill">
<div
className={clsx(
"flex guru-sm:flex-col gap-6 guru-sm:gap-0 h-full w-full",
Expand Down
Loading

0 comments on commit 648dfa5

Please sign in to comment.