base setup
This commit is contained in:
0
at_django_boilerplate/utils/__init__.py
Executable file
0
at_django_boilerplate/utils/__init__.py
Executable file
BIN
at_django_boilerplate/utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
at_django_boilerplate/utils/__pycache__/mixins.cpython-312.pyc
Normal file
BIN
at_django_boilerplate/utils/__pycache__/mixins.cpython-312.pyc
Normal file
Binary file not shown.
2
at_django_boilerplate/utils/calendar_utils.py
Executable file
2
at_django_boilerplate/utils/calendar_utils.py
Executable file
@@ -0,0 +1,2 @@
|
||||
def has_google_calendar(user):
|
||||
return hasattr(user, 'google_calender') and bool(user.google_calender.token)
|
||||
175
at_django_boilerplate/utils/company_matching.py
Executable file
175
at_django_boilerplate/utils/company_matching.py
Executable file
@@ -0,0 +1,175 @@
|
||||
from rapidfuzz import fuzz
|
||||
from company_search.models import CompanySearch
|
||||
|
||||
from django.views.generic import View
|
||||
from django.http import JsonResponse
|
||||
|
||||
from django.conf import settings
|
||||
import random
|
||||
|
||||
CORPORATE_STOPWORDS = {
|
||||
"private", "pvt", "limited", "ltd", "llp", "company",
|
||||
"corp", "corporation", "india", "enterprises", "enterprise",
|
||||
"solutions", "product", "products"
|
||||
}
|
||||
|
||||
def clean_text(text: str) -> str:
|
||||
"""Remove common corporate words."""
|
||||
if not text:
|
||||
return ""
|
||||
words = text.lower().split()
|
||||
cleaned = [w for w in words if w not in CORPORATE_STOPWORDS]
|
||||
return " ".join(cleaned)
|
||||
|
||||
|
||||
def normalize_query(query: str) -> str:
|
||||
"""Ensure query includes 'Limited' if missing."""
|
||||
q = query.strip().lower()
|
||||
if "limited" not in q and "ltd" not in q and "pvt" not in q:
|
||||
q += " limited"
|
||||
return q
|
||||
|
||||
|
||||
MIN_SCORE = getattr(settings, "COMPANY_MATCHING_MIN_SCORE", 50)
|
||||
|
||||
def find_match(query: str, min_score: int = None):
|
||||
"""
|
||||
Loops through all companies and returns the best fuzzy match.
|
||||
|
||||
Returns:
|
||||
(best_company_object, score)
|
||||
OR (None, score) if score >= min_score (too similar)
|
||||
"""
|
||||
if min_score is None:
|
||||
min_score = MIN_SCORE
|
||||
|
||||
# Normalize and clean query
|
||||
q_clean = clean_text(normalize_query(query))
|
||||
|
||||
companies = CompanySearch.objects.all()
|
||||
|
||||
best_company = None
|
||||
best_score = 0
|
||||
|
||||
for company in companies:
|
||||
name = company.company_name or ""
|
||||
name_clean = clean_text(name)
|
||||
|
||||
score = fuzz.partial_ratio(q_clean, name_clean)
|
||||
|
||||
# pick the highest score
|
||||
if score > best_score:
|
||||
best_company = company
|
||||
best_score = score
|
||||
|
||||
# Reject if similarity too high
|
||||
if best_score >= min_score:
|
||||
return None, best_score
|
||||
|
||||
return best_company, best_score
|
||||
|
||||
|
||||
def find_company_match(query: str, min_score: int = None):
|
||||
"""
|
||||
Returns:
|
||||
(best_company_object, score)
|
||||
OR (None, score) when score >= cutoff (too similar)
|
||||
"""
|
||||
if min_score is None:
|
||||
min_score = MIN_SCORE
|
||||
return find_match(query, min_score)
|
||||
|
||||
|
||||
class CheckCompanyNameAvailability(View):
|
||||
MIN_SCORE = MIN_SCORE # gets default from settings
|
||||
|
||||
def get(self, request):
|
||||
company_name = request.GET.get('company_name', None)
|
||||
is_taken = False
|
||||
|
||||
if company_name:
|
||||
if CompanySearch.objects.filter(company_name__iexact=company_name).exists():
|
||||
is_taken = True
|
||||
else:
|
||||
best_company, best_score = find_match(company_name, min_score=self.MIN_SCORE)
|
||||
|
||||
if best_company is None and best_score >= self.MIN_SCORE:
|
||||
is_taken = True
|
||||
|
||||
data = {
|
||||
'is_taken': is_taken,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
|
||||
def generate_suggestions(original_name, num_suggestions=4):
|
||||
"""
|
||||
Generate intelligent, varied company name suggestions based on the original name.
|
||||
Avoids repetitive hardcoded patterns.
|
||||
"""
|
||||
if not original_name:
|
||||
return []
|
||||
|
||||
# Clean the original name
|
||||
name = original_name.strip().upper()
|
||||
|
||||
# Common Indian company suffixes to remove for better base name
|
||||
suffixes = [
|
||||
'PRIVATE LIMITED', 'PVT LTD', 'LTD', 'LIMITED', 'LLP', 'OPC',
|
||||
'INDIA PRIVATE LIMITED', 'TECHNOLOGIES', 'SOLUTIONS', 'VENTURES',
|
||||
'ENTERPRISES', 'SYSTEMS', 'LABS', 'DIGITAL', 'TECH'
|
||||
]
|
||||
|
||||
base_name = name
|
||||
for suffix in suffixes:
|
||||
if base_name.endswith(suffix):
|
||||
base_name = base_name.replace(suffix, '').strip()
|
||||
break
|
||||
|
||||
# Split into words for smarter mixing
|
||||
words = [word for word in base_name.split() if len(word) > 2]
|
||||
if not words:
|
||||
words = [base_name[:10]] # fallback
|
||||
|
||||
# Dynamic prefix/suffix lists (more variety, India-relevant)
|
||||
prefixes = ['Nex', 'Pro', 'Smart', 'Prime', 'Elite', 'Global', 'Alpha', 'Beta', 'Core', 'Zen', 'Apex', 'Nova', 'Viva', 'Omni', 'Eco', 'True', 'Pure', 'First', 'Best', 'Ultra', 'Mega', 'Neo']
|
||||
suffixes = ['Hub', 'Labs', 'Works', 'Craft', 'Forge', 'Nest', 'Space', 'Grid', 'Link', 'Wave', 'Spark', 'Pulse', 'Flow', 'Edge', 'Peak', 'Rise', 'Shift', 'Bridge', 'Path', 'Root', 'Source']
|
||||
domains = ['Tech', 'Digital', 'Info', 'Net', 'Web', 'Cloud', 'Data', 'Soft', 'Systems', 'Networks', 'Media', 'Vision', 'Horizon', 'Future']
|
||||
|
||||
legal_suffixes = ['PRIVATE LIMITED', 'PVT LTD', 'LIMITED', 'LLP']
|
||||
|
||||
suggestions = set() # Use set to avoid duplicates
|
||||
|
||||
# 1. Smart combinations
|
||||
if len(words) >= 1:
|
||||
main_word = random.choice(words)
|
||||
suggestions.add(f"{main_word} {random.choice(suffixes)} PRIVATE LIMITED")
|
||||
suggestions.add(f"{random.choice(prefixes)}{main_word} PRIVATE LIMITED")
|
||||
suggestions.add(f"{main_word}{random.choice(domains)} PRIVATE LIMITED")
|
||||
|
||||
# 2. Add year variations (common in India)
|
||||
current_year = 2026
|
||||
for year in [current_year, current_year + 1, current_year - 1]:
|
||||
suggestions.add(f"{base_name} {year} PRIVATE LIMITED")
|
||||
|
||||
# 3. Location-inspired (if name has city/state hint — optional future enhancement)
|
||||
# You can expand this later with user location
|
||||
|
||||
# 4. Premium feel
|
||||
premium = ['Capital', 'Holdings', 'Group', 'Industries', 'Corporation']
|
||||
suggestions.add(f"{base_name} {random.choice(premium)} PRIVATE LIMITED")
|
||||
|
||||
# 5. Modern trendy words
|
||||
trendy = ['Byte', 'Pixel', 'Quantum', 'Fusion', 'Synergy', 'Nexus', 'Vertex', 'Axis', 'Orbit']
|
||||
suggestions.add(f"{random.choice(trendy)} {base_name} PRIVATE LIMITED")
|
||||
|
||||
# Convert to list, shuffle for variety, limit
|
||||
suggestion_list = list(suggestions)
|
||||
random.shuffle(suggestion_list)
|
||||
|
||||
# Always end with legal suffix
|
||||
final_suggestions = [s.strip() for s in suggestion_list if s.strip()]
|
||||
|
||||
return final_suggestions[:num_suggestions]
|
||||
163
at_django_boilerplate/utils/custom_fields.py
Executable file
163
at_django_boilerplate/utils/custom_fields.py
Executable file
@@ -0,0 +1,163 @@
|
||||
from django.db import models
|
||||
from cryptography.fernet import InvalidToken
|
||||
from at_django_boilerplate.utils.hash_utils import hexdigest
|
||||
from at_django_boilerplate.utils.encryption_utils import get_fernet
|
||||
from django.db.models.signals import class_prepared
|
||||
from django.db import models
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
import json
|
||||
|
||||
class EncryptedSearchableTextField(models.TextField):
|
||||
|
||||
def __init__(self, *args, hash_field=None, **kwargs):
|
||||
self._provided_hash_field = hash_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name, private_only=False):
|
||||
super().contribute_to_class(cls, name, private_only)
|
||||
|
||||
self.hash_field = self._provided_hash_field or f"{self.attname}_hash"
|
||||
|
||||
# Register with class_prepared (SAFE)
|
||||
class_prepared.connect(self._finalize_model, sender=cls, weak=False)
|
||||
|
||||
def _finalize_model(self, sender, **kwargs):
|
||||
"""
|
||||
Runs AFTER Django has built the class, but BEFORE migrations finalize.
|
||||
SAFE because we avoid touching registry or relational fields.
|
||||
"""
|
||||
|
||||
# Use ONLY local_fields — NEVER get_fields() (unsafe before apps are loaded)
|
||||
existing = {f.name for f in sender._meta.local_fields}
|
||||
|
||||
# Add hash field dynamically
|
||||
if self.hash_field not in existing:
|
||||
sender.add_to_class(
|
||||
self.hash_field,
|
||||
models.CharField(
|
||||
max_length=64,
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=False
|
||||
),
|
||||
)
|
||||
|
||||
# Patch save() only once
|
||||
if not getattr(sender, "_encrypted_searchable_patched", False):
|
||||
|
||||
original_save = sender.save
|
||||
|
||||
def new_save(instance, *args, **kwargs):
|
||||
raw = getattr(instance, self.attname)
|
||||
if raw is not None:
|
||||
norm = raw.lower() if isinstance(raw, str) else str(raw)
|
||||
setattr(instance, self.hash_field, hexdigest(norm))
|
||||
return original_save(instance, *args, **kwargs)
|
||||
|
||||
sender.save = new_save
|
||||
sender._encrypted_searchable_patched = True
|
||||
|
||||
# -------------------------------------------
|
||||
# Encryption
|
||||
# -------------------------------------------
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
f = get_fernet()
|
||||
return f.encrypt(value.encode()).decode()
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
f = get_fernet()
|
||||
return f.decrypt(value.encode()).decode()
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
|
||||
|
||||
class EncryptedTextField(models.TextField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Use a key derived from SECRET_KEY, safe for Fernet (32 url-safe base64-encoded bytes)
|
||||
self.fernet = get_fernet()
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
return self.fernet.encrypt(value.encode()).decode()
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return self.fernet.decrypt(value.encode()).decode()
|
||||
except (InvalidToken, AttributeError):
|
||||
return value # In case it's not encrypted or corrupted
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return self.fernet.decrypt(value.encode()).decode()
|
||||
except (InvalidToken, AttributeError):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
|
||||
|
||||
class FlexibleField(models.Field):
|
||||
description = "Flexible typed field (text, bool, number, file)"
|
||||
|
||||
def __init__(self, *args, data_type='text', **kwargs):
|
||||
self.data_type = data_type
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'text'
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if self.data_type == 'file' and isinstance(value, str):
|
||||
return json.dumps({'type': 'file', 'value': value})
|
||||
|
||||
if self.data_type == 'bool':
|
||||
return json.dumps({'type': 'bool', 'value': bool(value)})
|
||||
|
||||
if self.data_type == 'number':
|
||||
try:
|
||||
return json.dumps({'type': 'number', 'value': float(value)})
|
||||
except ValueError:
|
||||
raise ValidationError(f"'{value}' is not a valid number")
|
||||
|
||||
return json.dumps({'type': 'text', 'value': str(value)})
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(value)
|
||||
except Exception:
|
||||
return value
|
||||
val_type = data.get('type')
|
||||
val = data.get('value')
|
||||
|
||||
if val_type == 'bool':
|
||||
return bool(val)
|
||||
if val_type == 'number':
|
||||
return float(val)
|
||||
return val
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if self.data_type == 'number' and not isinstance(value, (int, float)):
|
||||
raise ValidationError("This field only accepts numbers.")
|
||||
if self.data_type == 'bool' and not isinstance(value, bool):
|
||||
raise ValidationError("This field only accepts True/False.")
|
||||
super().validate(value, model_instance)
|
||||
24
at_django_boilerplate/utils/encryption_utils.py
Executable file
24
at_django_boilerplate/utils/encryption_utils.py
Executable file
@@ -0,0 +1,24 @@
|
||||
# encryption_utils.py
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
class EncryptionUtils:
|
||||
def __init__(self):
|
||||
# SECRET_KEY ko 32 bytes bana kar base64 encode karo
|
||||
key_bytes = settings.SECRET_KEY.encode()[:32]
|
||||
key = urlsafe_b64encode(key_bytes.ljust(32, b'\0'))
|
||||
self.f = Fernet(key)
|
||||
|
||||
def encrypt(self, data):
|
||||
return self.f.encrypt(data.encode()).decode()
|
||||
|
||||
def decrypt(self, data):
|
||||
return self.f.decrypt(data.encode()).decode()
|
||||
|
||||
|
||||
def get_fernet():
|
||||
# Same logic yaha bhi use kare
|
||||
key_bytes = settings.SECRET_KEY.encode()[:32]
|
||||
key = urlsafe_b64encode(key_bytes.ljust(32, b'\0'))
|
||||
return Fernet(key)
|
||||
57
at_django_boilerplate/utils/geolocation.py
Executable file
57
at_django_boilerplate/utils/geolocation.py
Executable file
@@ -0,0 +1,57 @@
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
from ipware import get_client_ip
|
||||
import socket
|
||||
|
||||
|
||||
def get_ip_and_country(request, default_country='in', default_currency='INR'):
|
||||
ip, is_routable = get_client_ip(request)
|
||||
|
||||
if not ip:
|
||||
return '127.0.0.1', default_country, default_currency
|
||||
|
||||
try:
|
||||
socket.inet_aton(ip)
|
||||
if ip.startswith(('127.', '192.168.', '10.', '172.')):
|
||||
return ip, default_country, default_currency
|
||||
except socket.error:
|
||||
return ip, default_country, default_currency
|
||||
|
||||
try:
|
||||
g = GeoIP2()
|
||||
country = g.country(ip)['country_code'].lower()
|
||||
currency_map = {
|
||||
'us': 'USD',
|
||||
'eu': 'EUR',
|
||||
'gb': 'GBP',
|
||||
'in': 'INR',
|
||||
'au': 'AUD',
|
||||
'ca': 'CAD',
|
||||
'jp': 'JPY',
|
||||
}
|
||||
currency = currency_map.get(country, default_currency)
|
||||
return ip, country, currency
|
||||
except Exception:
|
||||
return ip, default_country, default_currency
|
||||
|
||||
def get_city_and_country_from_ip(ip):
|
||||
g = GeoIP2()
|
||||
|
||||
tmp = {'country':'',
|
||||
'city':'',
|
||||
'latitude':'',
|
||||
'longitude':''}
|
||||
if ip!='127.0.0.1':
|
||||
|
||||
try:
|
||||
# country = g.country(ip)
|
||||
city = g.city(ip)
|
||||
# print(city)
|
||||
tmp['country'] = city['country_name']
|
||||
tmp['city'] = city['city']
|
||||
tmp['latitude'] = city['latitude']
|
||||
tmp['longitude'] = city['longitude']
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return tmp
|
||||
|
||||
18
at_django_boilerplate/utils/hash_utils.py
Executable file
18
at_django_boilerplate/utils/hash_utils.py
Executable file
@@ -0,0 +1,18 @@
|
||||
import hashlib
|
||||
from hashids import Hashids
|
||||
from django.conf import settings
|
||||
|
||||
def hexdigest(text):
|
||||
text_hash = hashlib.sha256(text.encode()).hexdigest()
|
||||
return text_hash
|
||||
|
||||
|
||||
|
||||
|
||||
HASHIDS = Hashids(salt=settings.SECRET_KEY, min_length=12) # or 8
|
||||
|
||||
def encode_id(pk):
|
||||
return HASHIDS.encode(pk)
|
||||
|
||||
def decode_slug(slug):
|
||||
return HASHIDS.decode(slug)[0] if HASHIDS.decode(slug) else None
|
||||
32
at_django_boilerplate/utils/mixins.py
Executable file
32
at_django_boilerplate/utils/mixins.py
Executable file
@@ -0,0 +1,32 @@
|
||||
# mixins.py
|
||||
from django.db import models
|
||||
from at_django_boilerplate.utils.hash_utils import encode_id
|
||||
import uuid
|
||||
|
||||
|
||||
class UUIDMixin(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class HashidSlugMixin(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
slug = models.SlugField(max_length=10, unique=True,null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# is_new = self.pk is None
|
||||
is_new = True
|
||||
super().save(*args, **kwargs)
|
||||
if is_new and not self.slug:
|
||||
self.slug = encode_id(self.pk)
|
||||
super().save(update_fields=['slug'])
|
||||
|
||||
def generate_slug(self):
|
||||
if not self.slug:
|
||||
self.slug = encode_id(self.pk)
|
||||
self.save(update_fields=['slug'])
|
||||
return self.slug
|
||||
Reference in New Issue
Block a user