Skip to content

Commit 0330f01

Browse files
feat: add account activation direct endpoint, custom category caching, and Stripe CLI integration
• Added a direct URL for account activation and template (activate.html) • Improved category cache invalidation logic and logging in shop/signals.py • Refactored CategoryViewSet to use a custom cache key and removed the cache_page decorator • Updated ReviewSerializer to include the review ID • Enhanced ChatConsumer for safer async user loading • Integrated Stripe CLI service to docker-compose.yml for webhook forwarding • Minor fixes and import updates across API and account modules
1 parent 14a9a5c commit 0330f01

File tree

8 files changed

+293
-11
lines changed

8 files changed

+293
-11
lines changed

account/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,4 @@ def get(self, request, uid, token):
372372
'token': token,
373373
}
374374
)
375+

chat/consumers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,17 @@ async def connect(self):
4343
from shop.models import Product # Import Product here
4444

4545
try:
46-
self.product = await Product.objects.aget(product_id=self.product_id)
46+
# Use async database operations
47+
self.product = await Product.objects.select_related('user').aget(product_id=self.product_id)
4748
except Product.DoesNotExist:
4849
await self.close()
4950
return
5051

5152
# Create a unique room name for the seller-buyer-product combination
5253
# Sort user IDs to ensure same room name regardless of who connects first
53-
user_ids = sorted([str(self.user.id), str(self.product.user.id)])
54+
# Use sync_to_async to safely access the product.user.id
55+
product_user_id = await sync_to_async(lambda: self.product.user.id)()
56+
user_ids = sorted([str(self.user.id), str(product_user_id)])
5457
self.room_name = f'chat_product_{self.product_id}_users_{"_".join(user_ids)}'
5558
self.room_group_name = f'chat_{self.room_name}'
5659

docker-compose.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,33 @@ services:
4242
- cache
4343
- database
4444

45+
stripe-cli:
46+
image: stripe/stripe-cli:latest
47+
container_name: stripe-cli
48+
restart: "no"
49+
env_file:
50+
- .env
51+
environment:
52+
- STRIPE_API_KEY=${STRIPE_SECRET_KEY}
53+
- STRIPE_DEVICE_NAME=docker-stripe-cli
54+
volumes:
55+
- stripe_config:/root/.config/stripe
56+
entrypoint: ["/bin/sh", "-c"]
57+
command: >
58+
"while ! nc -z web 8000; do
59+
echo 'Waiting for web service on port 8000...';
60+
sleep 2;
61+
done;
62+
echo 'Web service is ready!';
63+
stripe login --api-key $${STRIPE_SECRET_KEY} &&
64+
stripe listen --forward-to web:8000/payment/webhook/"
65+
depends_on:
66+
- web
67+
networks:
68+
- default
4569

4670
volumes:
4771
database:
4872
static_volume:
49-
media_volume:
73+
media_volume:
74+
stripe_config:

ecommerce_api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from shop.feeds import TrendingProductsFeed
2828
from shop.sitemaps import ProductSitemap
29+
from account.views import ActivateView
2930

3031
sitemaps = {
3132
'products': ProductSitemap,
@@ -52,6 +53,7 @@ def api_root(request):
5253
urlpatterns = [
5354
path('', api_root, name='api-root'), # Handle root path requests
5455
path('auth/', include('account.urls', namespace='auth')),
56+
path('activate/<str:uid>/<str:token>/', ActivateView.as_view(), name='activate-direct'),
5557
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
5658
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
5759
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),

shop/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class ReviewSerializer(serializers.ModelSerializer):
118118

119119
class Meta:
120120
model = Review
121-
fields = ['user', 'rating', 'comment', 'created']
121+
fields = ['id', 'user', 'rating', 'comment', 'created']
122122
read_only_fields = ['created']
123123

124124

shop/signals.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,36 @@
77

88

99
@receiver([post_save, post_delete], sender=Category)
10-
def invalidate_category_cache(sender, instance, **kwargs):
10+
def invalidate_category_cache(sender, instance, created=False, **kwargs):
1111
"""
12-
Invalidate the cache for the category list when a category is created or deleted.
12+
Invalidate the cache for the category list when a category is created, updated or deleted.
1313
"""
14-
from django.core.cache import cache
14+
import logging
15+
logger = logging.getLogger(__name__)
1516

16-
# Clear the cache for the category list
17-
cache.delete_pattern('*category_list*')
17+
action = "created" if kwargs.get('created', False) else "updated" if sender == post_save else "deleted"
18+
logger.info(f"Category {instance.name} was {action}, invalidating cache...")
1819

20+
# Clear the custom category cache key
21+
try:
22+
# Clear the specific cache key used in the views
23+
cache.delete('category_list_custom')
24+
logger.info("Category cache invalidated successfully")
25+
26+
# Also clear any cache_page patterns if they exist from other views
27+
if hasattr(cache, 'delete_pattern'):
28+
cache.delete_pattern('*category_list*')
29+
cache.delete_pattern('*views.decorators.cache.cache_page*category_list*')
30+
logger.info("Cache patterns also cleared")
31+
32+
except Exception as e:
33+
logger.error(f"Error invalidating cache: {e}")
34+
try:
35+
# Fallback: Clear the entire cache if specific key deletion fails
36+
cache.clear()
37+
logger.info("Fallback: Entire cache cleared")
38+
except Exception as fallback_e:
39+
logger.error(f"Fallback cache clear also failed: {fallback_e}")
1940

2041
@receiver(post_save, sender=Review)
2142
def update_rating_on_review_save(sender, instance, created, **kwargs):

shop/views.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,31 @@ def get_permissions(self):
449449
self.permission_classes = [permissions.IsAdminUser]
450450
return super().get_permissions()
451451

452-
@method_decorator(cache_page(60 * 60 * 24, key_prefix=r'category_list'))
452+
# Remove the cache_page decorator and implement custom caching
453453
def list(self, request, *args, **kwargs):
454+
from django.core.cache import cache
454455
try:
455456
logger.info("Listing categories")
456-
return super().list(request, *args, **kwargs)
457+
458+
# Create a custom cache key that we can easily invalidate
459+
cache_key = 'category_list_custom'
460+
461+
# Check if we have cached data
462+
cached_data = cache.get(cache_key)
463+
464+
if cached_data is None:
465+
# If no cached data, get the response and cache it
466+
response = super().list(request, *args, **kwargs)
467+
468+
# Cache the response data for 24 hours
469+
cache.set(cache_key, response.data, 60 * 60 * 24)
470+
471+
return response
472+
else:
473+
# Return cached data
474+
from rest_framework.response import Response
475+
return Response(cached_data)
476+
457477
except Exception as e:
458478
logger.error("Error listing categories: %s", e, exc_info=True)
459479
raise

templates/account/activate.html

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Account Activation</title>
7+
<style>
8+
body {
9+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11+
margin: 0;
12+
padding: 0;
13+
min-height: 100vh;
14+
display: flex;
15+
justify-content: center;
16+
align-items: center;
17+
}
18+
19+
.container {
20+
background: white;
21+
border-radius: 15px;
22+
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
23+
padding: 40px;
24+
width: 100%;
25+
max-width: 500px;
26+
text-align: center;
27+
}
28+
29+
.logo {
30+
font-size: 28px;
31+
font-weight: bold;
32+
color: #667eea;
33+
margin-bottom: 30px;
34+
}
35+
36+
.icon {
37+
font-size: 64px;
38+
margin-bottom: 20px;
39+
color: #667eea;
40+
}
41+
42+
h1 {
43+
color: #333;
44+
margin-bottom: 20px;
45+
font-size: 24px;
46+
}
47+
48+
.description {
49+
color: #666;
50+
margin-bottom: 30px;
51+
line-height: 1.6;
52+
}
53+
54+
.btn {
55+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
56+
color: white;
57+
padding: 15px 30px;
58+
border: none;
59+
border-radius: 8px;
60+
font-size: 16px;
61+
font-weight: 600;
62+
cursor: pointer;
63+
transition: transform 0.2s;
64+
width: 100%;
65+
margin-top: 20px;
66+
}
67+
68+
.btn:hover {
69+
transform: translateY(-2px);
70+
}
71+
72+
.btn:disabled {
73+
opacity: 0.6;
74+
cursor: not-allowed;
75+
transform: none;
76+
}
77+
78+
.alert {
79+
padding: 15px;
80+
border-radius: 8px;
81+
margin-bottom: 20px;
82+
font-weight: 500;
83+
display: none;
84+
}
85+
86+
.alert-success {
87+
background: #d4edda;
88+
color: #155724;
89+
border: 1px solid #c3e6cb;
90+
}
91+
92+
.alert-error {
93+
background: #f8d7da;
94+
color: #721c24;
95+
border: 1px solid #f5c6cb;
96+
}
97+
98+
.loading {
99+
display: none;
100+
margin-top: 20px;
101+
}
102+
103+
.spinner {
104+
border: 2px solid #f3f3f3;
105+
border-top: 2px solid #667eea;
106+
border-radius: 50%;
107+
width: 20px;
108+
height: 20px;
109+
animation: spin 1s linear infinite;
110+
display: inline-block;
111+
margin-right: 10px;
112+
}
113+
114+
@keyframes spin {
115+
0% { transform: rotate(0deg); }
116+
100% { transform: rotate(360deg); }
117+
}
118+
</style>
119+
</head>
120+
<body>
121+
<div class="container">
122+
<div class="logo">🛍️ Hypex eCommerce</div>
123+
124+
<div class="icon"></div>
125+
126+
<h1>Account Activation</h1>
127+
128+
<div class="description">
129+
Welcome! Click the button below to activate your account and start shopping.
130+
</div>
131+
132+
<div id="messages" class="alert"></div>
133+
134+
<form id="activationForm" method="post" action="/auth/activate/">
135+
{% csrf_token %}
136+
<input type="hidden" name="uid" value="{{ uid }}">
137+
<input type="hidden" name="token" value="{{ token }}">
138+
139+
<button type="submit" class="btn" id="activateBtn">
140+
Activate Account
141+
</button>
142+
143+
<div class="loading" id="loading">
144+
<div class="spinner"></div>
145+
Activating your account...
146+
</div>
147+
</form>
148+
</div>
149+
150+
<script>
151+
document.getElementById('activationForm').addEventListener('submit', async function(e) {
152+
e.preventDefault();
153+
154+
const formData = new FormData(this);
155+
const uid = formData.get('uid');
156+
const token = formData.get('token');
157+
const activateBtn = document.getElementById('activateBtn');
158+
const loading = document.getElementById('loading');
159+
const messages = document.getElementById('messages');
160+
161+
// Clear previous messages
162+
messages.style.display = 'none';
163+
messages.className = 'alert';
164+
165+
// Show loading state
166+
activateBtn.disabled = true;
167+
loading.style.display = 'block';
168+
169+
try {
170+
const response = await fetch('/auth/activate/', {
171+
method: 'POST',
172+
headers: {
173+
'Content-Type': 'application/json',
174+
'X-CSRFToken': formData.get('csrfmiddlewaretoken')
175+
},
176+
body: JSON.stringify({ uid, token })
177+
});
178+
179+
if (response.ok || response.status === 204) {
180+
messages.className = 'alert alert-success';
181+
messages.innerHTML = '🎉 Account activated successfully! Redirecting to login...';
182+
messages.style.display = 'block';
183+
184+
// Redirect to login page after 2 seconds
185+
setTimeout(() => {
186+
window.location.href = '/admin/login/';
187+
}, 2000);
188+
189+
} else {
190+
const data = await response.json();
191+
const errorMessage = data.detail || data.error || 'Activation failed. Please try again.';
192+
messages.className = 'alert alert-error';
193+
messages.innerHTML = `❌ ${errorMessage}`;
194+
messages.style.display = 'block';
195+
}
196+
197+
} catch (error) {
198+
console.error('Activation error:', error);
199+
messages.className = 'alert alert-error';
200+
messages.innerHTML = '❌ Network error. Please check your connection and try again.';
201+
messages.style.display = 'block';
202+
} finally {
203+
// Hide loading state
204+
activateBtn.disabled = false;
205+
loading.style.display = 'none';
206+
}
207+
});
208+
</script>
209+
</body>
210+
</html>

0 commit comments

Comments
 (0)