Compare commits

...

2 Commits
main ... ai-dev

Author SHA1 Message Date
Flatlogic Bot
f3c8e0ee36 Autosave: 20260520-105030 2026-05-20 10:50:30 +00:00
Flatlogic Bot
2f63d1b7f1 Digital Bazar 2026-05-20 02:45:11 +00:00
7884 changed files with 1167192 additions and 869 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

1
.venv/bin/python Symbolic link
View File

@ -0,0 +1 @@
python3

1
.venv/bin/python3 Symbolic link
View File

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

1
.venv/bin/python3.11 Symbolic link
View File

@ -0,0 +1 @@
python3

1
.venv/lib64 Symbolic link
View File

@ -0,0 +1 @@
lib

5
.venv/pyvenv.cfg Normal file
View File

@ -0,0 +1,5 @@
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

17
db/config.php Normal file
View File

@ -0,0 +1,17 @@
<?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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,20 +1,88 @@
from django.contrib import admin from django.contrib import admin
from django.db.models import Q
from django.utils.html import format_html from django.utils.html import format_html
from accounts.models import Profile 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) @admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin): class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'is_seller', 'image_preview') 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')}),
)
@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): def image_preview(self, obj):
if obj.image: if obj.image:
return format_html( return format_html(
'<img src="{}" width="40" height="40" ' '<img src="{}" width="44" height="44" style="border-radius: 50%; object-fit: cover;" />',
'style="border-radius: 50%; object-fit: cover;" />', obj.image.url,
obj.image.url
) )
return "No Image" return 'No image'
image_preview.short_description = 'Image'

View File

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

View File

@ -0,0 +1,128 @@
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,27 +1,266 @@
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models import Profile
from .delivery import apply_profile_delivery_fields, derive_location_label, phone_has_valid_digits
from .models import Profile
class ProfileForm(forms.ModelForm):
first_name = forms.CharField(required=False, max_length=30)
last_name = forms.CharField(required=False, max_length=150) class DeliveryLocationValidationMixin:
email = forms.EmailField(required=False) def clean_phone(self):
phone = (self.cleaned_data.get('phone') or '').strip()
class Meta: if not phone_has_valid_digits(phone):
model = Profile raise forms.ValidationError('Please enter a valid phone number.')
fields = ['image', 'bio'] return phone
def save(self, commit=True): def clean_default_address(self):
profile = super().save(commit=False) address = (self.cleaned_data.get('default_address') or '').strip()
# update related user fields if not address:
user = profile.user raise forms.ValidationError('Please enter your delivery address.')
user.first_name = self.cleaned_data.get('first_name', user.first_name) return address
user.last_name = self.cleaned_data.get('last_name', user.last_name)
email = self.cleaned_data.get('email') def clean(self):
if email: cleaned_data = super().clean()
user.email = email latitude = cleaned_data.get('latitude')
if commit: longitude = cleaned_data.get('longitude')
user.save()
profile.save() if (latitude is None) != (longitude is None):
return profile 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):
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
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()
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

@ -0,0 +1,48 @@
# 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,31 +1,54 @@
from django.db import models from django.contrib.auth.models import User
from django.contrib.auth.models import User from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
def user_profile_upload_path(instance, filename): def user_profile_upload_path(instance, filename):
# Files will be uploaded to MEDIA_ROOT/profile_pics/user_<id>/<filename> # Files will be uploaded to MEDIA_ROOT/profile_pics/user_<id>/<filename>
return f'profile_pics/user_{instance.user.id}/{filename}' return f'profile_pics/user_{instance.user.id}/{filename}'
class Profile(models.Model): class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True, null=True) bio = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to=user_profile_upload_path, blank=True, null=True) image = models.ImageField(upload_to=user_profile_upload_path, blank=True, null=True)
is_seller = models.BooleanField(default=False) is_seller = models.BooleanField(default=False)
phone = models.CharField(max_length=30, blank=True, default='')
def __str__(self): location_label = models.CharField(max_length=255, blank=True, default='')
return f'Profile for {self.user.username}' 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)
@receiver(post_save, sender=User) location_accuracy_m = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
def ensure_profile_exists(sender, instance, created, **kwargs): location_updated_at = models.DateTimeField(blank=True, null=True)
if created:
Profile.objects.create(user=instance) def __str__(self):
else: return f'Profile for {self.user.username}'
# save existing profile to ensure any related signals run
try: @property
instance.profile.save() def short_location(self):
except Exception: if self.location_label and self.location_label.strip():
pass 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):
if created:
Profile.objects.create(user=instance)
else:
# save existing profile to ensure any related signals run
try:
instance.profile.save()
except Exception:
pass

View File

@ -1,3 +1,109 @@
from django.test import TestCase from django.contrib.auth import get_user_model
from django.test import TestCase
# Create your tests here. 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)

View File

@ -1,13 +1,39 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.db.models import Sum from django.db.models import Sum
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from orders.models import Order from orders.models import Order
from products.models import WishlistItem 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): def login_view(request):
if request.user.is_authenticated: if request.user.is_authenticated:
@ -24,6 +50,7 @@ def login_view(request):
if user: if user:
login(request, user) login(request, user)
_sync_delivery_location_session(request, user)
messages.success(request, f'Welcome back, {username}!') messages.success(request, f'Welcome back, {username}!')
return redirect('profile') return redirect('profile')
@ -37,41 +64,28 @@ def register_view(request):
return redirect('profile') return redirect('profile')
if request.method == 'POST': if request.method == 'POST':
username = request.POST.get('username', '').strip() form = RegistrationForm(request.POST)
password = request.POST.get('password', '').strip() if form.is_valid():
confirm_password = request.POST.get('confirm_password', '').strip() form.save()
email = request.POST.get('email', '').strip() messages.success(request, 'Account created successfully! Please log in.')
register_as_seller = request.POST.get('register_as_seller') == 'on' return redirect('login')
messages.error(request, 'Please correct the highlighted fields and try again.')
if not username or not password or not confirm_password: else:
return render(request, 'accounts/register.html', {'error': 'All fields are required', 'username': username, 'email': email, 'register_as_seller': register_as_seller}) form = RegistrationForm(initial={'register_as_seller': request.GET.get('seller') == '1'})
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')
return render( return render(
request, request,
'accounts/register.html', 'accounts/register.html',
{'register_as_seller': request.GET.get('seller') == '1'}, {
'form': form,
'register_as_seller': request.GET.get('seller') == '1',
},
) )
def logout_view(request): def logout_view(request):
logout(request) logout(request)
request.session.pop('delivery_location', None)
return redirect('/') return redirect('/')
@ -80,53 +94,53 @@ def profile_view(request):
user_orders = Order.objects.filter(user=request.user) user_orders = Order.objects.filter(user=request.user)
delivered_orders = user_orders.filter(status='Delivered') delivered_orders = user_orders.filter(status='Delivered')
recent_orders = user_orders.order_by('-created_at')[:5] 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 total_spent = delivered_orders.aggregate(total=Sum('total_price')).get('total') or 0
wishlist_count = WishlistItem.objects.filter(user=request.user).count() 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( return render(
request, request,
'accounts/profile.html', 'accounts/profile.html',
{ {
'user': request.user, 'user': request.user,
'profile': profile,
'orders_count': user_orders.count(), 'orders_count': user_orders.count(),
'delivered_count': delivered_orders.count(), 'delivered_count': delivered_orders.count(),
'pending_count': user_orders.exclude(status='Delivered').count(), 'pending_count': user_orders.exclude(status='Delivered').count(),
'wishlist_count': wishlist_count, 'wishlist_count': wishlist_count,
'total_spent': total_spent, 'total_spent': total_spent,
'recent_orders': recent_orders, 'recent_orders': recent_orders,
'recent_delivery_points': recent_delivery_points,
'profile_completion': profile_completion,
}, },
) )
@login_required @login_required
def edit_profile(request): def edit_profile(request):
from .forms import ProfileForm
profile = getattr(request.user, 'profile', None) profile = getattr(request.user, 'profile', None)
if profile is None: if profile is None:
# ensure profile exists
from .models import Profile
profile = Profile.objects.create(user=request.user) profile = Profile.objects.create(user=request.user)
if request.method == 'POST': if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=profile) form = ProfileForm(request.POST, request.FILES, instance=profile, user=request.user)
# 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(): if form.is_valid():
form.save() profile = form.save()
_sync_delivery_location_session(request, request.user)
messages.success(request, 'Profile updated successfully.') messages.success(request, 'Profile updated successfully.')
return redirect('profile') return redirect('profile')
else: messages.error(request, 'Please correct the errors below.')
messages.error(request, 'Please correct the errors below.')
else: else:
form = ProfileForm(instance=profile) form = ProfileForm(instance=profile, user=request.user)
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}) return render(request, 'accounts/edit_profile.html', {'form': form, 'profile': profile})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -1,8 +1,11 @@
from decimal import Decimal from decimal import Decimal
from django.db.models import Avg from django.contrib import messages
from django.db.models import Avg, Count, Q
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from accounts.forms import DeliveryPreferencesForm
from accounts.models import Profile
from products.models import Product from products.models import Product
@ -18,6 +21,26 @@ 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): def home(request):
trending_products = Product.objects.filter(featured=True).order_by('-created_at')[:4] trending_products = Product.objects.filter(featured=True).order_by('-created_at')[:4]
if not trending_products.exists(): if not trending_products.exists():
@ -40,24 +63,30 @@ def home(request):
if offer_deal: if offer_deal:
special_deals.append(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.', 'title': f'{len(discounted_products)} offers live',
'note': 'Browse products with extra savings today.', 'description': 'Discounts available across top Nepali categories.',
}) 'note': 'Browse products with extra savings today.',
}
)
if featured_count: if featured_count:
special_deals.append({ special_deals.append(
'title': 'Featured Seller Picks', {
'description': f'{featured_count} curated products from trusted sellers.', 'title': 'Featured Seller Picks',
'note': 'Popular with Nepali shoppers.', 'description': f'{featured_count} curated products from trusted sellers.',
}) 'note': 'Popular with Nepali shoppers.',
}
)
else: else:
special_deals.append({ special_deals.append(
'title': 'Daily Deals', {
'description': 'Fresh discount offers updated every day.', 'title': 'Daily Deals',
'note': 'Check back for more savings.', 'description': 'Fresh discount offers updated every day.',
}) 'note': 'Check back for more savings.',
}
)
advertised_products = sorted( advertised_products = sorted(
discounted_products, discounted_products,
@ -68,7 +97,7 @@ def home(request):
if not advertised_products: if not advertised_products:
advertised_products = list(trending_products[:4]) advertised_products = list(trending_products[:4])
for index, product in enumerate(advertised_products): for product in advertised_products:
savings = (product.price - product.discount_price) if product.discount_price else Decimal('0') 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 percent = int((savings / product.price * Decimal('100')).quantize(Decimal('1'))) if product.price else 0
if percent >= 50: if percent >= 50:
@ -83,6 +112,29 @@ def home(request):
label = 'Featured Deal' label = 'Featured Deal'
product.deal_label = label 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( return render(
request, request,
'core/home.html', 'core/home.html',
@ -94,6 +146,8 @@ def home(request):
'avg_rating': aggregates.get('avg_rating') or 0, 'avg_rating': aggregates.get('avg_rating') or 0,
'special_deals': special_deals, 'special_deals': special_deals,
'advertised_products': advertised_products, 'advertised_products': advertised_products,
'category_spotlights': _build_category_spotlights(),
'experience_highlights': experience_highlights,
}, },
) )
@ -128,12 +182,44 @@ def landing(request):
def settings_view(request): def settings_view(request):
if request.method == 'POST': location_form = None
delivery_location = request.POST.get('delivery_location', '').strip() guest_delivery_location = request.session.get('delivery_location', '').strip()
if delivery_location: profile = None
request.session['delivery_location'] = delivery_location
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
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: else:
request.session.pop('delivery_location', None) request.session.pop('delivery_location', None)
messages.info(request, 'Temporary delivery location cleared.')
return redirect('settings') return redirect('settings')
return render(request, 'core/settings.html') 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),
},
)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,69 +1,72 @@
""" """
Django settings for myproject project. Django settings for myproject project.
Generated by 'django-admin startproject' using Django 6.0.5. Generated by 'django-admin startproject' using Django 6.0.5.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/ https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/ https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
from pathlib import Path import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-%)ie75*7@1xl91^h6^$d!npm$=cf7@oqcc9b&jqxqc8t!@9uj1' # SECURITY WARNING: keep the secret key used in production secret!
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 # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', 'testserver']
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', 'testserver', '.flatlogic.app']
CSRF_TRUSTED_ORIGINS = ['https://*.flatlogic.app', 'http://*.flatlogic.app']
# Application definition SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth', # Application definition
'django.contrib.contenttypes',
'django.contrib.sessions', INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.admin',
'django.contrib.staticfiles', 'django.contrib.auth',
'core', # core app for home, about, contact 'django.contrib.contenttypes',
'accounts', # accounts app for user profiles 'django.contrib.sessions',
'cart', # cart app for shopping cart functionality 'django.contrib.messages',
'orders', # orders app for order management 'django.contrib.staticfiles',
'products', # products app for product management 'core', # core app for home, about, contact
'accounts', # accounts app for user profiles
] 'cart', # cart app for shopping cart functionality
'orders', # orders app for order management
MIDDLEWARE = [ 'products', # products app for product management
'django.middleware.security.SecurityMiddleware', ]
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
] 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
ROOT_URLCONF = 'myproject.urls' 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
TEMPLATES = [
{ ROOT_URLCONF = 'myproject.urls'
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'tempelates'], # ✅ FIX HERE TEMPLATES = [
'APP_DIRS': True, {
'OPTIONS': { 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'context_processors': [ 'DIRS': [BASE_DIR / 'tempelates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
@ -73,64 +76,100 @@ TEMPLATES = [
}, },
}, },
] ]
WSGI_APPLICATION = 'myproject.wsgi.application' WSGI_APPLICATION = 'myproject.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases # https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3',
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media' MEDIA_ROOT = BASE_DIR / 'media'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/ # https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/ # https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = '/static/'
STATICFILES_DIRS = [ STATICFILES_DIRS = [
BASE_DIR / "static", BASE_DIR / 'static',
] ]
# Authentication Settings # Authentication Settings
LOGIN_URL = 'login' LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'profile' LOGIN_REDIRECT_URL = 'profile'
LOGOUT_REDIRECT_URL = 'home' 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'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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