adding details

This commit is contained in:
abhishekpythons
2026-05-19 04:38:02 +05:30
parent 246b5381ce
commit a16520420d
63 changed files with 6123 additions and 1 deletions

7
backend/.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Copy this file to `.env` and fill in real values.
DJANGO_SECRET_KEY=replace-me-with-a-50-char-random-string
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=postgres://risingcompute:change-this-password@127.0.0.1:5432/risingcompute
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
NOTIFY_EMAIL_TO=contact@risingcompute.in

0
backend/api/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

76
backend/api/admin.py Normal file
View File

@@ -0,0 +1,76 @@
from django.contrib import admin
from .models import (
BlogPost,
ContactSubmission,
Founder,
JobApplication,
JobOpening,
NewsletterSignup,
Product,
)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ("name", "category", "is_published", "sort_order", "updated_at")
list_filter = ("category", "is_published")
search_fields = ("name", "tagline", "slug")
prepopulated_fields = {"slug": ("name",)}
@admin.register(Founder)
class FounderAdmin(admin.ModelAdmin):
list_display = ("name", "role", "is_published", "sort_order")
list_filter = ("is_published",)
search_fields = ("name", "role")
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
list_display = ("title", "category", "author_name", "is_published", "published_at")
list_filter = ("category", "is_published")
search_fields = ("title", "excerpt", "body")
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "published_at"
@admin.register(JobOpening)
class JobOpeningAdmin(admin.ModelAdmin):
list_display = ("title", "location", "employment_type", "is_open", "posted_at")
list_filter = ("location", "is_open")
search_fields = ("title", "description")
prepopulated_fields = {"slug": ("title",)}
@admin.register(ContactSubmission)
class ContactSubmissionAdmin(admin.ModelAdmin):
list_display = ("name", "email", "interest", "company", "created_at")
list_filter = ("interest", "created_at")
search_fields = ("name", "email", "company", "message")
readonly_fields = (
"name",
"email",
"company",
"role",
"country",
"interest",
"message",
"referrer",
"user_agent",
"ip_address",
"created_at",
)
@admin.register(NewsletterSignup)
class NewsletterSignupAdmin(admin.ModelAdmin):
list_display = ("email", "source", "confirmed", "created_at")
list_filter = ("source", "confirmed")
search_fields = ("email",)
@admin.register(JobApplication)
class JobApplicationAdmin(admin.ModelAdmin):
list_display = ("name", "email", "role_applied_for", "created_at")
search_fields = ("name", "email", "role_applied_for")

7
backend/api/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"
verbose_name = "RisingCompute API"

View File

@@ -0,0 +1,126 @@
# Generated by Django 5.0.6
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='BlogPost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(max_length=200, unique=True)),
('title', models.CharField(max_length=200)),
('excerpt', models.CharField(max_length=300)),
('body', models.TextField(help_text='Markdown.')),
('category', models.CharField(choices=[('ai', 'AI'), ('space', 'Space'), ('robotics', 'Robotics'), ('engineering', 'Engineering'), ('company', 'Company')], default='company', max_length=20)),
('author_name', models.CharField(max_length=120)),
('read_time_minutes', models.PositiveIntegerField(default=5)),
('cover_image_url', models.URLField(blank=True)),
('is_published', models.BooleanField(default=True)),
('published_at', models.DateTimeField(default=django.utils.timezone.now)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={'ordering': ['-published_at']},
),
migrations.CreateModel(
name='ContactSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
('email', models.EmailField(max_length=254)),
('company', models.CharField(blank=True, max_length=160)),
('role', models.CharField(blank=True, max_length=120)),
('country', models.CharField(blank=True, max_length=80)),
('interest', models.CharField(choices=[('ai-ip', 'AI Inference IP'), ('security-ip', 'Cybersecurity IP'), ('comms-ip', 'Communication IP'), ('custom-asic', 'Custom ASIC'), ('careers', 'Careers'), ('press', 'Press'), ('other', 'Other')], default='other', max_length=20)),
('message', models.TextField()),
('referrer', models.CharField(blank=True, max_length=200)),
('user_agent', models.CharField(blank=True, max_length=400)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={'verbose_name': 'Contact submission', 'ordering': ['-created_at']},
),
migrations.CreateModel(
name='Founder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
('role', models.CharField(max_length=160)),
('domain', models.CharField(help_text='Short description of the technical domain they own.', max_length=240)),
('bio', models.TextField(help_text='~80-word bio. Markdown allowed.')),
('photo_url', models.URLField(blank=True)),
('linkedin_url', models.URLField(blank=True)),
('sort_order', models.PositiveIntegerField(default=0)),
('is_published', models.BooleanField(default=True)),
],
options={'ordering': ['sort_order', 'name']},
),
migrations.CreateModel(
name='JobApplication',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
('email', models.EmailField(max_length=254)),
('role_applied_for', models.CharField(max_length=160)),
('portfolio_url', models.URLField(blank=True)),
('message', models.TextField(blank=True)),
('cv', models.FileField(blank=True, null=True, upload_to='applications/%Y/%m/')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={'ordering': ['-created_at']},
),
migrations.CreateModel(
name='JobOpening',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(max_length=120, unique=True)),
('title', models.CharField(max_length=160)),
('location', models.CharField(choices=[('surat', 'Surat, India'), ('iist', 'STIIC / IIST, Thiruvananthapuram'), ('remote', 'Remote (India)'), ('hybrid', 'Hybrid')], default='surat', max_length=20)),
('employment_type', models.CharField(default='Full-time', max_length=40)),
('description', models.TextField(help_text='Markdown.')),
('is_open', models.BooleanField(default=True)),
('posted_at', models.DateField(default=django.utils.timezone.now)),
],
options={'ordering': ['-posted_at']},
),
migrations.CreateModel(
name='NewsletterSignup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254, unique=True)),
('source', models.CharField(blank=True, help_text='e.g. footer, blog, contact', max_length=120)),
('confirmed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={'ordering': ['-created_at']},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(max_length=80, unique=True)),
('name', models.CharField(max_length=120)),
('category', models.CharField(choices=[('ai', 'AI Inference'), ('security', 'Cybersecurity'), ('comms', 'Communication Protocol'), ('asic', 'Custom ASIC Services'), ('other', 'Other')], max_length=20)),
('tagline', models.CharField(max_length=200)),
('summary', models.TextField(help_text='23 sentence overview shown on the products grid.')),
('description', models.TextField(help_text='Long-form description shown on the product detail page (markdown).')),
('benefits', models.JSONField(blank=True, default=list, help_text='List of benefit strings. Example: ["High throughput per watt", "INT8/INT4"].')),
('features', models.JSONField(blank=True, default=list)),
('spec_table', models.JSONField(blank=True, default=list, help_text='List of {"label": "...", "value": "..."} pairs for the spec table.')),
('primary_cta_label', models.CharField(default='Request evaluation', max_length=60)),
('is_published', models.BooleanField(default=True)),
('sort_order', models.PositiveIntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={'ordering': ['sort_order', 'name']},
),
]

View File

@@ -0,0 +1,268 @@
"""Seed the database with the launch content (products, founders, blog)."""
from django.db import migrations
from django.utils import timezone
PRODUCTS = [
{
"slug": "ai-inference-ip",
"name": "AI Inference IP Core",
"category": "ai",
"tagline": "Parallel inference at the edge — deterministic, quantised, portable.",
"summary": (
"A parallel-compute IP core for running quantised neural networks on FPGA "
"or ASIC. Built for vision, sensor fusion, and on-orbit inference."
),
"description": (
"Our AI Inference IP Core is designed for workloads where GPUs are too "
"power-hungry and general-purpose NPUs are too opaque. It runs INT8 / INT4 "
"quantised networks with deterministic latency, ports cleanly from FPGA "
"prototype to ASIC tape-out, and is verifiable end-to-end."
),
"benefits": [
"Higher throughput-per-watt than CPU / GPU at the edge",
"Deterministic latency for real-time control loops",
"INT8 / INT4 quantisation-aware datapath",
"Synthesises on standard FPGA tooling",
"Same RTL portable from FPGA to ASIC",
],
"features": [
"Parameterised RTL",
"Reference compiler for an ONNX subset",
"FPGA reference design",
"Integration guide & testbench",
],
"spec_table": [
{"label": "Interface", "value": "AXI4 / AXI-Stream"},
{"label": "Precision", "value": "INT8 / INT4"},
{"label": "Targets", "value": "Xilinx UltraScale+, Intel Agilex, 28nm ASIC"},
{"label": "Verification", "value": "UVM testbench included"},
],
"primary_cta_label": "Request evaluation",
"sort_order": 1,
},
{
"slug": "cybersecurity-ip",
"name": "Cybersecurity IP Core",
"category": "security",
"tagline": "Hardware-accelerated cryptography — side-channel hardened.",
"summary": (
"A drop-in hardware accelerator for symmetric and asymmetric cryptography "
"and secure boot — built for defense electronics and secure-element SoCs."
),
"description": (
"Hardware acceleration for AES, SHA, ECC and RSA primitives, plus a secure "
"boot block. Designed constant-time and side-channel hardened from the "
"first line of RTL — security is not a wrapper, it's the architecture."
),
"benefits": [
"Constant-time implementation",
"Side-channel hardened",
"FIPS-aligned algorithm set",
"Low gate count for embedded targets",
],
"features": [
"AES-128 / 256, SHA-2 / 3",
"ECC (P-256, P-384), RSA-2048 / 4096",
"True random number generator interface",
"AXI / AHB wrappers",
"Security white paper",
],
"spec_table": [
{"label": "Interface", "value": "AXI4-Lite / AHB-Lite"},
{"label": "Algorithms", "value": "AES, SHA-2/3, ECC, RSA"},
{"label": "Targets", "value": "FPGA + 28/40nm ASIC"},
{"label": "Compliance", "value": "FIPS-aligned"},
],
"primary_cta_label": "Request evaluation",
"sort_order": 2,
},
{
"slug": "communication-ip",
"name": "Communication Protocol IP",
"category": "comms",
"tagline": "Space- and avionics-grade communication blocks. Flight-proven.",
"summary": (
"SpaceWire, CAN, UART/SPI/I2C and custom satellite payload buses — "
"low gate-count, well-documented, and flight-proven on operational missions."
),
"description": (
"Our communication IP catalogue is the most battle-tested part of our "
"stack — variants of these cores have flown on operational satellite "
"missions. We licence the same blocks to ground systems, payload "
"integrators, and avionics OEMs."
),
"benefits": [
"Space-qualified design practice",
"Low gate-count for power-constrained targets",
"Flight-heritage documentation",
"Comprehensive verification IP",
],
"features": [
"SpaceWire / SpaceFibre",
"CAN-FD",
"UART / SPI / I2C masters & slaves",
"Custom satellite payload buses",
"Reference designs + integration guide",
],
"spec_table": [
{"label": "Protocols", "value": "SpaceWire, CAN-FD, UART, SPI, I2C"},
{"label": "Heritage", "value": "Flown on operational satellite missions"},
{"label": "Verification", "value": "Protocol-compliant testbenches"},
],
"primary_cta_label": "Request evaluation",
"sort_order": 3,
},
]
FOUNDERS = [
{
"name": "Ali Murabbi",
"role": "Co-founder · VLSI & Robotics Engineer",
"domain": "RTL / FPGA design · robotics control hardware · motion and sensor-fusion blocks",
"bio": (
"Hardware designer specialising in RTL and FPGA for robotics and motion "
"systems. At SSPACE, IIST, contributed to onboard compute blocks for "
"satellite payloads and now leads RisingCompute's communication-protocol "
"and robotics IP work."
),
"sort_order": 1,
},
{
"name": "Bhavy Savani",
"role": "Co-founder · VLSI & AI Engineer",
"domain": "RTL / FPGA design · quantised neural-network accelerators · verification",
"bio": (
"VLSI engineer focused on AI accelerator architectures and verification. "
"At SSPACE, IIST, designed and verified compute IP that has flown in "
"space; at RisingCompute, leads the AI Inference IP Core and the "
"Cybersecurity IP Core."
),
"sort_order": 2,
},
{
"name": "Abhishek Verma",
"role": "Co-founder · System Engineer & Project Manager",
"domain": "System architecture · integration · project delivery · GTM",
"bio": (
"Systems engineer and programme lead. Brings the IP, the engineering "
"team, and the customer programme together — sets architecture, owns "
"project delivery, and runs partner and customer conversations."
),
"sort_order": 3,
},
]
BLOG_POSTS = [
{
"slug": "why-we-started-risingcompute",
"title": "Why we started RisingCompute",
"excerpt": (
"Two years inside a satellite-software lab taught us something simple: "
"the difference between what a research team can do with AI and what most "
"engineers can do comes down to compute. We started RisingCompute to close "
"that gap."
),
"category": "company",
"author_name": "Abhishek Verma",
"read_time_minutes": 4,
"body": (
"## A note from the founders\n\n"
"RisingCompute was born inside the SSPACE lab at IIST. We spent two "
"years building computation systems for satellites — IP that has since "
"flown in space. What we kept noticing was the gap between what a "
"research team can do with the right hardware and what a typical "
"product team can do with whatever GPU they could afford.\n\n"
"Our bet is that closing that gap is a hardware problem, not a "
"software one. Parallel architectures, well-designed IP cores, and a "
"vendor that ships datasheets you can actually read."
),
},
{
"slug": "what-flight-heritage-means-for-ip",
"title": "What flight heritage actually means for IP cores",
"excerpt": (
"\"Flight-heritage\" is one of the most over-claimed phrases in the IP "
"industry. Here's what it means to us, what it doesn't, and what to "
"ask the next vendor that uses the term."
),
"category": "space",
"author_name": "Ali Murabbi",
"read_time_minutes": 6,
"body": (
"## What counts, and what doesn't\n\n"
"Flight heritage is not just \"this RTL was synthesised onto a flight "
"FPGA once.\" Real heritage is end-to-end: a documented chain from RTL "
"to verification artifacts to a specific board, on a specific mission, "
"with a specific telemetry track."
),
},
{
"slug": "int8-inference-without-accuracy-loss",
"title": "Designing AI inference cores for INT8 without accuracy loss",
"excerpt": (
"Quantisation is a free lunch — until it isn't. A practical walk "
"through the datapath choices that decide whether INT8 inference is "
"production-ready or just a benchmark trick."
),
"category": "ai",
"author_name": "Bhavy Savani",
"read_time_minutes": 8,
"body": (
"## The two failure modes\n\n"
"Most INT8 implementations fail in one of two ways: accuracy collapse "
"from poor calibration, or throughput collapse from naive datapath "
"layout. We walk through how we designed around both."
),
},
{
"slug": "indian-sovereign-ip-stack",
"title": "An Indian sovereign IP stack — and why it matters now",
"excerpt": (
"\"Make in India\" content rules are reshaping defense and space "
"procurement. A founder note on what an Indian-origin IP stack should "
"look like — and what it shouldn't."
),
"category": "company",
"author_name": "Abhishek Verma",
"read_time_minutes": 5,
"body": (
"## Sovereign doesn't mean isolated\n\n"
"Sovereign IP should mean Indian-origin design, Indian-origin "
"verification, and an Indian-origin support chain. It does not mean "
"rejecting global tooling or global customers."
),
},
]
def seed(apps, schema_editor):
Product = apps.get_model("api", "Product")
Founder = apps.get_model("api", "Founder")
BlogPost = apps.get_model("api", "BlogPost")
for row in PRODUCTS:
Product.objects.update_or_create(slug=row["slug"], defaults=row)
for row in FOUNDERS:
Founder.objects.update_or_create(name=row["name"], defaults=row)
for row in BLOG_POSTS:
BlogPost.objects.update_or_create(
slug=row["slug"],
defaults={**row, "published_at": timezone.now()},
)
def unseed(apps, schema_editor):
Product = apps.get_model("api", "Product")
Founder = apps.get_model("api", "Founder")
BlogPost = apps.get_model("api", "BlogPost")
Product.objects.filter(slug__in=[r["slug"] for r in PRODUCTS]).delete()
Founder.objects.filter(name__in=[r["name"] for r in FOUNDERS]).delete()
BlogPost.objects.filter(slug__in=[r["slug"] for r in BLOG_POSTS]).delete()
class Migration(migrations.Migration):
dependencies = [("api", "0001_initial")]
operations = [migrations.RunPython(seed, unseed)]

View File

200
backend/api/models.py Normal file
View File

@@ -0,0 +1,200 @@
"""Domain models for the RisingCompute marketing site."""
from __future__ import annotations
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
# --------------------------------------------------------------------------- #
# Content (curated by the team in the admin)
# --------------------------------------------------------------------------- #
class Product(models.Model):
"""An IP core / product line shown on the marketing site."""
CATEGORY_CHOICES = [
("ai", "AI Inference"),
("security", "Cybersecurity"),
("comms", "Communication Protocol"),
("asic", "Custom ASIC Services"),
("other", "Other"),
]
slug = models.SlugField(unique=True, max_length=80)
name = models.CharField(max_length=120)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
tagline = models.CharField(max_length=200)
summary = models.TextField(
help_text="23 sentence overview shown on the products grid."
)
description = models.TextField(
help_text="Long-form description shown on the product detail page (markdown)."
)
benefits = models.JSONField(
default=list,
blank=True,
help_text='List of benefit strings. Example: ["High throughput per watt", "INT8/INT4"].',
)
features = models.JSONField(default=list, blank=True)
spec_table = models.JSONField(
default=list,
blank=True,
help_text='List of {"label": "...", "value": "..."} pairs for the spec table.',
)
primary_cta_label = models.CharField(max_length=60, default="Request evaluation")
is_published = models.BooleanField(default=True)
sort_order = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["sort_order", "name"]
def __str__(self) -> str:
return self.name
class Founder(models.Model):
"""Founding-team profile shown on the About page."""
name = models.CharField(max_length=120)
role = models.CharField(max_length=160)
domain = models.CharField(
max_length=240,
help_text="Short description of the technical domain they own.",
)
bio = models.TextField(help_text="~80-word bio. Markdown allowed.")
photo_url = models.URLField(blank=True)
linkedin_url = models.URLField(blank=True)
sort_order = models.PositiveIntegerField(default=0)
is_published = models.BooleanField(default=True)
class Meta:
ordering = ["sort_order", "name"]
def __str__(self) -> str:
return self.name
class BlogPost(models.Model):
"""Editorial / engineering blog post."""
CATEGORY_CHOICES = [
("ai", "AI"),
("space", "Space"),
("robotics", "Robotics"),
("engineering", "Engineering"),
("company", "Company"),
]
slug = models.SlugField(unique=True, max_length=200)
title = models.CharField(max_length=200)
excerpt = models.CharField(max_length=300)
body = models.TextField(help_text="Markdown.")
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default="company")
author_name = models.CharField(max_length=120)
read_time_minutes = models.PositiveIntegerField(default=5)
cover_image_url = models.URLField(blank=True)
is_published = models.BooleanField(default=True)
published_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-published_at"]
def __str__(self) -> str:
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)[:200]
super().save(*args, **kwargs)
class JobOpening(models.Model):
"""An open role displayed on the Careers page."""
LOCATION_CHOICES = [
("surat", "Surat, India"),
("iist", "STIIC / IIST, Thiruvananthapuram"),
("remote", "Remote (India)"),
("hybrid", "Hybrid"),
]
slug = models.SlugField(unique=True, max_length=120)
title = models.CharField(max_length=160)
location = models.CharField(max_length=20, choices=LOCATION_CHOICES, default="surat")
employment_type = models.CharField(max_length=40, default="Full-time")
description = models.TextField(help_text="Markdown.")
is_open = models.BooleanField(default=True)
posted_at = models.DateField(default=timezone.now)
class Meta:
ordering = ["-posted_at"]
def __str__(self) -> str:
return self.title
# --------------------------------------------------------------------------- #
# Submissions (user input via forms — written by the frontend)
# --------------------------------------------------------------------------- #
class ContactSubmission(models.Model):
INTEREST_CHOICES = [
("ai-ip", "AI Inference IP"),
("security-ip", "Cybersecurity IP"),
("comms-ip", "Communication IP"),
("custom-asic", "Custom ASIC"),
("careers", "Careers"),
("press", "Press"),
("other", "Other"),
]
name = models.CharField(max_length=120)
email = models.EmailField()
company = models.CharField(max_length=160, blank=True)
role = models.CharField(max_length=120, blank=True)
country = models.CharField(max_length=80, blank=True)
interest = models.CharField(max_length=20, choices=INTEREST_CHOICES, default="other")
message = models.TextField()
referrer = models.CharField(max_length=200, blank=True)
user_agent = models.CharField(max_length=400, blank=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Contact submission"
def __str__(self) -> str:
return f"{self.name} <{self.email}> · {self.get_interest_display()}"
class NewsletterSignup(models.Model):
email = models.EmailField(unique=True)
source = models.CharField(max_length=120, blank=True, help_text="e.g. footer, blog, contact")
confirmed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.email
class JobApplication(models.Model):
name = models.CharField(max_length=120)
email = models.EmailField()
role_applied_for = models.CharField(max_length=160)
portfolio_url = models.URLField(blank=True)
message = models.TextField(blank=True)
cv = models.FileField(upload_to="applications/%Y/%m/", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.name}{self.role_applied_for}"

125
backend/api/serializers.py Normal file
View File

@@ -0,0 +1,125 @@
from __future__ import annotations
from rest_framework import serializers
from .models import (
BlogPost,
ContactSubmission,
Founder,
JobApplication,
JobOpening,
NewsletterSignup,
Product,
)
class ProductSerializer(serializers.ModelSerializer):
category_label = serializers.CharField(source="get_category_display", read_only=True)
class Meta:
model = Product
fields = [
"slug",
"name",
"category",
"category_label",
"tagline",
"summary",
"description",
"benefits",
"features",
"spec_table",
"primary_cta_label",
]
class FounderSerializer(serializers.ModelSerializer):
class Meta:
model = Founder
fields = ["name", "role", "domain", "bio", "photo_url", "linkedin_url"]
class BlogPostListSerializer(serializers.ModelSerializer):
category_label = serializers.CharField(source="get_category_display", read_only=True)
class Meta:
model = BlogPost
fields = [
"slug",
"title",
"excerpt",
"category",
"category_label",
"author_name",
"read_time_minutes",
"cover_image_url",
"published_at",
]
class BlogPostDetailSerializer(BlogPostListSerializer):
class Meta(BlogPostListSerializer.Meta):
fields = BlogPostListSerializer.Meta.fields + ["body"]
class JobOpeningSerializer(serializers.ModelSerializer):
location_label = serializers.CharField(source="get_location_display", read_only=True)
class Meta:
model = JobOpening
fields = [
"slug",
"title",
"location",
"location_label",
"employment_type",
"description",
"posted_at",
]
class ContactSubmissionSerializer(serializers.ModelSerializer):
class Meta:
model = ContactSubmission
fields = [
"name",
"email",
"company",
"role",
"country",
"interest",
"message",
"referrer",
]
extra_kwargs = {
"name": {"required": True, "allow_blank": False},
"email": {"required": True},
"message": {"required": True, "allow_blank": False},
}
def validate_message(self, value: str) -> str:
if len(value.strip()) < 10:
raise serializers.ValidationError(
"Please write a few words about what you're looking for."
)
return value.strip()
class NewsletterSignupSerializer(serializers.ModelSerializer):
class Meta:
model = NewsletterSignup
fields = ["email", "source"]
def create(self, validated_data):
email = validated_data["email"].lower().strip()
obj, _ = NewsletterSignup.objects.get_or_create(
email=email,
defaults={"source": validated_data.get("source", "")},
)
return obj
class JobApplicationSerializer(serializers.ModelSerializer):
class Meta:
model = JobApplication
fields = ["name", "email", "role_applied_for", "portfolio_url", "message", "cv"]

20
backend/api/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import path
from . import views
app_name = "api"
urlpatterns = [
path("health/", views.HealthView.as_view(), name="health"),
# content
path("products/", views.ProductListView.as_view(), name="product-list"),
path("products/<slug:slug>/", views.ProductDetailView.as_view(), name="product-detail"),
path("founders/", views.FounderListView.as_view(), name="founder-list"),
path("posts/", views.BlogPostListView.as_view(), name="post-list"),
path("posts/<slug:slug>/", views.BlogPostDetailView.as_view(), name="post-detail"),
path("jobs/", views.JobOpeningListView.as_view(), name="job-list"),
# submissions
path("contact/", views.ContactSubmissionView.as_view(), name="contact"),
path("newsletter/", views.NewsletterSignupView.as_view(), name="newsletter"),
path("apply/", views.JobApplicationView.as_view(), name="apply"),
]

134
backend/api/views.py Normal file
View File

@@ -0,0 +1,134 @@
"""API endpoints for the RisingCompute marketing site."""
from __future__ import annotations
from django.conf import settings
from django.core.mail import send_mail
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import BlogPost, Founder, JobOpening, Product
from .serializers import (
BlogPostDetailSerializer,
BlogPostListSerializer,
ContactSubmissionSerializer,
FounderSerializer,
JobApplicationSerializer,
JobOpeningSerializer,
NewsletterSignupSerializer,
ProductSerializer,
)
def _client_ip(request) -> str | None:
forwarded = request.META.get("HTTP_X_FORWARDED_FOR", "")
if forwarded:
return forwarded.split(",")[0].strip()
return request.META.get("REMOTE_ADDR")
# --------------------------------------------------------------------------- #
# Read endpoints — content for the public site
# --------------------------------------------------------------------------- #
class ProductListView(generics.ListAPIView):
serializer_class = ProductSerializer
queryset = Product.objects.filter(is_published=True)
class ProductDetailView(generics.RetrieveAPIView):
serializer_class = ProductSerializer
queryset = Product.objects.filter(is_published=True)
lookup_field = "slug"
class FounderListView(generics.ListAPIView):
serializer_class = FounderSerializer
queryset = Founder.objects.filter(is_published=True)
class BlogPostListView(generics.ListAPIView):
serializer_class = BlogPostListSerializer
queryset = BlogPost.objects.filter(is_published=True)
class BlogPostDetailView(generics.RetrieveAPIView):
serializer_class = BlogPostDetailSerializer
queryset = BlogPost.objects.filter(is_published=True)
lookup_field = "slug"
class JobOpeningListView(generics.ListAPIView):
serializer_class = JobOpeningSerializer
queryset = JobOpening.objects.filter(is_open=True)
# --------------------------------------------------------------------------- #
# Write endpoints — form submissions
# --------------------------------------------------------------------------- #
class ContactSubmissionView(APIView):
"""Accepts a contact / evaluation request from the website."""
def post(self, request):
serializer = ContactSubmissionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
submission = serializer.save(
user_agent=request.META.get("HTTP_USER_AGENT", "")[:400],
ip_address=_client_ip(request),
)
try:
send_mail(
subject=f"[risingcompute.in] New enquiry — {submission.get_interest_display()}",
message=(
f"From: {submission.name} <{submission.email}>\n"
f"Company: {submission.company}\n"
f"Role: {submission.role}\n"
f"Country: {submission.country}\n"
f"Interest: {submission.get_interest_display()}\n"
f"Referrer: {submission.referrer}\n\n"
f"Message:\n{submission.message}\n"
),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[settings.NOTIFY_EMAIL_TO],
fail_silently=True,
)
except Exception:
# Never let an email hiccup break the submission flow.
pass
return Response(
{"ok": True, "message": "Thanks — we'll be in touch shortly."},
status=status.HTTP_201_CREATED,
)
class NewsletterSignupView(APIView):
def post(self, request):
serializer = NewsletterSignupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{"ok": True, "message": "You're on the list."},
status=status.HTTP_201_CREATED,
)
class JobApplicationView(APIView):
def post(self, request):
serializer = JobApplicationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{"ok": True, "message": "Application received — we'll be in touch."},
status=status.HTTP_201_CREATED,
)
# --------------------------------------------------------------------------- #
# Health check
# --------------------------------------------------------------------------- #
class HealthView(APIView):
"""Simple uptime check used by deploy / monitoring."""
def get(self, request):
return Response({"status": "ok", "service": "risingcompute-api"})

20
backend/manage.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main() -> None:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "risingcompute.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Is it installed and on PYTHONPATH? "
"Did you forget to activate the virtualenv?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

8
backend/requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
Django==5.0.6
djangorestframework==3.15.1
django-cors-headers==4.3.1
psycopg[binary]==3.1.18
python-decouple==3.8
dj-database-url==2.1.0
gunicorn==22.0.0
whitenoise==6.6.0

View File

Binary file not shown.

View File

@@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "risingcompute.settings")
application = get_asgi_application()

View File

@@ -0,0 +1,161 @@
"""
Django settings for the RisingCompute marketing site.
All sensitive / environment-specific values come from a `.env` file
(see `.env.example`) and are loaded via python-decouple.
"""
from __future__ import annotations
from pathlib import Path
import dj_database_url
from decouple import Csv, config
BASE_DIR = Path(__file__).resolve().parent.parent
# --------------------------------------------------------------------------- #
# Core
# --------------------------------------------------------------------------- #
SECRET_KEY = config("DJANGO_SECRET_KEY", default="dev-insecure-key-change-me")
DEBUG = config("DJANGO_DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config(
"DJANGO_ALLOWED_HOSTS",
default="localhost,127.0.0.1",
cast=Csv(),
)
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third-party
"rest_framework",
"corsheaders",
# local
"api",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "risingcompute.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "risingcompute.wsgi.application"
# --------------------------------------------------------------------------- #
# Database (PostgreSQL via DATABASE_URL — see DATABASE_SETUP.md)
# --------------------------------------------------------------------------- #
DATABASES = {
"default": dj_database_url.config(
default=config(
"DATABASE_URL",
default="postgres://risingcompute:risingcompute@127.0.0.1:5432/risingcompute",
),
conn_max_age=600,
),
}
# --------------------------------------------------------------------------- #
# Auth, i18n, static
# --------------------------------------------------------------------------- #
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "en-in"
TIME_ZONE = "Asia/Kolkata"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# --------------------------------------------------------------------------- #
# DRF
# --------------------------------------------------------------------------- #
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.MultiPartParser",
"rest_framework.parsers.FormParser",
],
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "30/hour",
"user": "120/hour",
},
}
# --------------------------------------------------------------------------- #
# CORS — Vue dev server on :5173, production domain
# --------------------------------------------------------------------------- #
CORS_ALLOWED_ORIGINS = config(
"CORS_ALLOWED_ORIGINS",
default="http://localhost:5173,http://127.0.0.1:5173",
cast=Csv(),
)
CORS_ALLOW_CREDENTIALS = True
# --------------------------------------------------------------------------- #
# Email (form submission notifications)
# --------------------------------------------------------------------------- #
NOTIFY_EMAIL_TO = config("NOTIFY_EMAIL_TO", default="contact@risingcompute.in")
DEFAULT_FROM_EMAIL = config(
"DEFAULT_FROM_EMAIL", default="noreply@risingcompute.in"
)
EMAIL_BACKEND = config(
"EMAIL_BACKEND",
default="django.core.mail.backends.console.EmailBackend",
)
# --------------------------------------------------------------------------- #
# Security hardening for production
# --------------------------------------------------------------------------- #
if not DEBUG:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 30 # 30 days; raise to 1 year once stable
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"

View File

@@ -0,0 +1,7 @@
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("api.urls")),
]

View File

@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "risingcompute.settings")
application = get_wsgi_application()

0
backend/verify.sqlite3 Normal file
View File