adding details
This commit is contained in:
0
backend/api/__init__.py
Normal file
0
backend/api/__init__.py
Normal file
BIN
backend/api/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/admin.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/apps.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/models.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/serializers.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/serializers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/urls.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/views.cpython-310.pyc
Normal file
BIN
backend/api/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
76
backend/api/admin.py
Normal file
76
backend/api/admin.py
Normal 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
7
backend/api/apps.py
Normal 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"
|
||||
126
backend/api/migrations/0001_initial.py
Normal file
126
backend/api/migrations/0001_initial.py
Normal 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='2–3 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']},
|
||||
),
|
||||
]
|
||||
268
backend/api/migrations/0002_seed_initial_data.py
Normal file
268
backend/api/migrations/0002_seed_initial_data.py
Normal 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)]
|
||||
0
backend/api/migrations/__init__.py
Normal file
0
backend/api/migrations/__init__.py
Normal file
BIN
backend/api/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
backend/api/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/api/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
backend/api/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
200
backend/api/models.py
Normal file
200
backend/api/models.py
Normal 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="2–3 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
125
backend/api/serializers.py
Normal 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
20
backend/api/urls.py
Normal 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
134
backend/api/views.py
Normal 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"})
|
||||
Reference in New Issue
Block a user