Skip to content

Commit af6b6a7

Browse files
Add intercepted blog post
1 parent e374ec3 commit af6b6a7

File tree

7 files changed

+397
-138
lines changed

7 files changed

+397
-138
lines changed

backend/blog/templates/blog/blog_detail_diy_savings.html

+170
Large diffs are not rendered by default.

backend/blog/urls.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
from django.urls import path
2-
from . import views
1+
from django.urls import path, register_converter
2+
from blog import views
33

44
app_name = "blog"
55

66
urlpatterns = [
77
path("", views.blog_list, name="blog_list"), # Blog list page
88
path(
9-
"category/<slug:category_slug>/", views.blog_list, name="category_posts"
10-
), # Category-filtered posts
11-
path("<slug:slug>/", views.blog_detail, name="blog_detail"), # Blog detail page
9+
"how-much-cheaper-is-diy/",
10+
views.blog_detail_diy_savings,
11+
name="blog_detail_diy_savings",
12+
), # Custom route
13+
path("category/<slug:category_slug>/", views.blog_list, name="category_posts"),
14+
path(
15+
"<slug:slug>/", views.blog_detail, name="blog_detail"
16+
), # Catch-all for other blog posts
1217
]

backend/blog/views.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from collections import namedtuple
2+
from types import SimpleNamespace
13
from django.shortcuts import render, get_object_or_404
24
from .models import BlogPost, Category
35
from django.core.paginator import Paginator
6+
from core.calculate_diy_savings_stats import calculate_diy_savings_stats
7+
from django.http import HttpResponse
48

59

610
def blog_list(request, category_slug=None):
@@ -53,3 +57,31 @@ def blog_detail(request, slug):
5357
"categories": categories, # Pass the categories
5458
},
5559
)
60+
61+
62+
def blog_detail_diy_savings(request):
63+
# Fetch DIY savings stats
64+
diy_savings_stats = calculate_diy_savings_stats()
65+
66+
post = get_object_or_404(BlogPost, slug="how-much-cheaper-is-diy")
67+
68+
# Get related posts based on shared categories, excluding the current post
69+
related_posts = (
70+
BlogPost.objects.filter(categories__in=post.categories.all())
71+
.exclude(id=post.id)
72+
.distinct()[:3]
73+
)
74+
75+
# Get categories associated with the current post
76+
categories = post.categories.all()
77+
78+
return render(
79+
request,
80+
"blog/blog_detail_diy_savings.html",
81+
{
82+
"post": post,
83+
"related_posts": related_posts,
84+
"categories": categories, # Pass the categories
85+
"diy_savings_stats": diy_savings_stats, # Pass the DIY savings stats
86+
},
87+
)
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import statistics
2+
from decimal import Decimal
3+
from django.db.models import Sum, F, Avg
4+
from modules.models import (
5+
Module,
6+
ModuleBomListItem,
7+
)
8+
9+
10+
def calculate_diy_savings_stats():
11+
"""Calculate cost savings when building a DIY module compared to buying assembled, as a kit, or as a partial kit."""
12+
13+
modules_with_costs = Module.objects.filter(
14+
cost_built__isnull=False, cost_built__gt=0
15+
).values(
16+
"id",
17+
"cost_built",
18+
"cost_kit",
19+
"cost_partial_kit",
20+
"cost_pcb_only",
21+
"cost_pcb_plus_front",
22+
)
23+
24+
cost_differences = {
25+
"pcb_only_vs_assembled": [],
26+
"pcb_plus_front_vs_assembled": [],
27+
"pcb_only_vs_kit": [],
28+
"pcb_plus_front_vs_kit": [],
29+
"pcb_only_vs_partial_kit": [],
30+
"pcb_plus_front_vs_partial_kit": [],
31+
"partial_kit_vs_assembled": [],
32+
"partial_kit_vs_full_kit": [],
33+
}
34+
35+
for module in modules_with_costs:
36+
module_id = module["id"]
37+
cost_built = module["cost_built"] or Decimal("0")
38+
cost_kit = module["cost_kit"] or Decimal("0")
39+
cost_partial_kit = module["cost_partial_kit"] or Decimal("0")
40+
cost_pcb_only = module["cost_pcb_only"] or Decimal("0")
41+
cost_pcb_plus_front = module["cost_pcb_plus_front"] or Decimal("0")
42+
43+
# Calculate DIY cost from BOM (typically representing the DIY kit build)
44+
bom_stats = (
45+
ModuleBomListItem.objects.filter(module_id=module_id)
46+
.annotate(
47+
diy_cost=Sum(
48+
F("components_options__supplier_items__unit_price") * F("quantity")
49+
)
50+
)
51+
.aggregate(avg_diy=Avg("diy_cost"))
52+
)
53+
diy_cost = bom_stats["avg_diy"] or Decimal("0")
54+
if diy_cost == 0:
55+
continue # Skip modules where DIY cost is missing
56+
57+
# Comparisons to Assembled cost
58+
if cost_built > 0:
59+
if cost_pcb_only > 0:
60+
cost_differences["pcb_only_vs_assembled"].append(
61+
float((cost_built - cost_pcb_only) / cost_built * 100)
62+
)
63+
if cost_pcb_plus_front > 0:
64+
cost_differences["pcb_plus_front_vs_assembled"].append(
65+
float((cost_built - cost_pcb_plus_front) / cost_built * 100)
66+
)
67+
if cost_partial_kit > 0:
68+
cost_differences["partial_kit_vs_assembled"].append(
69+
float((cost_built - cost_partial_kit) / cost_built * 100)
70+
)
71+
72+
# Comparisons to Full Kit cost
73+
if cost_kit > 0:
74+
if cost_pcb_only > 0:
75+
cost_differences["pcb_only_vs_kit"].append(
76+
float((cost_kit - cost_pcb_only) / cost_kit * 100)
77+
)
78+
if cost_pcb_plus_front > 0:
79+
cost_differences["pcb_plus_front_vs_kit"].append(
80+
float((cost_kit - cost_pcb_plus_front) / cost_kit * 100)
81+
)
82+
if cost_partial_kit > 0:
83+
cost_differences["partial_kit_vs_full_kit"].append(
84+
float((cost_kit - cost_partial_kit) / cost_kit * 100)
85+
)
86+
87+
# Comparisons to Partial Kit cost
88+
if cost_partial_kit > 0:
89+
if cost_pcb_only > 0:
90+
cost_differences["pcb_only_vs_partial_kit"].append(
91+
float((cost_partial_kit - cost_pcb_only) / cost_partial_kit * 100)
92+
)
93+
if cost_pcb_plus_front > 0:
94+
cost_differences["pcb_plus_front_vs_partial_kit"].append(
95+
float(
96+
(cost_partial_kit - cost_pcb_plus_front)
97+
/ cost_partial_kit
98+
* 100
99+
)
100+
)
101+
102+
# Compute median and average savings safely
103+
def compute_stats(values):
104+
values = list(filter(None, values)) # Remove None values
105+
return {
106+
"average_savings": round(sum(values) / len(values), 2) if values else "N/A",
107+
"median_savings": round(statistics.median(values), 2) if values else "N/A",
108+
}
109+
110+
return {
111+
"pcb_only_vs_assembled": compute_stats(
112+
cost_differences["pcb_only_vs_assembled"]
113+
),
114+
"pcb_plus_front_vs_assembled": compute_stats(
115+
cost_differences["pcb_plus_front_vs_assembled"]
116+
),
117+
"pcb_only_vs_kit": compute_stats(cost_differences["pcb_only_vs_kit"]),
118+
"pcb_plus_front_vs_kit": compute_stats(
119+
cost_differences["pcb_plus_front_vs_kit"]
120+
),
121+
"pcb_only_vs_partial_kit": compute_stats(
122+
cost_differences["pcb_only_vs_partial_kit"]
123+
),
124+
"pcb_plus_front_vs_partial_kit": compute_stats(
125+
cost_differences["pcb_plus_front_vs_partial_kit"]
126+
),
127+
"partial_kit_vs_assembled": compute_stats(
128+
cost_differences["partial_kit_vs_assembled"]
129+
),
130+
"partial_kit_vs_full_kit": compute_stats(
131+
cost_differences["partial_kit_vs_full_kit"]
132+
),
133+
}

backend/core/views.py

+3-80
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import statistics
33
from django.http import HttpResponse
44
from django.shortcuts import render
5+
from core.calculate_diy_savings_stats import calculate_diy_savings_stats
56
from blog.models import BlogPost
67
from modules.models import Module, Manufacturer, ModuleBomListItem
78
from django.views.decorators.cache import cache_page
@@ -173,86 +174,8 @@ def homepage(request):
173174
manufacturer_count = Module.objects.values("manufacturer").distinct().count()
174175
component_count_display = f"{(component_count // 5) * 5}+"
175176

176-
# Compute DIY vs Assembled/Kit Savings
177-
178-
modules_with_costs = Module.objects.filter(
179-
cost_built__isnull=False, cost_built__gt=0
180-
).values("id", "cost_built", "cost_kit", "cost_pcb_only", "cost_pcb_plus_front")
181-
182-
cost_differences = {
183-
"pcb_only_vs_assembled": [],
184-
"pcb_plus_front_vs_assembled": [],
185-
"pcb_only_vs_kit": [],
186-
"pcb_plus_front_vs_kit": [],
187-
}
188-
189-
for module in modules_with_costs:
190-
module_id = module["id"]
191-
cost_built = module["cost_built"] or Decimal("0")
192-
cost_kit = module["cost_kit"] or Decimal("0")
193-
cost_pcb_only = module["cost_pcb_only"] or Decimal("0")
194-
cost_pcb_plus_front = module["cost_pcb_plus_front"] or Decimal("0")
195-
196-
# Calculate DIY cost from BOM (typically representing the DIY kit build)
197-
bom_stats = (
198-
ModuleBomListItem.objects.filter(module_id=module_id)
199-
.annotate(
200-
diy_cost=Sum(
201-
F("components_options__supplier_items__unit_price") * F("quantity")
202-
)
203-
)
204-
.aggregate(avg_diy=Avg("diy_cost"))
205-
)
206-
diy_cost = bom_stats["avg_diy"] or Decimal("0")
207-
if diy_cost == 0:
208-
continue # Skip modules where DIY cost is missing
209-
210-
# For Assembled comparisons, use cost_built as the baseline.
211-
if cost_built > 0:
212-
# PCB Only vs Assembled: How much cheaper is PCB Only compared to the assembled cost?
213-
if cost_pcb_only > 0:
214-
cost_differences["pcb_only_vs_assembled"].append(
215-
float((cost_built - cost_pcb_only) / cost_built * 100)
216-
)
217-
# PCB + Front Panel vs Assembled: How much cheaper is PCB + Front Panel compared to assembled?
218-
if cost_pcb_plus_front > 0:
219-
cost_differences["pcb_plus_front_vs_assembled"].append(
220-
float((cost_built - cost_pcb_plus_front) / cost_built * 100)
221-
)
222-
223-
# For Kit comparisons, use cost_kit as the baseline.
224-
if cost_kit > 0:
225-
# PCB Only vs Kit: How much cheaper is PCB Only compared to the kit cost?
226-
if cost_pcb_only > 0:
227-
cost_differences["pcb_only_vs_kit"].append(
228-
float((cost_kit - cost_pcb_only) / cost_kit * 100)
229-
)
230-
# PCB + Front Panel vs Kit: How much cheaper is PCB + Front Panel compared to the kit cost?
231-
if cost_pcb_plus_front > 0:
232-
cost_differences["pcb_plus_front_vs_kit"].append(
233-
float((cost_kit - cost_pcb_plus_front) / cost_kit * 100)
234-
)
235-
236-
# Compute median and average savings safely
237-
def compute_stats(values):
238-
values = list(filter(None, values)) # Remove None values
239-
return {
240-
"average_savings": round(sum(values) / len(values), 2) if values else "N/A",
241-
"median_savings": round(statistics.median(values), 2) if values else "N/A",
242-
}
243-
244-
diy_savings_stats = {
245-
"pcb_only_vs_assembled": compute_stats(
246-
cost_differences["pcb_only_vs_assembled"]
247-
),
248-
"pcb_plus_front_vs_assembled": compute_stats(
249-
cost_differences["pcb_plus_front_vs_assembled"]
250-
),
251-
"pcb_only_vs_kit": compute_stats(cost_differences["pcb_only_vs_kit"]),
252-
"pcb_plus_front_vs_kit": compute_stats(
253-
cost_differences["pcb_plus_front_vs_kit"]
254-
),
255-
}
177+
# Use the extracted function to compute DIY savings statistics
178+
diy_savings_stats = calculate_diy_savings_stats()
256179

257180
context = {
258181
"user": request.user,

backend/static/css/tailwind.css

+49
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,55 @@
9494
#table__wrapper > div:first-child {
9595
max-height: none !important;
9696
}
97+
98+
.savings-section {
99+
position: relative;
100+
background-image: url("{% static 'images/electronics_diagram_background_square.jpeg' %}");
101+
background-size: cover;
102+
background-position: center;
103+
}
104+
105+
.savings-section::before {
106+
content: "";
107+
position: absolute;
108+
inset: 0;
109+
background: rgba(255, 255, 255, 0.8);
110+
z-index: 1;
111+
}
112+
113+
.savings-content {
114+
position: relative;
115+
z-index: 2;
116+
background: rgba(255, 255, 255);
117+
border-radius: 8px;
118+
}
119+
/* Initial state for left and right elements */
120+
.slide-in-left,
121+
.slide-in-right {
122+
opacity: 0;
123+
transform: translateX(-500px);
124+
transition: opacity 0.8s ease-out, transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
125+
}
126+
127+
.slide-in-right {
128+
transform: translateX(500px);
129+
}
130+
131+
/* When in view, animate into position */
132+
.show {
133+
opacity: 1;
134+
transform: translateX(0);
135+
}
136+
137+
/* Hover effect: Grow slightly */
138+
.slide-in-left:hover,
139+
.slide-in-right:hover,
140+
.left-info-box-text:hover,
141+
.middle-info-box-text:hover
142+
{
143+
transform: scale(1.05); /* Slight enlargement */
144+
transition: transform 0.4s ease-out; /* Smooth transition */
145+
}
97146
}
98147

99148
@layer components {

0 commit comments

Comments
 (0)