Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3c8e0ee36 | ||
|
|
2f63d1b7f1 |
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
1
.venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
.venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
1
.venv/bin/python3.11
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
.venv/lib64
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
lib
|
||||||
5
.venv/pyvenv.cfg
Normal 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
@ -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;
|
||||||
|
}
|
||||||
BIN
myproject/accounts/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/admin.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/apps.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/delivery.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/forms.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/models.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/tests.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/urls.cpython-311.pyc
Normal file
BIN
myproject/accounts/__pycache__/views.cpython-311.pyc
Normal 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'
|
|
||||||
|
|||||||
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class AccountsConfig(AppConfig):
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'accounts'
|
name = 'accounts'
|
||||||
|
|||||||
128
myproject/accounts/delivery.py
Normal 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,
|
||||||
|
)
|
||||||
@ -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 .delivery import apply_profile_delivery_fields, derive_location_label, phone_has_valid_digits
|
||||||
from .models import Profile
|
from .models import Profile
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(forms.ModelForm):
|
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):
|
||||||
first_name = forms.CharField(required=False, max_length=30)
|
first_name = forms.CharField(required=False, max_length=30)
|
||||||
last_name = forms.CharField(required=False, max_length=150)
|
last_name = forms.CharField(required=False, max_length=150)
|
||||||
email = forms.EmailField(required=False)
|
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:
|
class Meta:
|
||||||
model = Profile
|
model = Profile
|
||||||
fields = ['image', 'bio']
|
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):
|
def save(self, commit=True):
|
||||||
profile = super().save(commit=False)
|
profile = super().save(commit=False)
|
||||||
# update related user fields
|
self.apply_location_to_profile(profile)
|
||||||
user = profile.user
|
|
||||||
user.first_name = self.cleaned_data.get('first_name', user.first_name)
|
user = self.user or profile.user
|
||||||
user.last_name = self.cleaned_data.get('last_name', user.last_name)
|
user.first_name = self.cleaned_data.get('first_name', '').strip()
|
||||||
email = self.cleaned_data.get('email')
|
user.last_name = self.cleaned_data.get('last_name', '').strip()
|
||||||
if email:
|
user.email = self.cleaned_data.get('email', '').strip()
|
||||||
user.email = email
|
|
||||||
if commit:
|
if commit:
|
||||||
user.save()
|
user.save()
|
||||||
profile.save()
|
profile.save()
|
||||||
return profile
|
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
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,5 +1,5 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -14,10 +14,33 @@ class Profile(models.Model):
|
|||||||
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='')
|
||||||
|
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):
|
def __str__(self):
|
||||||
return f'Profile for {self.user.username}'
|
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)
|
@receiver(post_save, sender=User)
|
||||||
def ensure_profile_exists(sender, instance, created, **kwargs):
|
def ensure_profile_exists(sender, instance, created, **kwargs):
|
||||||
|
|||||||
@ -1,3 +1,109 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
# Create your tests here.
|
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)
|
||||||
|
|||||||
@ -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()
|
|
||||||
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.')
|
messages.success(request, 'Account created successfully! Please log in.')
|
||||||
return redirect('login')
|
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(
|
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})
|
||||||
|
|||||||
BIN
myproject/cart/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/admin.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/apps.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/models.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/tests.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/urls.cpython-311.pyc
Normal file
BIN
myproject/cart/__pycache__/views.cpython-311.pyc
Normal file
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class CartConfig(AppConfig):
|
class CartConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'cart'
|
name = 'cart'
|
||||||
|
|||||||
BIN
myproject/cart/migrations/__pycache__/__init__.cpython-311.pyc
Normal 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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
myproject/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/admin.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/apps.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/context_processors.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/models.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/tests.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/urls.cpython-311.pyc
Normal file
BIN
myproject/core/__pycache__/views.cpython-311.pyc
Normal file
@ -142,7 +142,11 @@ 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:
|
||||||
|
profile = getattr(request.user, 'profile', None)
|
||||||
|
if profile and profile.short_location:
|
||||||
|
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()
|
last_address = Order.objects.filter(user=request.user).exclude(address='').order_by('-created_at').values_list('address', flat=True).first()
|
||||||
if last_address:
|
if last_address:
|
||||||
delivery_location = last_address.split('\n', 1)[0].strip()
|
delivery_location = last_address.split('\n', 1)[0].strip()
|
||||||
|
|||||||
BIN
myproject/core/migrations/__pycache__/__init__.cpython-311.pyc
Normal 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',
|
'title': f'{len(discounted_products)} offers live',
|
||||||
'description': 'Discounts available across top Nepali categories.',
|
'description': 'Discounts available across top Nepali categories.',
|
||||||
'note': 'Browse products with extra savings today.',
|
'note': 'Browse products with extra savings today.',
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if featured_count:
|
if featured_count:
|
||||||
special_deals.append({
|
special_deals.append(
|
||||||
|
{
|
||||||
'title': 'Featured Seller Picks',
|
'title': 'Featured Seller Picks',
|
||||||
'description': f'{featured_count} curated products from trusted sellers.',
|
'description': f'{featured_count} curated products from trusted sellers.',
|
||||||
'note': 'Popular with Nepali shoppers.',
|
'note': 'Popular with Nepali shoppers.',
|
||||||
})
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
special_deals.append({
|
special_deals.append(
|
||||||
|
{
|
||||||
'title': 'Daily Deals',
|
'title': 'Daily Deals',
|
||||||
'description': 'Fresh discount offers updated every day.',
|
'description': 'Fresh discount offers updated every day.',
|
||||||
'note': 'Check back for more savings.',
|
'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):
|
||||||
|
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':
|
if request.method == 'POST':
|
||||||
delivery_location = request.POST.get('delivery_location', '').strip()
|
location_form = DeliveryPreferencesForm(request.POST, instance=profile)
|
||||||
if delivery_location:
|
if location_form.is_valid():
|
||||||
request.session['delivery_location'] = delivery_location
|
profile = location_form.save()
|
||||||
|
if profile.short_location:
|
||||||
|
request.session['delivery_location'] = profile.short_location
|
||||||
else:
|
else:
|
||||||
request.session.pop('delivery_location', None)
|
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 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),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
BIN
myproject/media/products/checkout.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_7Tq3pK9.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_Apl5oIV.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_Fu8w9sg.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_OIU3UZr.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_UwESDAP.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_XwH82Ej.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_cHT6Jqv.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_kHc70mM.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_kXmZj1w.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_swmZZ69.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/checkout_tynTK9s.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_43wNxQz.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_6lHiVJy.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_Ant52cM.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_AuUXWqH.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_ETEIG5v.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_ElMt9Bz.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_Jr0cyVq.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_LLFVOs6.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_OEdan1B.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_P2RBysy.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_QD5Pj7T.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_R2JLv1Y.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_RHmtSgn.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_RoWLVcr.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_SaeMYQz.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_bbYiWAc.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_g0hum12.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_k2IZBN8.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_qrGyMS2.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_rCx1Jzm.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_rGz8cvy.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_tjnDy5f.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/products/product_vcUB9za.gif
Normal file
|
After Width: | Height: | Size: 43 B |
BIN
myproject/media/profile_pics/user_3/samanya_you.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
myproject/media/profile_pics/user_6/logo.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
myproject/myproject/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
myproject/myproject/__pycache__/settings.cpython-311.pyc
Normal file
BIN
myproject/myproject/__pycache__/urls.cpython-311.pyc
Normal file
BIN
myproject/myproject/__pycache__/wsgi.cpython-311.pyc
Normal file
@ -10,6 +10,7 @@ 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/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
@ -25,7 +26,10 @@ 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
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']
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@ -42,7 +46,6 @@ INSTALLED_APPS = [
|
|||||||
'cart', # cart app for shopping cart functionality
|
'cart', # cart app for shopping cart functionality
|
||||||
'orders', # orders app for order management
|
'orders', # orders app for order management
|
||||||
'products', # products app for product management
|
'products', # products app for product management
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -60,7 +63,7 @@ ROOT_URLCONF = 'myproject.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [BASE_DIR / 'tempelates'], # ✅ FIX HERE
|
'DIRS': [BASE_DIR / 'tempelates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@ -124,13 +127,49 @@ 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'
|
||||||
|
|||||||