F10: Company Detail Page Flow - Feature Analysis¶
Overview¶
The Company Detail Page serves as the public profile for each registered AI company within the Futr Connect platform. It provides users with comprehensive information about an AI company, including its metadata, insights, products, and services. The page allows users to follow companies, book meetings, and explore detailed company insights across multiple categories. This feature bridges the gap between users and companies by presenting organized, accessible company information while providing interactive elements that facilitate engagement. The responsive design ensures a consistent user experience across devices, with a focus on clear information architecture and intuitive navigation through company insights.
API Endpoints¶
1. Get Company Profile¶
GET /api/company/{company_id}/
Response:
{
"id": "uuid-string",
"name": "AI Solutions Ltd",
"logo": "https://storage.futrconnect.com/company-logos/ai-solutions.png",
"overview": "AI Solutions Ltd is a leading provider of artificial intelligence solutions for enterprise clients, specializing in natural language processing and machine learning applications.",
"social_link": {
"website": "https://aisolutions.example.com",
"linkedin": "https://linkedin.com/company/ai-solutions",
"twitter": "https://twitter.com/aisolutions",
"facebook": "https://facebook.com/aisolutions"
},
"company_founded": "2018-01-01",
"is_claimed": true,
"is_featured_by_admin": true,
"total_followers_count": 12850,
"is_followed": false,
"meeting_url": "https://calendly.com/ai-solutions/meeting"
}
2. Follow Company¶
POST /api/company-follow/
Request Body:
{
"company": "company-uuid"
}
Response (201 Created):
{
"id": "uuid-string",
"company": "company-uuid"
}
3. Get Company People¶
GET /api/people/?company={company_id}
4. Get Company Finance Insights¶
GET /api/finance/?company={company_id}
5. Get Company Metrics¶
GET /api/metric/?company={company_id}
6. Get Company Content¶
GET /api/content/?company={company_id}
7. Get Company News¶
GET /api/news/?company={company_id}
8. Get Company Privacy¶
GET /api/company-privacy-policy/?company={company_id}
9. Submit Company Feedback¶
POST /api/admin-feedback/
Request Body:
{
"rating": 4,
"message": "Great product, but could use more documentation. The customer support team was very helpful."
}
State Management¶
React-query¶
interface CompanyDetails {
id: string;
category: Category[];
user: User;
claimed_by: null;
company_addresses: any[];
company_social_links: CompanySocialLinks | null;
is_followed: boolean;
following_users: any[];
created_on: string;
edited_on: string;
name: string;
email: string;
company_founded: string;
phone_number: string;
logo: string;
meeting_url: string;
short_video: string;
overview: string;
about: string;
best_suited_for: string[];
where_to_use: string;
standout_function: string;
status: string;
publish_date: string;
is_claimed: boolean;
is_featured_by_admin: boolean;
total_followers_count: number;
demo_video?: null | string;
demo_video_url?: null | string;
first_call?: null | string;
first_call_url?: null | string;
}
interface User {
id: string;
email: string;
full_name: string;
avatar: string;
title: null;
location: null;
organization_name: null;
}
interface CompanySocialLinks {
id: string;
created_on: string;
edited_on: string;
career: null | string;
event_page: null | string;
customer_portal: null | string;
facebook: null | string;
instagram: null | string;
youtube: null | string;
twitter: null | string;
linkedin: null | string;
website: string | null;
company: string | null;
}
interface Category {
id: string;
title: string;
description: string;
}
type PeopleTypes = {
id: string;
created_on: string;
edited_on: string;
avatar?: string | null;
name?: string | null;
title?: string | null;
url?: string | null;
company?: string | null;
designation_type?: string | null;
};
type CompanyFinanceType = {
id: string;
created_on: string;
edited_on: string;
ipo_status: string;
latest_funding_round: string;
yahoo_finance_url: string | null;
latest_valuation: string;
annual_revenue: string;
total_funding: string;
company: string;
};
Database Models¶
PostgreSQL Models¶
Company¶
id(UUID, primary key)name(varchar, required)logo(ImageField, nullable)overview(text, nullable)short_video(URLField, nullable)category(ManyToManyField to CompanyCategory)is_featured_by_admin(boolean, default: false)is_claimed(boolean, default: false)claimed_by(ForeignKey to User, nullable)meeting_url(URLField, nullable)social_link(OneToOneField to CompanySocialLink)
People¶
id(UUID, primary key)company(ForeignKey to Company)designation_type(ForeignKey to DesignationType)avatar(ImageField, nullable)name(varchar, required)title(varchar, nullable)url(URLField, nullable)
Finance¶
id(UUID, primary key)company(ForeignKey to Company)ipo_status(varchar, required)latest_funding_round(varchar, required)yahoo_finance_url(URLField, nullable)latest_valuation(varchar, nullable)annual_revenue(varchar, required)total_funding(varchar, nullable)
Metric¶
id(UUID, primary key)company(ForeignKey to Company)total_employees(varchar, required)total_customers(varchar, required)client_retention(IntegerField, nullable)
Content¶
id(UUID, primary key)company(ForeignKey to Company)link(URLField, required)title(varchar, required)video_type(varchar, required)marked_video_as(varchar, choices: demo_video, featured_video, video_call, nullable)total_count(PositiveIntegerField, default: 0)
News¶
id(UUID, primary key)company(ForeignKey to Company)source_url(URLField, nullable)total_count(PositiveIntegerField, default: 0)
CompanyPrivacyPolicy¶
id(UUID, primary key)company(OneToOneField to Company)file(FileField, nullable)privacy_url(URLField, nullable)
Adminfeedback¶
id(UUID, primary key)user(ForeignKey to User, nullable)rating(DecimalField, required)message(text, required)is_active(boolean, default: false)
Developer Note on Company Insight Models¶
Multiple Models Approach¶
We have chosen to implement separate database models for each type of company insight. This decision was made to optimize for type safety, schema clarity, and long-term maintainability. Each insight type has its own dedicated model with type-specific fields.
Naming Convention¶
All specialized company insight models follow a consistent naming convention:
CompanyInsightProduct- For product informationCompanyInsightPeople- For team members and leadershipCompanyInsightFinance- For financial informationCompanyInsightMetrics- For company metricsCompanyInsightContent- For content and mediaCompanyInsightNews- For news articlesCompanyInsightAwards- For awards and recognitionsCompanyInsightReferences- For testimonials and referencesCompanyInsightFeedback- For user feedbackCompanyInsightPrivacy- For privacy policies and data handling
This naming convention ensures consistency across the codebase and makes it clear that these models are specialized versions of company insights.
Key Implementation Details¶
- Model Structure:
- Individual models for each insight type:
CompanyInsightProduct,CompanyInsightPeople,CompanyInsightFinance,CompanyInsightMetrics,CompanyInsightContent,CompanyInsightNews,CompanyInsightAwards,CompanyInsightReferences,CompanyInsightFeedback, andCompanyInsightPrivacy - Each model has common fields (title, description, media) and type-specific fields
- Clear schema definition with explicit fields for each insight type
- API Layer:
- Type-specific endpoints:
/api/companies/{company_id}/products/,/api/companies/{company_id}/people/, etc. - Each endpoint works with its own dedicated model
- Type-specific filtering and sorting capabilities for each endpoint
- Validation:
- Built-in database-level validation for each model
- Type safety through defined schema for each insight type
- Clear constraints on required and optional fields
Benefits of Specialized Models¶
-
Type Safety: Each model has its own specific fields with appropriate types, reducing runtime errors and providing better IDE autocomplete support.
-
Schema Clarity: Developers can easily understand what fields are available for each insight type without wading through conditional logic or documentation.
-
Enhanced Validation: Each model can have its own validation rules specific to that type of insight, making it easier to ensure data integrity.
-
Optimized Queries: Queries are more efficient as they target only the relevant tables for each insight type rather than filtering a larger generic table.
-
Independent Evolution: Each insight type can evolve independently without affecting other types, making it easier to add or modify fields for a specific insight type.
-
Simplified UI Components: UI components can be built specifically for each insight type, with props that exactly match the model structure.
-
Better Documentation: API documentation is clearer because each endpoint has a well-defined request and response structure specific to an insight type.
-
Reduced Conditional Logic: Less need for conditional rendering or processing based on insight type, as each type already has its own dedicated processing flow.
API Usage Guidelines¶
Developers should use these endpoints to interact with company insights:
- Get Company Profile - To see what insight types are available:
GET /api/companies/{company_id}/
This returns the company profile including the insight_types array showing available insight types.
- Type-Specific Endpoints - Use dedicated endpoints for each type:
GET /api/companies/{company_id}/people/
GET /api/companies/{company_id}/finance/
GET /api/companies/{company_id}/metrics/
Benefits of This Approach¶
- Type Safety: Strong typing at the database level
- Schema Clarity: Explicit fields for each type
- Maintainability: Easier to evolve each insight type independently
- Performance: Optimal query performance with specific indexes
- Validation: Straightforward validation rules for each model
UI Component Strategy¶
Create type-specific UI components that correspond to each model:
'use client';
import { forwardRef, useState } from 'react';
import FianceAndMetrics from '../FianceAndMetrics/FianceAndMetrics';
import NewsAndContent from '../NewsAndContent/NewAndContent';
import PeopleSection from '../PeopleSection/PeopleSection';
import { motion } from 'framer-motion';
const navItems = [
'People',
'Finance'
];
const companyComponents = {
People: <PeopleSection />,
Finance: <FianceAndMetrics />
};
const CompanyInsightsSection = forwardRef<HTMLDivElement>((_, ref) => {
const [activeItem, setActiveItem] = useState<keyof typeof companyComponents>('People');
return (
<div className=' max-w-[1530px] w-full '>
<div className='flex flex-col md:flex-row mt-9 gap-8 md:gap-11'>
{/** Sidebar with animation **/}
<motion.div
initial={{ x: -50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className='w-full self-start md:w-[300px] flex-shrink-1 bg-white rounded-xl shadow space-y-2 overflow-hidden'>
<div className='block sm:flex md:block sm:p-2 md:p-0'>
{navItems.map((item, index) => {
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
onClick={() => setActiveItem(item as keyof typeof companyComponents)}
className={`px-4 flex-1 cursor-pointer py-5 block border-b border-gray-100 transition-colors
${
item === activeItem
? 'bg-primaryColor text-white sm:rounded-lg md:rounded-none'
: 'text-black font-normal hover:bg-orientSecondary'
}`}>
{item}
</motion.div>
);
})}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1 }}
ref={ref}
className='flex-1 overflow-x-hidden bg-white p-8 rounded-lg '>
{companyComponents[activeItem]}
</motion.div>
</div>
</div>
);
});
export default CompanyInsightsSection;
'use client';
import React, { Suspense } from 'react';
import { getDesignationQuery } from '@/services/modules/company-insights/queries';
import { designationType } from '@/services/modules/company-insights/types';
import { ApiResponseBaseType } from '@/types/api';
import { PEOPLE_TYPES_COMPANY_INSIGHTS } from '@/utils';
import { useQuery } from '@tanstack/react-query';
import PeopleLists from '../CompanyInsights/PeopleLists';
import CompanyDetailsInsightsPeopleLoading from './CompanyDetailsInsightsPeopleLoading';
import InvestorList from './InvestorList';
export default function PeopleSection() {
const { data, isLoading } = useQuery<ApiResponseBaseType<designationType[]>>({
queryKey: ['designation_key'],
queryFn: getDesignationQuery
});
if (isLoading || !data) {
return <CompanyDetailsInsightsPeopleLoading />;
}
return (
<Suspense fallback={<CompanyDetailsInsightsPeopleLoading />}>
<h1 className='text-3xl'>People</h1>
<PeopleLists
isEditAccess={false}
peopleQueryKey={
data.results.filter((value) => value.title == PEOPLE_TYPES_COMPANY_INSIGHTS.Executive)?.[0]?.id ??
''
}
title='Executive leadership'
/>
<PeopleLists
isEditAccess={false}
peopleQueryKey={
data.results.filter((value) => value.title == PEOPLE_TYPES_COMPANY_INSIGHTS.advisor)?.[0]?.id ?? ''
}
title='Advisors'
/>
<InvestorList
peopleQuery={
data.results.find((item) => item.title == PEOPLE_TYPES_COMPANY_INSIGHTS.Investor)?.id ?? ''
}
/>
<PeopleLists
isEditAccess={false}
peopleQueryKey={
data.results.filter((value) => value.title == PEOPLE_TYPES_COMPANY_INSIGHTS.board)?.[0]?.id ?? ''
}
title='Board Members'
/>
</Suspense>
);
}
'use client';
import React, { Fragment, useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
import { Skeleton } from '@/components/ui/skeleton';
import useErrorHandelForGet from '@/hooks/useErrorHandelForGet';
import { getFinanceListQuery, getMetricListQuery } from '@/services/modules/company-insights/queries';
import { companyInsightsKeys } from '@/services/modules/company-insights/query-keys';
import { CompanyFinanceType, CompanyMetrics } from '@/services/modules/company-insights/types';
import { ApiResponseBaseType, CustomErrorType } from '@/types/api';
import { formatNumber } from '@/utils/formatNumber';
import { useQuery } from '@tanstack/react-query';
import { FinanceCard } from '../FinanceCard/Finance';
import MetricCard from '../MetricCard/MetricCard';
const pageType = {
Finance: 'Finance',
Metric: 'Metrics'
};
export default function FianceAndMetrics() {
const { slug } = useParams();
const { errorHandelForGet } = useErrorHandelForGet();
const [selectContentType, setSelectContentType] = useState<'Finance' | 'Metrics'>('Finance');
const {
data: financeData,
isLoading: loadingData,
isError,
error
} = useQuery<ApiResponseBaseType<CompanyFinanceType[]>, CustomErrorType>({
queryKey: [...companyInsightsKeys.companyType('finance'), slug],
queryFn: () =>
getFinanceListQuery({
company: slug as string
}),
enabled: selectContentType == 'Finance'
});
const { data: metricData, isLoading: loadingMetricData } = useQuery<
ApiResponseBaseType<CompanyMetrics[]>,
CustomErrorType
>({
queryKey: [...companyInsightsKeys.companyType('metrics'), slug],
queryFn: () =>
getMetricListQuery({
company: slug as string
}),
enabled: selectContentType == 'Metrics'
});
useEffect(() => {
if (isError) {
errorHandelForGet(error.code);
}
}, [error, isError]);
return (
<div>
<div className='flex gap-5 '>
{['Finance', 'Metrics'].map((item) => (
<button
key={item}
className={`px-4 min-w-[120px] py-3 text-mirageLight font-normal bg-orientSecondary text-sm rounded-md whitespace-nowrap ${
selectContentType == item ? 'border-primaryColor border-2 font-semibold' : ''
}`}
onClick={() => setSelectContentType(item as 'Finance' | 'Metrics')}>
{item}
</button>
))}
</div>
<p className='text-3xl my-4 md:my-9'> {pageType[selectContentType]}</p>
<div className='flex flex-wrap gap-6 max-h-[500px] overflow-auto py-4 px-2'>
{pageType[selectContentType] == 'Finance'
? loadingData
? Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className={`shadow-[0_4px_10px_0_rgba(0,0,0,0.15)]
px-4 py-5 max-w-[500px] w-full gap-3 flex rounded-lg transition-colors `}>
<div>
<Skeleton className='w-[50px] h-[50px] rounded-full' />
</div>
<div className='flex flex-col justify-center'>
<Skeleton className='h-4 w-[100px] mb-2' />
<Skeleton className='h-5 w-[150px]' />
</div>
</div>
))
: financeData?.results.map((item, index) => (
<Fragment key={item.id}>
<FinanceCard title={item.ipo_status ?? 'N/A'} label='IPO Status' />
<FinanceCard
title={item.latest_funding_round ?? 'N/A'}
label='Latest Funding Round'
/>{' '}
<FinanceCard
title={item.total_funding ? formatNumber(item.total_funding) : 'N/A'}
label='Total Funding'
/>{' '}
<FinanceCard
title={item.latest_valuation ? formatNumber(item.latest_valuation) : 'N/A'}
label='Latest Valuation'
/>{' '}
<FinanceCard title={item.annual_revenue} label='Annual Revenue' />
</Fragment>
))
: loadingMetricData
? Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className={`bg-backgroundLight h-[180px] max-w-[300px] w-full flex flex-col items-center justify-center shadow-sm rounded-lg`}>
<Skeleton className='h-4 w-[200px] mb-3' />
<Skeleton className='h-6 w-[200px]' />
</div>
))
: metricData?.results.map((item, index) => (
<Fragment key={index}>
<MetricCard title='Employees' value={item.total_employees ?? 'N/A'} />
<MetricCard title='Customers' value={item.total_customers ?? 'N/A'} />
{/* <MetricCard
title="Client retention rate"
value={item.client_retention ? `${item.client_retention+"%"}`: "N/A"}
/> */}
</Fragment>
))}
</div>
</div>
);
}
Algorithms¶
1. Company Similarity Algorithm¶
This algorithm calculates similarity between companies to recommend related companies based on industry tags, content, and user interactions.
Inputs:
- Primary company’s industry tags
- Primary company’s content and insights
- User interaction data (which users follow which companies)
Process:
- Calculate tag-based similarity using Jaccard similarity coefficient
- Analyze insight content using TF-IDF for content similarity
- Apply collaborative filtering based on users who follow similar companies
- Combine scores using a weighted approach:
similarity_score = (
(0.4 * tag_similarity) +
(0.3 * content_similarity) +
(0.3 * collaborative_similarity)
)
- Sort by similarity score and filter out the primary company
Implementation:
def get_related_companies(company_id, limit=5):
# Get the primary company
primary_company = Company.objects.get(id=company_id)
primary_tags = set(primary_company.industry_tags)
# Get all other companies
other_companies = Company.objects.exclude(id=company_id)
related_companies = []
for company in other_companies:
# Tag similarity (Jaccard)
company_tags = set(company.industry_tags)
tag_intersection = len(primary_tags.intersection(company_tags))
tag_union = len(primary_tags.union(company_tags))
tag_similarity = tag_intersection / tag_union if tag_union > 0 else 0
# Content similarity
content_similarity = calculate_content_similarity(primary_company, company)
# Collaborative similarity
collaborative_similarity = calculate_collaborative_similarity(primary_company, company)
# Combined score
similarity_score = (
(0.4 * tag_similarity) +
(0.3 * content_similarity) +
(0.3 * collaborative_similarity)
)
related_companies.append({
'company': company,
'similarity_score': similarity_score
})
# Sort by similarity score and return top matches
related_companies.sort(key=lambda x: x['similarity_score'], reverse=True)
return related_companies[:limit]
def calculate_content_similarity(company1, company2):
# Get insights for both companies (across all insight types)
company1_products = CompanyInsightProduct.objects.filter(company_id=company1.id, status='published')
company1_people = CompanyInsightPeople.objects.filter(company_id=company1.id, status='published')
company1_finance = CompanyInsightFinance.objects.filter(company_id=company1.id, status='published')
company1_metrics = CompanyInsightMetrics.objects.filter(company_id=company1.id, status='published')
company1_content = CompanyInsightContent.objects.filter(company_id=company1.id, status='published')
company1_news = CompanyInsightNews.objects.filter(company_id=company1.id, status='published')
company1_awards = CompanyInsightAwards.objects.filter(company_id=company1.id, status='published')
company1_references = CompanyInsightReferences.objects.filter(company_id=company1.id, status='published')
company1_feedback = CompanyInsightFeedback.objects.filter(company_id=company1.id, status='published')
company1_privacy = CompanyInsightPrivacy.objects.filter(company_id=company1.id, status='published')
company2_products = CompanyInsightProduct.objects.filter(company_id=company2.id, status='published')
company2_people = CompanyInsightPeople.objects.filter(company_id=company2.id, status='published')
company2_finance = CompanyInsightFinance.objects.filter(company_id=company2.id, status='published')
company2_metrics = CompanyInsightMetrics.objects.filter(company_id=company2.id, status='published')
company2_content = CompanyInsightContent.objects.filter(company_id=company2.id, status='published')
company2_news = CompanyInsightNews.objects.filter(company_id=company2.id, status='published')
company2_awards = CompanyInsightAwards.objects.filter(company_id=company2.id, status='published')
company2_references = CompanyInsightReferences.objects.filter(company_id=company2.id, status='published')
company2_feedback = CompanyInsightFeedback.objects.filter(company_id=company2.id, status='published')
company2_privacy = CompanyInsightPrivacy.objects.filter(company_id=company2.id, status='published')
# Combine text from all insight types
company1_texts = []
for product in company1_products:
company1_texts.append(product.title + ' ' + product.description)
for person in company1_people:
company1_texts.append(person.title + ' ' + person.description)
for finance in company1_finance:
company1_texts.append(finance.title + ' ' + finance.description)
for metric in company1_metrics:
company1_texts.append(metric.title + ' ' + metric.description)
for content in company1_content:
company1_texts.append(content.title + ' ' + content.description)
for news in company1_news:
company1_texts.append(news.title + ' ' + news.description)
for award in company1_awards:
company1_texts.append(award.title + ' ' + award.description)
for reference in company1_references:
company1_texts.append(reference.title + ' ' + reference.description)
for feedback in company1_feedback:
company1_texts.append(feedback.title + ' ' + feedback.description)
for privacy in company1_privacy:
company1_texts.append(privacy.title + ' ' + privacy.description)
company2_texts = []
for product in company2_products:
company2_texts.append(product.title + ' ' + product.description)
for person in company2_people:
company2_texts.append(person.title + ' ' + person.description)
for finance in company2_finance:
company2_texts.append(finance.title + ' ' + finance.description)
for metric in company2_metrics:
company2_texts.append(metric.title + ' ' + metric.description)
for content in company2_content:
company2_texts.append(content.title + ' ' + content.description)
for news in company2_news:
company2_texts.append(news.title + ' ' + news.description)
for award in company2_awards:
company2_texts.append(award.title + ' ' + award.description)
for reference in company2_references:
company2_texts.append(reference.title + ' ' + reference.description)
for feedback in company2_feedback:
company2_texts.append(feedback.title + ' ' + feedback.description)
for privacy in company2_privacy:
company2_texts.append(privacy.title + ' ' + privacy.description)
company1_text = ' '.join(company1_texts)
company2_text = ' '.join(company2_texts)
# Use TF-IDF to calculate similarity
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform([company1_text, company2_text])
# Calculate cosine similarity
return cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
def calculate_collaborative_similarity(company1, company2):
# Get followers for both companies
company1_followers = CompanyFollower.objects.filter(company_id=company1.id).values_list('user_id', flat=True)
company2_followers = CompanyFollower.objects.filter(company_id=company2.id).values_list('user_id', flat=True)
# Find common followers
common_followers = set(company1_followers).intersection(set(company2_followers))
# Calculate Jaccard similarity
union_followers = set(company1_followers).union(set(company2_followers))
return len(common_followers) / len(union_followers) if len(union_followers) > 0 else 0
2. Follower Count Tracking Algorithm¶
This algorithm efficiently tracks and updates follower counts for companies while maintaining consistency.
Inputs:
- Company ID
- User action (follow or unfollow)
- User ID
Process:
- When a user follows a company:
- Create a CompanyFollower record
- Atomically increment the follower_count in the denormalized counter - When a user unfollows a company:
- Delete the CompanyFollower record
- Atomically decrement the follower_count - Schedule periodic reconciliation to ensure count accuracy
Implementation:
def follow_company(company_id, user_id):
# Get company
company = Company.objects.get(id=company_id)
# Create follower record if it doesn't exist
_, created = CompanyFollower.objects.get_or_create(
company_id=company_id,
user_id=user_id,
defaults={'followed_at': timezone.now()}
)
if created:
# Atomically increment follower count
Company.objects.filter(id=company_id).update(
follower_count=F('follower_count') + 1
)
# Refresh company object to get updated count
company.refresh_from_db()
# Log the follow event for analytics
log_follow_event(company_id, user_id)
return {
'company_id': company_id,
'is_followed': True,
'followed_at': timezone.now(),
'follower_count': company.follower_count
}
def unfollow_company(company_id, user_id):
# Try to find and delete the follower record
try:
follower = CompanyFollower.objects.get(company_id=company_id, user_id=user_id)
follower.delete()
# Atomically decrement follower count
Company.objects.filter(id=company_id).update(
follower_count=F('follower_count') - 1
)
# Log the unfollow event for analytics
log_unfollow_event(company_id, user_id)
# Refresh company object to get updated count
company = Company.objects.get(id=company_id)
return {
'company_id': company_id,
'is_followed': False,
'follower_count': company.follower_count
}
except CompanyFollower.DoesNotExist:
# User wasn't following this company
company = Company.objects.get(id=company_id)
return {
'company_id': company_id,
'is_followed': False,
'follower_count': company.follower_count
}
def reconcile_follower_counts():
"""
Periodic task to ensure follower counts are accurate.
Runs daily or weekly to fix any inconsistencies.
"""
companies = Company.objects.all()
for company in companies:
actual_count = CompanyFollower.objects.filter(company_id=company.id).count()
if company.follower_count != actual_count:
# Log the discrepancy
log_follower_count_discrepancy(company.id, company.follower_count, actual_count)
# Update to the correct count
company.follower_count = actual_count
company.save(update_fields=['follower_count'])
Implementation Guide for Multiple Models Approach¶
Working with Type-Specific Models¶
The multiple models approach for company insights provides strong type safety and schema clarity. Here’s how to effectively implement and work with this approach:
1. Data Structure Overview¶
We use separate models for each insight type:
PeopleSection- For peoples informationFianceAndMetrics- For team finance and metrics
2. Backend Implementation¶
Working with Products¶
# Creating a product insight
def create_company_product(company_id, data):
product = CompanyInsightProduct.objects.create(
company_id=company_id,
title=data.get('title'),
description=data.get('description', ''),
media=data.get('media', []),
highlights=data.get('highlights', []),
cta=data.get('cta'),
pricing_model=data.get('pricing_model'),
features=data.get('features', []),
technical_specs=data.get('technical_specs', {}),
published_date=data.get('published_date'),
created_by=data.get('created_by')
)
return product
# Retrieving products with filtering
def get_company_products(company_id, filters=None):
query = CompanyInsightProduct.objects.filter(company_id=company_id)
if filters:
if 'featured' in filters:
query = query.filter(status='published')
if 'pricing_model' in filters:
query = query.filter(pricing_model=filters['pricing_model'])
return query.order_by('-published_date')
Working with People¶
# Creating a people insight
def create_company_person(company_id, data):
person = CompanyInsightPeople.objects.create(
company_id=company_id,
title=data.get('title'),
description=data.get('description', ''),
media=data.get('media', []),
highlights=data.get('highlights', []),
cta=data.get('cta'),
person_name=data.get('person_name'),
role=data.get('role'),
bio=data.get('bio', ''),
linkedin_url=data.get('linkedin_url'),
is_executive=data.get('is_executive', False),
published_date=data.get('published_date'),
created_by=data.get('created_by')
)
return person
# Retrieving executives
def get_company_executives(company_id):
return CompanyInsightPeople.objects.filter(
company_id=company_id,
is_executive=True,
status='published'
).order_by('role')
3. Adding New Insight Types¶
When adding a new insight type, follow these steps:
- Create a new database model:
class CompanyInsightAwards(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
# Common fields...
# Type-specific fields
award_name = models.CharField(max_length=255)
award_date = models.DateField()
issuing_organization = models.CharField(max_length=255)
award_category = models.CharField(max_length=255, null=True, blank=True)
# ...
- Create API endpoints:
# urls.py
urlpatterns = [
# ...
path('companies/<uuid:company_id>/awards/', views.CompanyAwardsListView.as_view()),
path('companies/<uuid:company_id>/awards/<uuid:award_id>/', views.CompanyAwardDetailView.as_view()),
]
# views.py
class CompanyAwardsListView(APIView):
def get(self, request, company_id):
awards = CompanyInsightAwards.objects.filter(company_id=company_id)
serializer = CompanyAwardSerializer(awards, many=True)
return Response(serializer.data)
def post(self, request, company_id):
serializer = CompanyAwardSerializer(data=request.data)
if serializer.is_valid():
serializer.save(company_id=company_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Backend implementation of the Privacy API endpoint¶
@api_view([‘GET’])
@permission_classes([IsAuthenticated])
def get_company_privacy(request, company_id):
“”“
Retrieve privacy policy information for a company
“”“
try: # Check if company exists
company = Company.objects.get(id=company_id)
# Get privacy insights
privacy_insights = CompanyInsightPrivacy.objects.filter(
company_id=company_id,
status='published'
).order_by('-published_date')
# Paginate results
paginator = PageNumberPagination()
paginator.page_size = 10
result_page = paginator.paginate_queryset(privacy_insights, request)
# Serialize
serializer = CompanyInsightPrivacySerializer(result_page, many=True)
# Track view
track_company_view(request, company)
return paginator.get_paginated_response(serializer.data)
except Company.DoesNotExist:
return Response(
{"detail": "Company not found"},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
return Response(
{"detail": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
Privacy serializer¶
class CompanyInsightPrivacySerializer(serializers.ModelSerializer):
class Meta:
model = CompanyInsightPrivacy
fields = ‘all‘