Compare commits

..

No commits in common. "ai-dev" and "main" have entirely different histories.
ai-dev ... main

7884 changed files with 866 additions and 1167189 deletions

View File

View File

View File

@ -1 +0,0 @@
python3

View File

@ -1 +0,0 @@
/usr/bin/python3

View File

@ -1 +0,0 @@
python3

View File

@ -1 +0,0 @@
lib

View File

@ -1,5 +0,0 @@
home = /usr/bin
include-system-site-packages = false
version = 3.11.2
executable = /usr/bin/python3.11
command = /usr/bin/python3 -m venv /home/ubuntu/executor/workspace/.venv

View File

@ -1,17 +0,0 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_40045');
define('DB_USER', 'app_40045');
define('DB_PASS', '37ed550e-aefb-4c8d-a597-3c67c556d8fe');
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

View File

@ -1,88 +1,20 @@
from django.contrib import admin
from django.db.models import Q
from django.utils.html import format_html
from accounts.models import Profile
class GPSAvailabilityFilter(admin.SimpleListFilter):
title = 'GPS availability'
parameter_name = 'gps'
def lookups(self, request, model_admin):
return (
('yes', 'With GPS'),
('no', 'Manual only'),
)
def queryset(self, request, queryset):
if self.value() == 'yes':
return queryset.filter(latitude__isnull=False, longitude__isnull=False)
if self.value() == 'no':
return queryset.filter(Q(latitude__isnull=True) | Q(longitude__isnull=True))
return queryset
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = (
'user',
'delivery_readiness',
'phone',
'short_location',
'has_precise_location',
'location_updated_at',
'is_seller',
'image_preview',
)
list_select_related = ('user',)
search_fields = ('user__username', 'user__email', 'phone', 'location_label', 'default_address')
list_filter = ('is_seller', GPSAvailabilityFilter, 'location_updated_at')
readonly_fields = ('location_updated_at', 'image_preview', 'maps_link')
autocomplete_fields = ('user',)
fieldsets = (
('Account', {'fields': ('user', 'is_seller')}),
('Delivery defaults', {'fields': ('phone', 'location_label', 'default_address')}),
('GPS location', {'fields': ('latitude', 'longitude', 'location_accuracy_m', 'location_updated_at', 'maps_link')}),
('Profile details', {'fields': ('image', 'image_preview', 'bio')}),
)
list_display = ('user', 'is_seller', 'image_preview')
@admin.display(description='Delivery readiness')
def delivery_readiness(self, obj):
missing = []
if not obj.phone:
missing.append('phone')
if not obj.formatted_delivery_address:
missing.append('address')
if not obj.has_precise_location:
missing.append('GPS')
if not missing:
return 'Complete'
return 'Missing ' + ', '.join(missing)
@admin.display(description='Location')
def short_location(self, obj):
return obj.short_location or '-'
@admin.display(boolean=True, description='GPS')
def has_precise_location(self, obj):
return obj.has_precise_location
@admin.display(description='Map')
def maps_link(self, obj):
if not obj.has_precise_location:
return '-'
return format_html(
'<a href="https://maps.google.com/?q={},{}" target="_blank" rel="noopener">Open map</a>',
obj.latitude,
obj.longitude,
)
@admin.display(description='Image')
def image_preview(self, obj):
if obj.image:
return format_html(
'<img src="{}" width="44" height="44" style="border-radius: 50%; object-fit: cover;" />',
obj.image.url,
'<img src="{}" width="40" height="40" '
'style="border-radius: 50%; object-fit: cover;" />',
obj.image.url
)
return 'No image'
return "No Image"
image_preview.short_description = 'Image'

View File

@ -2,5 +2,4 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

View File

@ -1,128 +0,0 @@
from decimal import Decimal, InvalidOperation
import re
from django.utils import timezone
def phone_has_valid_digits(phone):
digits = re.sub(r'\D+', '', (phone or '').strip())
return 7 <= len(digits) <= 15
def first_address_line(address):
trimmed = (address or '').strip()
if not trimmed:
return ''
return trimmed.splitlines()[0].strip()
def derive_location_label(location_label='', address=''):
label = (location_label or '').strip()
if label:
return label[:255]
return first_address_line(address)[:255]
def parse_decimal_value(raw_value, *, field_label, minimum=None, maximum=None):
value = (raw_value or '').strip()
if not value:
return None
try:
parsed = Decimal(value)
except (InvalidOperation, TypeError):
raise ValueError(f'Invalid {field_label}.')
if minimum is not None and parsed < minimum:
raise ValueError(f'{field_label.capitalize()} must be at least {minimum}.')
if maximum is not None and parsed > maximum:
raise ValueError(f'{field_label.capitalize()} must be no more than {maximum}.')
return parsed
def stringify_decimal(value):
if value in {None, ''}:
return ''
return str(value)
def build_delivery_payload(
*,
full_name='',
phone='',
address='',
location_label='',
delivery_notes='',
latitude=None,
longitude=None,
location_accuracy_m=None,
save_as_default=True,
**extra,
):
payload = {
'full_name': (full_name or '').strip(),
'phone': (phone or '').strip(),
'address': (address or '').strip(),
'location_label': derive_location_label(location_label, address),
'delivery_notes': (delivery_notes or '').strip(),
'latitude': stringify_decimal(latitude),
'longitude': stringify_decimal(longitude),
'location_accuracy_m': stringify_decimal(location_accuracy_m),
'save_as_default': bool(save_as_default),
}
payload.update(extra)
return payload
def profile_delivery_changes(*, phone, address, location_label='', latitude=None, longitude=None, location_accuracy_m=None):
return {
'phone': (phone or '').strip(),
'default_address': (address or '').strip(),
'location_label': derive_location_label(location_label, address),
'latitude': latitude,
'longitude': longitude,
'location_accuracy_m': location_accuracy_m,
'location_updated_at': timezone.now() if latitude is not None and longitude is not None else None,
}
def apply_profile_delivery_fields(profile, **kwargs):
changes = profile_delivery_changes(**kwargs)
for field, value in changes.items():
setattr(profile, field, value)
return profile
def save_profile_delivery_defaults(profile, **kwargs):
if profile is None:
return []
changes = profile_delivery_changes(**kwargs)
update_fields = []
for field, value in changes.items():
if getattr(profile, field) != value:
setattr(profile, field, value)
update_fields.append(field)
if update_fields:
profile.save(update_fields=update_fields)
return update_fields
def delivery_payload_from_profile(profile, *, full_name='', **extra):
if profile is None:
return build_delivery_payload(full_name=full_name, **extra)
return build_delivery_payload(
full_name=full_name,
phone=profile.phone,
address=profile.formatted_delivery_address,
location_label=profile.short_location,
latitude=profile.latitude,
longitude=profile.longitude,
location_accuracy_m=profile.location_accuracy_m,
save_as_default=True,
**extra,
)

View File

@ -1,266 +1,27 @@
from django import forms
from django.contrib.auth.models import User
from .delivery import apply_profile_delivery_fields, derive_location_label, phone_has_valid_digits
from .models import Profile
class DeliveryLocationValidationMixin:
def clean_phone(self):
phone = (self.cleaned_data.get('phone') or '').strip()
if not phone_has_valid_digits(phone):
raise forms.ValidationError('Please enter a valid phone number.')
return phone
def clean_default_address(self):
address = (self.cleaned_data.get('default_address') or '').strip()
if not address:
raise forms.ValidationError('Please enter your delivery address.')
return address
def clean(self):
cleaned_data = super().clean()
latitude = cleaned_data.get('latitude')
longitude = cleaned_data.get('longitude')
if (latitude is None) != (longitude is None):
raise forms.ValidationError('GPS location is incomplete. Please retry the location button or save without GPS.')
if latitude is not None and not (-90 <= latitude <= 90):
raise forms.ValidationError('Latitude is outside the valid range.')
if longitude is not None and not (-180 <= longitude <= 180):
raise forms.ValidationError('Longitude is outside the valid range.')
location_accuracy_m = cleaned_data.get('location_accuracy_m')
if location_accuracy_m is not None and location_accuracy_m < 0:
raise forms.ValidationError('Location accuracy cannot be negative.')
if cleaned_data.get('default_address') and not cleaned_data.get('location_label'):
cleaned_data['location_label'] = derive_location_label('', cleaned_data['default_address'])
return cleaned_data
def apply_location_to_profile(self, profile):
return apply_profile_delivery_fields(
profile,
phone=self.cleaned_data.get('phone', ''),
location_label=self.cleaned_data.get('location_label', ''),
address=self.cleaned_data.get('default_address', ''),
latitude=self.cleaned_data.get('latitude'),
longitude=self.cleaned_data.get('longitude'),
location_accuracy_m=self.cleaned_data.get('location_accuracy_m'),
)
class RegistrationForm(DeliveryLocationValidationMixin, forms.Form):
username = forms.CharField(
min_length=3,
max_length=150,
widget=forms.TextInput(
attrs={
'placeholder': 'Choose a username',
'autocomplete': 'username',
}
),
)
first_name = forms.CharField(
required=False,
max_length=30,
widget=forms.TextInput(attrs={'placeholder': 'First name', 'autocomplete': 'given-name'}),
)
last_name = forms.CharField(
required=False,
max_length=150,
widget=forms.TextInput(attrs={'placeholder': 'Last name', 'autocomplete': 'family-name'}),
)
email = forms.EmailField(
required=False,
widget=forms.EmailInput(attrs={'placeholder': 'your@email.com', 'autocomplete': 'email'}),
)
password = forms.CharField(
min_length=6,
widget=forms.PasswordInput(attrs={'placeholder': 'At least 6 characters', 'autocomplete': 'new-password'}),
)
confirm_password = forms.CharField(
min_length=6,
widget=forms.PasswordInput(attrs={'placeholder': 'Confirm your password', 'autocomplete': 'new-password'}),
)
phone = forms.CharField(
max_length=30,
required=True,
widget=forms.TextInput(attrs={'placeholder': 'Phone number', 'autocomplete': 'tel'}),
)
location_label = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
'placeholder': 'Area / city / delivery label',
'autocomplete': 'address-level2',
}
),
)
default_address = forms.CharField(
required=True,
widget=forms.Textarea(
attrs={
'rows': 4,
'placeholder': 'Street, city, area, landmark',
'autocomplete': 'street-address',
}
),
)
latitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
location_accuracy_m = forms.DecimalField(required=False, max_digits=8, decimal_places=2, widget=forms.HiddenInput())
register_as_seller = forms.BooleanField(required=False)
def clean_username(self):
username = (self.cleaned_data.get('username') or '').strip()
if User.objects.filter(username__iexact=username).exists():
raise forms.ValidationError('Username already exists.')
return username
def clean_email(self):
email = (self.cleaned_data.get('email') or '').strip()
if email and User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError('Email already registered.')
return email
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm_password = cleaned_data.get('confirm_password')
if password and confirm_password and password != confirm_password:
self.add_error('confirm_password', 'Passwords do not match.')
return cleaned_data
def save(self):
user = User.objects.create_user(
username=self.cleaned_data['username'],
password=self.cleaned_data['password'],
email=self.cleaned_data.get('email', ''),
)
user.first_name = self.cleaned_data.get('first_name', '').strip()
user.last_name = self.cleaned_data.get('last_name', '').strip()
user.save(update_fields=['first_name', 'last_name', 'email'])
profile = user.profile
self.apply_location_to_profile(profile)
profile.is_seller = self.cleaned_data.get('register_as_seller', False)
profile.save()
return user
class ProfileForm(DeliveryLocationValidationMixin, forms.ModelForm):
class ProfileForm(forms.ModelForm):
first_name = forms.CharField(required=False, max_length=30)
last_name = forms.CharField(required=False, max_length=150)
email = forms.EmailField(required=False)
phone = forms.CharField(
max_length=30,
required=True,
widget=forms.TextInput(attrs={'placeholder': 'Phone number', 'autocomplete': 'tel'}),
)
location_label = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
'placeholder': 'Area / city / delivery label',
'autocomplete': 'address-level2',
}
),
)
default_address = forms.CharField(
required=True,
widget=forms.Textarea(
attrs={
'rows': 4,
'placeholder': 'Street, city, area, landmark',
'autocomplete': 'street-address',
}
),
)
latitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
location_accuracy_m = forms.DecimalField(required=False, max_digits=8, decimal_places=2, widget=forms.HiddenInput())
class Meta:
model = Profile
fields = ['image', 'bio', 'phone', 'location_label', 'default_address', 'latitude', 'longitude', 'location_accuracy_m']
widgets = {
'bio': forms.Textarea(attrs={'rows': 5, 'placeholder': 'Tell shoppers something about yourself'}),
}
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
self.user = user or getattr(self.instance, 'user', None)
if self.user:
self.fields['first_name'].initial = self.user.first_name
self.fields['last_name'].initial = self.user.last_name
self.fields['email'].initial = self.user.email
def clean_email(self):
email = (self.cleaned_data.get('email') or '').strip()
if email and User.objects.exclude(pk=getattr(self.user, 'pk', None)).filter(email__iexact=email).exists():
raise forms.ValidationError('That email is already being used by another account.')
return email
fields = ['image', 'bio']
def save(self, commit=True):
profile = super().save(commit=False)
self.apply_location_to_profile(profile)
user = self.user or profile.user
user.first_name = self.cleaned_data.get('first_name', '').strip()
user.last_name = self.cleaned_data.get('last_name', '').strip()
user.email = self.cleaned_data.get('email', '').strip()
# update related user fields
user = profile.user
user.first_name = self.cleaned_data.get('first_name', user.first_name)
user.last_name = self.cleaned_data.get('last_name', user.last_name)
email = self.cleaned_data.get('email')
if email:
user.email = email
if commit:
user.save()
profile.save()
return profile
class DeliveryPreferencesForm(DeliveryLocationValidationMixin, forms.ModelForm):
phone = forms.CharField(
max_length=30,
required=True,
widget=forms.TextInput(attrs={'placeholder': 'Phone number', 'autocomplete': 'tel'}),
)
location_label = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
'placeholder': 'Area / city / delivery label',
'autocomplete': 'address-level2',
}
),
)
default_address = forms.CharField(
required=True,
widget=forms.Textarea(
attrs={
'rows': 4,
'placeholder': 'Street, city, area, landmark',
'autocomplete': 'street-address',
}
),
)
latitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
longitude = forms.DecimalField(required=False, max_digits=9, decimal_places=6, widget=forms.HiddenInput())
location_accuracy_m = forms.DecimalField(required=False, max_digits=8, decimal_places=2, widget=forms.HiddenInput())
class Meta:
model = Profile
fields = ['phone', 'location_label', 'default_address', 'latitude', 'longitude', 'location_accuracy_m']
def save(self, commit=True):
profile = super().save(commit=False)
self.apply_location_to_profile(profile)
if commit:
profile.save()
return profile

View File

@ -1,48 +0,0 @@
# Generated by Django 5.2.14 on 2026-05-20 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_profile_is_seller'),
]
operations = [
migrations.AddField(
model_name='profile',
name='default_address',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='profile',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='profile',
name='location_accuracy_m',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
),
migrations.AddField(
model_name='profile',
name='location_label',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='profile',
name='location_updated_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='profile',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='profile',
name='phone',
field=models.CharField(blank=True, default='', max_length=30),
),
]

View File

@ -1,5 +1,5 @@
from django.contrib.auth.models import User
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
@ -14,33 +14,10 @@ class Profile(models.Model):
bio = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to=user_profile_upload_path, blank=True, null=True)
is_seller = models.BooleanField(default=False)
phone = models.CharField(max_length=30, blank=True, default='')
location_label = models.CharField(max_length=255, blank=True, default='')
default_address = models.TextField(blank=True, default='')
latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
location_accuracy_m = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
location_updated_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return f'Profile for {self.user.username}'
@property
def short_location(self):
if self.location_label and self.location_label.strip():
return self.location_label.strip()
if self.default_address and self.default_address.strip():
return self.default_address.splitlines()[0].strip()
return ''
@property
def formatted_delivery_address(self):
return (self.default_address or '').strip()
@property
def has_precise_location(self):
return self.latitude is not None and self.longitude is not None
@receiver(post_save, sender=User)
def ensure_profile_exists(sender, instance, created, **kwargs):

View File

@ -1,109 +1,3 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from accounts.forms import DeliveryPreferencesForm
User = get_user_model()
class AccountRegistrationTests(TestCase):
def test_registration_creates_profile_with_delivery_location(self):
response = self.client.post(
reverse('register'),
{
'username': 'newbuyer',
'first_name': 'New',
'last_name': 'Buyer',
'email': 'newbuyer@example.com',
'password': 'strongpass123',
'confirm_password': 'strongpass123',
'phone': '+9779800000001',
'location_label': 'Boudha, Kathmandu',
'default_address': 'Boudha Stupa Gate\nKathmandu',
'latitude': '27.721900',
'longitude': '85.361100',
'location_accuracy_m': '14.50',
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], reverse('login'))
user = User.objects.get(username='newbuyer')
self.assertEqual(user.first_name, 'New')
self.assertEqual(user.last_name, 'Buyer')
self.assertEqual(user.profile.phone, '+9779800000001')
self.assertEqual(user.profile.location_label, 'Boudha, Kathmandu')
self.assertEqual(user.profile.default_address, 'Boudha Stupa Gate\nKathmandu')
self.assertIsNotNone(user.profile.latitude)
self.assertIsNotNone(user.profile.longitude)
self.assertIsNotNone(user.profile.location_updated_at)
def test_registration_defaults_location_label_from_address_when_blank(self):
self.client.post(
reverse('register'),
{
'username': 'autolabel',
'first_name': 'Auto',
'last_name': 'Label',
'email': 'autolabel@example.com',
'password': 'strongpass123',
'confirm_password': 'strongpass123',
'phone': '+9779800000002',
'location_label': '',
'default_address': 'Imadol Height\nLalitpur',
},
)
user = User.objects.get(username='autolabel')
self.assertEqual(user.profile.location_label, 'Imadol Height')
def test_registration_requires_phone_and_address(self):
response = self.client.post(
reverse('register'),
{
'username': 'missinglocation',
'password': 'strongpass123',
'confirm_password': 'strongpass123',
'phone': '',
'default_address': '',
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Please correct the highlighted fields and try again.')
self.assertFalse(User.objects.filter(username='missinglocation').exists())
class DeliveryPreferencesFormTests(TestCase):
def test_saving_manual_address_clears_stale_gps_from_profile(self):
user = User.objects.create_user(username='gpsuser', password='password123')
profile = user.profile
profile.phone = '+9779811111111'
profile.location_label = 'Old Baneshwor'
profile.default_address = 'Old Address\nKathmandu'
profile.latitude = '27.700000'
profile.longitude = '85.300000'
profile.location_accuracy_m = '9.50'
profile.save()
form = DeliveryPreferencesForm(
data={
'phone': '+9779801234567',
'location_label': 'Manual only',
'default_address': 'New Road\nBhaktapur',
},
instance=profile,
)
self.assertTrue(form.is_valid(), form.errors)
form.save()
profile.refresh_from_db()
self.assertEqual(profile.phone, '+9779801234567')
self.assertEqual(profile.default_address, 'New Road\nBhaktapur')
self.assertIsNone(profile.latitude)
self.assertIsNone(profile.longitude)
self.assertIsNone(profile.location_accuracy_m)
self.assertIsNone(profile.location_updated_at)
# Create your tests here.

View File

@ -1,39 +1,13 @@
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.db.models import Sum
from django.shortcuts import redirect, render
from orders.models import Order
from products.models import WishlistItem
from .forms import ProfileForm, RegistrationForm
from .models import Profile
def _sync_delivery_location_session(request, user):
profile = getattr(user, 'profile', None)
if profile and profile.short_location:
request.session['delivery_location'] = profile.short_location
else:
request.session.pop('delivery_location', None)
def _recent_delivery_points(order_queryset, *, limit=3):
points = []
seen = set()
for order in order_queryset.exclude(address='').order_by('-created_at'):
key = (order.phone.strip(), order.address.strip(), order.location_label.strip())
if key in seen:
continue
seen.add(key)
points.append(order)
if len(points) >= limit:
break
return points
def login_view(request):
if request.user.is_authenticated:
@ -50,7 +24,6 @@ def login_view(request):
if user:
login(request, user)
_sync_delivery_location_session(request, user)
messages.success(request, f'Welcome back, {username}!')
return redirect('profile')
@ -64,28 +37,41 @@ def register_view(request):
return redirect('profile')
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
form.save()
username = request.POST.get('username', '').strip()
password = request.POST.get('password', '').strip()
confirm_password = request.POST.get('confirm_password', '').strip()
email = request.POST.get('email', '').strip()
register_as_seller = request.POST.get('register_as_seller') == 'on'
if not username or not password or not confirm_password:
return render(request, 'accounts/register.html', {'error': 'All fields are required', 'username': username, 'email': email, 'register_as_seller': register_as_seller})
if len(username) < 3:
return render(request, 'accounts/register.html', {'error': 'Username must be at least 3 characters long', 'username': username, 'email': email, 'register_as_seller': register_as_seller})
if len(password) < 6:
return render(request, 'accounts/register.html', {'error': 'Password must be at least 6 characters long', 'username': username, 'email': email, 'register_as_seller': register_as_seller})
if password != confirm_password:
return render(request, 'accounts/register.html', {'error': 'Passwords do not match', 'username': username, 'email': email, 'register_as_seller': register_as_seller})
if User.objects.filter(username=username).exists():
return render(request, 'accounts/register.html', {'error': 'Username already exists', 'email': email, 'register_as_seller': register_as_seller})
if email and User.objects.filter(email=email).exists():
return render(request, 'accounts/register.html', {'error': 'Email already registered', 'username': username, 'register_as_seller': register_as_seller})
user = User.objects.create_user(username=username, password=password, email=email)
if register_as_seller:
user.profile.is_seller = True
user.profile.save(update_fields=['is_seller'])
messages.success(request, 'Account created successfully! Please log in.')
return redirect('login')
messages.error(request, 'Please correct the highlighted fields and try again.')
else:
form = RegistrationForm(initial={'register_as_seller': request.GET.get('seller') == '1'})
return render(
request,
'accounts/register.html',
{
'form': form,
'register_as_seller': request.GET.get('seller') == '1',
},
{'register_as_seller': request.GET.get('seller') == '1'},
)
def logout_view(request):
logout(request)
request.session.pop('delivery_location', None)
return redirect('/')
@ -94,53 +80,53 @@ def profile_view(request):
user_orders = Order.objects.filter(user=request.user)
delivered_orders = user_orders.filter(status='Delivered')
recent_orders = user_orders.order_by('-created_at')[:5]
recent_delivery_points = _recent_delivery_points(user_orders, limit=3)
profile = getattr(request.user, 'profile', None)
total_spent = delivered_orders.aggregate(total=Sum('total_price')).get('total') or 0
wishlist_count = WishlistItem.objects.filter(user=request.user).count()
profile_checks = [
bool(request.user.email),
bool(profile and profile.phone),
bool(profile and profile.formatted_delivery_address),
bool(profile and profile.has_precise_location),
]
profile_completion = int(sum(profile_checks) / len(profile_checks) * 100) if profile_checks else 0
return render(
request,
'accounts/profile.html',
{
'user': request.user,
'profile': profile,
'orders_count': user_orders.count(),
'delivered_count': delivered_orders.count(),
'pending_count': user_orders.exclude(status='Delivered').count(),
'wishlist_count': wishlist_count,
'total_spent': total_spent,
'recent_orders': recent_orders,
'recent_delivery_points': recent_delivery_points,
'profile_completion': profile_completion,
},
)
@login_required
def edit_profile(request):
from .forms import ProfileForm
profile = getattr(request.user, 'profile', None)
if profile is None:
# ensure profile exists
from .models import Profile
profile = Profile.objects.create(user=request.user)
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=profile, user=request.user)
form = ProfileForm(request.POST, request.FILES, instance=profile)
# populate user fields into form for display/save
form.fields['first_name'].initial = request.user.first_name
form.fields['last_name'].initial = request.user.last_name
form.fields['email'].initial = request.user.email
if form.is_valid():
profile = form.save()
_sync_delivery_location_session(request, request.user)
form.save()
messages.success(request, 'Profile updated successfully.')
return redirect('profile')
else:
messages.error(request, 'Please correct the errors below.')
else:
form = ProfileForm(instance=profile, user=request.user)
form = ProfileForm(instance=profile)
form.fields['first_name'].initial = request.user.first_name
form.fields['last_name'].initial = request.user.last_name
form.fields['email'].initial = request.user.email
return render(request, 'accounts/edit_profile.html', {'form': form, 'profile': profile})

View File

@ -2,5 +2,4 @@ from django.apps import AppConfig
class CartConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cart'

View File

@ -120,16 +120,12 @@ def cart_view(request):
cart = get_cart(request)
products = []
subtotal = Decimal('0')
item_count = 0
saved_amount = Decimal('0')
for id, qty in cart.items():
product = get_object_or_404(Product, id=int(id))
product.qty = qty
product.subtotal = product.display_price * qty
subtotal += product.subtotal
item_count += qty
saved_amount += Decimal(str(product.savings or 0)) * qty
products.append(product)
shipping = Decimal('60') if products else Decimal('0')
@ -159,7 +155,5 @@ def cart_view(request):
'grand_total': grand_total,
'coupon_code': coupon_code,
'coupon': coupon,
'item_count': item_count,
'saved_amount': saved_amount,
},
)

View File

@ -142,11 +142,7 @@ def language_context(request):
seen.add(default_category['value'].lower())
delivery_location = request.session.get('delivery_location', '').strip()
if request.user.is_authenticated:
profile = getattr(request.user, 'profile', None)
if profile and profile.short_location:
delivery_location = profile.short_location
elif not delivery_location:
if not delivery_location and request.user.is_authenticated:
last_address = Order.objects.filter(user=request.user).exclude(address='').order_by('-created_at').values_list('address', flat=True).first()
if last_address:
delivery_location = last_address.split('\n', 1)[0].strip()

View File

@ -1,11 +1,8 @@
from decimal import Decimal
from django.contrib import messages
from django.db.models import Avg, Count, Q
from django.db.models import Avg
from django.shortcuts import redirect, render
from accounts.forms import DeliveryPreferencesForm
from accounts.models import Profile
from products.models import Product
@ -21,26 +18,6 @@ def _format_discount_offer(product):
}
def _build_category_spotlights():
top_categories = list(
Product.objects.exclude(category='')
.values('category')
.annotate(
total=Count('id'),
featured_total=Count('id', filter=Q(featured=True)),
avg_rating=Avg('rating'),
)
.order_by('-total', 'category')[:4]
)
for item in top_categories:
category_products = Product.objects.filter(category=item['category'])
item['avg_rating'] = item.get('avg_rating') or 0
item['best_price'] = min((product.display_price for product in category_products), default=Decimal('0'))
return top_categories
def home(request):
trending_products = Product.objects.filter(featured=True).order_by('-created_at')[:4]
if not trending_products.exists():
@ -63,30 +40,24 @@ def home(request):
if offer_deal:
special_deals.append(offer_deal)
special_deals.append(
{
special_deals.append({
'title': f'{len(discounted_products)} offers live',
'description': 'Discounts available across top Nepali categories.',
'note': 'Browse products with extra savings today.',
}
)
})
if featured_count:
special_deals.append(
{
special_deals.append({
'title': 'Featured Seller Picks',
'description': f'{featured_count} curated products from trusted sellers.',
'note': 'Popular with Nepali shoppers.',
}
)
})
else:
special_deals.append(
{
special_deals.append({
'title': 'Daily Deals',
'description': 'Fresh discount offers updated every day.',
'note': 'Check back for more savings.',
}
)
})
advertised_products = sorted(
discounted_products,
@ -97,7 +68,7 @@ def home(request):
if not advertised_products:
advertised_products = list(trending_products[:4])
for product in advertised_products:
for index, product in enumerate(advertised_products):
savings = (product.price - product.discount_price) if product.discount_price else Decimal('0')
percent = int((savings / product.price * Decimal('100')).quantize(Decimal('1'))) if product.price else 0
if percent >= 50:
@ -112,29 +83,6 @@ def home(request):
label = 'Featured Deal'
product.deal_label = label
experience_highlights = [
{
'title': 'Verified payments',
'description': 'Orders are only marked paid after wallet or card verification is completed on the server.',
'badge': 'Trusted checkout',
},
{
'title': 'Flexible payment flow',
'description': 'Customers can start with cash on delivery and later switch to eSewa or Khalti before shipment.',
'badge': 'Wallet ready',
},
{
'title': 'Smart product discovery',
'description': 'Search, categories, sorting, wishlist, and featured filters make it easier to find the right item fast.',
'badge': 'Fast browsing',
},
{
'title': 'Trackable orders',
'description': 'Order status, payment status, and next actions are visible from one clean dashboard-style experience.',
'badge': 'Clear visibility',
},
]
return render(
request,
'core/home.html',
@ -146,8 +94,6 @@ def home(request):
'avg_rating': aggregates.get('avg_rating') or 0,
'special_deals': special_deals,
'advertised_products': advertised_products,
'category_spotlights': _build_category_spotlights(),
'experience_highlights': experience_highlights,
},
)
@ -182,44 +128,12 @@ def landing(request):
def settings_view(request):
location_form = None
guest_delivery_location = request.session.get('delivery_location', '').strip()
profile = None
if request.user.is_authenticated:
profile = getattr(request.user, 'profile', None)
if profile is None:
profile = Profile.objects.create(user=request.user)
if request.method == 'POST':
location_form = DeliveryPreferencesForm(request.POST, instance=profile)
if location_form.is_valid():
profile = location_form.save()
if profile.short_location:
request.session['delivery_location'] = profile.short_location
delivery_location = request.POST.get('delivery_location', '').strip()
if delivery_location:
request.session['delivery_location'] = delivery_location
else:
request.session.pop('delivery_location', None)
messages.success(request, 'Delivery preferences updated successfully.')
return redirect('settings')
messages.error(request, 'Please correct the location details below.')
else:
location_form = DeliveryPreferencesForm(instance=profile)
elif request.method == 'POST':
guest_delivery_location = request.POST.get('delivery_location', '').strip()
if guest_delivery_location:
request.session['delivery_location'] = guest_delivery_location
messages.success(request, 'Temporary delivery location saved for this browser.')
else:
request.session.pop('delivery_location', None)
messages.info(request, 'Temporary delivery location cleared.')
return redirect('settings')
return render(
request,
'core/settings.html',
{
'location_form': location_form,
'guest_delivery_location': guest_delivery_location,
'location_saved_at': getattr(profile, 'location_updated_at', None),
},
)
return render(request, 'core/settings.html')

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -10,7 +10,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -26,10 +25,7 @@ SECRET_KEY = 'django-insecure-%)ie75*7@1xl91^h6^$d!npm$=cf7@oqcc9b&jqxqc8t!@9uj1
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', 'testserver', '.flatlogic.app']
CSRF_TRUSTED_ORIGINS = ['https://*.flatlogic.app', 'http://*.flatlogic.app']
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', 'testserver']
# Application definition
@ -46,6 +42,7 @@ INSTALLED_APPS = [
'cart', # cart app for shopping cart functionality
'orders', # orders app for order management
'products', # products app for product management
]
MIDDLEWARE = [
@ -63,7 +60,7 @@ ROOT_URLCONF = 'myproject.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'tempelates'],
'DIRS': [BASE_DIR / 'tempelates'], # ✅ FIX HERE
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -127,49 +124,13 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / "static",
]
# Authentication Settings
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'profile'
LOGOUT_REDIRECT_URL = 'home'
# Payment configuration
PUBLIC_APP_URL = os.getenv('PUBLIC_APP_URL', '').strip().rstrip('/')
PAYMENT_CURRENCY = (os.getenv('PAYMENT_CURRENCY', 'NPR').strip() or 'NPR').upper()
# Stripe fallback for card payments
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '').strip()
STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET', '').strip()
STRIPE_CURRENCY = (os.getenv('STRIPE_CURRENCY', PAYMENT_CURRENCY.lower()).strip() or PAYMENT_CURRENCY.lower()).lower()
# eSewa ePay (defaults to official UAT credentials in this dev environment)
ESEWA_SANDBOX = (os.getenv('ESEWA_SANDBOX', '1').strip() or '1').lower() not in {'0', 'false', 'no'}
ESEWA_PRODUCT_CODE = os.getenv('ESEWA_PRODUCT_CODE', 'EPAYTEST' if ESEWA_SANDBOX else '').strip()
ESEWA_SECRET_KEY = os.getenv('ESEWA_SECRET_KEY', '8gBm/:&EnhH.1/q' if ESEWA_SANDBOX else '').strip()
ESEWA_FORM_URL = os.getenv(
'ESEWA_FORM_URL',
'https://rc-epay.esewa.com.np/api/epay/main/v2/form' if ESEWA_SANDBOX else 'https://epay.esewa.com.np/api/epay/main/v2/form',
).strip()
ESEWA_STATUS_URL = os.getenv(
'ESEWA_STATUS_URL',
'https://rc.esewa.com.np/api/epay/transaction/status/' if ESEWA_SANDBOX else 'https://epay.esewa.com.np/api/epay/transaction/status/',
).strip()
# Khalti KPG-2
KHALTI_SANDBOX = (os.getenv('KHALTI_SANDBOX', '1').strip() or '1').lower() not in {'0', 'false', 'no'}
KHALTI_SECRET_KEY = os.getenv('KHALTI_SECRET_KEY', '').strip()
KHALTI_INITIATE_URL = os.getenv(
'KHALTI_INITIATE_URL',
'https://dev.khalti.com/api/v2/epayment/initiate/' if KHALTI_SANDBOX else 'https://khalti.com/api/v2/epayment/initiate/',
).strip()
KHALTI_LOOKUP_URL = os.getenv(
'KHALTI_LOOKUP_URL',
'https://dev.khalti.com/api/v2/epayment/lookup/' if KHALTI_SANDBOX else 'https://khalti.com/api/v2/epayment/lookup/',
).strip()
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

Some files were not shown because too many files have changed in this diff Show More