views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import os
  2. import json
  3. import time
  4. import requests
  5. import uuid
  6. import threading
  7. import pandas as pd
  8. from bs4 import BeautifulSoup
  9. from django.shortcuts import get_object_or_404, redirect, render
  10. from django.core.files.storage import FileSystemStorage
  11. from django.http import JsonResponse
  12. from .models import TitleMapping, AttributeMaster,ProcessingTask # <--- THIS FIXES THE ERROR
  13. from django.conf import settings
  14. import cloudscraper
  15. from django.contrib import messages
  16. from django.contrib.auth import authenticate, login, logout
  17. # from django.contrib.auth.decorators import login_required
  18. from .decorators import login_required
  19. from django.contrib.auth.hashers import make_password
  20. # To login
  21. def login_view(request):
  22. if request.method == "POST":
  23. email = request.POST.get("username")
  24. password = request.POST.get("password")
  25. print("Email: ", email)
  26. print("Password: ", password)
  27. # Authenticate the user
  28. user = authenticate(request, username=email, password=password)
  29. print("user",user)
  30. if user is not None:
  31. print("User authenticated successfully.")
  32. login(request, user)
  33. request.session['user_email'] = user.email
  34. # request.session = user
  35. # request.session['full_name'] = f"{user.firstName} {user.lastName or ''}".strip()
  36. # # Store both human-readable role and code
  37. # request.session['role'] = user.get_role_display() # 'Super Admin', 'Admin', 'RTA'
  38. # request.session['role_code'] = user.role # '0', '1', '2'
  39. # request.session['joining_date'] = user.createdDate.strftime("%b, %Y")
  40. # request.session['userId'] = user.userId
  41. # 📌 Store client_id if user has a client associated
  42. # request.session['client_id'] = user.client.clientId if user.client else None
  43. return redirect('title_creator_home')
  44. else:
  45. print("Invalid credentials.")
  46. messages.error(request, "Invalid email or password.")
  47. return redirect('login')
  48. print("Rendering login page.")
  49. return render(request, 'login.html')
  50. # To logout
  51. @login_required
  52. def logout_view(request):
  53. logout(request)
  54. messages.success(request, "You have been logged out successfully.")
  55. return redirect('login')
  56. @login_required
  57. def master_config_view(request):
  58. if request.method == 'POST':
  59. action = request.POST.get('action')
  60. # Part 1: Add New Attribute
  61. if action == 'add_attribute':
  62. name = request.POST.get('attr_name')
  63. is_m = request.POST.get('is_mandatory') == 'on'
  64. if name:
  65. AttributeMaster.objects.get_or_create(name=name.strip(), defaults={'is_mandatory': is_m})
  66. # Part 2: Add New Title Mapping (Product Type)
  67. # --- MAPPING ACTIONS (CREATE & UPDATE) ---
  68. elif action in ['add_mapping', 'update_mapping']:
  69. pt = request.POST.get('pt_name')
  70. seq = request.POST.get('sequence')
  71. edit_id = request.POST.get('edit_id')
  72. if action == 'update_mapping' and edit_id:
  73. # Update existing
  74. mapping = get_object_or_404(TitleMapping, id=edit_id)
  75. mapping.product_type = pt.strip()
  76. mapping.format_sequence = seq
  77. mapping.save()
  78. else:
  79. # Create new (using get_or_create to prevent exact duplicates)
  80. if pt:
  81. TitleMapping.objects.get_or_create(
  82. product_type=pt.strip(),
  83. defaults={'format_sequence': seq}
  84. )
  85. # --- MAPPING DELETE ---
  86. elif action == 'delete_mapping':
  87. mapping_id = request.POST.get('id')
  88. TitleMapping.objects.filter(id=mapping_id).delete()
  89. # Part 3: Delete functionality
  90. elif action == 'delete_attribute':
  91. AttributeMaster.objects.filter(id=request.POST.get('id')).delete()
  92. return redirect('title_creator_master')
  93. # GET: Load all data
  94. context = {
  95. 'attributes': AttributeMaster.objects.all().order_by('name'),
  96. 'mappings': TitleMapping.objects.all().order_by('product_type'),
  97. }
  98. return render(request, 'title_creator_master.html', context)
  99. def save_config_api(request):
  100. if request.method == 'POST':
  101. try:
  102. data = json.loads(request.body)
  103. # Update Mandatory Attributes
  104. # Expected data: { "mandatory_ids": [1, 3, 5] }
  105. AttributeMaster.objects.all().update(is_mandatory=False)
  106. AttributeMaster.objects.filter(id__in=data.get('mandatory_ids', [])).update(is_mandatory=True)
  107. # Update Title Sequences
  108. # Expected data: { "mappings": [{"id": 1, "sequence": "Brand,Color"}] }
  109. for m in data.get('mappings', []):
  110. TitleMapping.objects.filter(id=m['id']).update(format_sequence=m['sequence'])
  111. return JsonResponse({'success': True})
  112. except Exception as e:
  113. return JsonResponse({'success': False, 'error': str(e)})
  114. # def extract_title_or_error(product,selected_pt):
  115. # # 1. Identify Product Type from JSON to fetch the correct Mapping
  116. # pt_name = selected_pt
  117. # try:
  118. # mapping = TitleMapping.objects.get(product_type=pt_name)
  119. # config_sequence = mapping.get_sequence_list()
  120. # except TitleMapping.DoesNotExist:
  121. # return f"No Title Configuration found for Product Type: {pt_name}"
  122. # # 2. Get Mandatory list from DB
  123. # mandatory_fields = list(AttributeMaster.objects.filter(is_mandatory=True).values_list('name', flat=True))
  124. # # 3. Data Extraction (Your logic)
  125. # extracted_data = {
  126. # "Brand": product.get("brand"),
  127. # "Product Type": pt_name
  128. # }
  129. # dimensions = {}
  130. # for group in product.get("attributeGroups", []):
  131. # for attr in group.get("attributes", []):
  132. # desc = attr.get("attributeDesc")
  133. # value = attr.get("attributeValue")
  134. # if desc == "Capacity":
  135. # extracted_data[desc] = f"Capacity {value}"
  136. # if desc in ["Door Type", "Capacity", "Color"]:
  137. # extracted_data[desc] = value
  138. # elif desc in ["Width", "Depth", "Height"]:
  139. # dimensions[desc] = value
  140. # if {"Width", "Depth", "Height"}.issubset(dimensions):
  141. # # extracted_data["Dimensions"] = f'{dimensions["Width"]} x {dimensions["Depth"]} x {dimensions["Height"]}'
  142. # w, d, h = dimensions["Width"], dimensions["Depth"], dimensions["Height"]
  143. # extracted_data["Dimensions"] = f'{w}"w x {d}"d x {h}"h'
  144. # # 4. Build Title and Check Mandatory Rules from DB
  145. # final_title_parts = []
  146. # missing_mandatory = []
  147. # for attr_name in config_sequence:
  148. # val = extracted_data.get(attr_name)
  149. # if not val or str(val).strip() == "":
  150. # # If DB says it's mandatory, track the error
  151. # if attr_name in mandatory_fields:
  152. # missing_mandatory.append(attr_name)
  153. # continue
  154. # final_title_parts.append(str(val))
  155. # # 5. Result
  156. # if missing_mandatory:
  157. # return f"Could not found {', '.join(missing_mandatory)} on Product Details page"
  158. # return " ".join(final_title_parts)
  159. def extract_title_or_error(product, selected_pt):
  160. # 1. Identify Product Type
  161. pt_name = selected_pt
  162. try:
  163. mapping = TitleMapping.objects.get(product_type=pt_name)
  164. config_sequence = mapping.get_sequence_list()
  165. except TitleMapping.DoesNotExist:
  166. return f"No Title Configuration found for Product Type: {pt_name}"
  167. mandatory_fields = list(AttributeMaster.objects.filter(is_mandatory=True).values_list('name', flat=True))
  168. # 2. Data Extraction
  169. extracted_data = {
  170. "Brand": product.get("brand")+"©",
  171. "Product Type": pt_name
  172. }
  173. dimensions = {}
  174. for group in product.get("attributeGroups", []):
  175. for attr in group.get("attributes", []):
  176. desc = attr.get("attributeDesc")
  177. val = attr.get("attributeValue")
  178. if desc == "Capacity":
  179. extracted_data[desc] = f"Capacity {val}"
  180. elif desc in ["Door Type", "Color"]:
  181. extracted_data[desc] = val
  182. elif desc in ["Width", "Depth", "Height"]:
  183. dimensions[desc] = val
  184. if {"Width", "Depth", "Height"}.issubset(dimensions):
  185. w, d, h = dimensions["Width"], dimensions["Depth"], dimensions["Height"]
  186. # We use .replace(" in", "") to remove the existing unit before adding the " symbol
  187. w = dimensions["Width"].replace(" in", "").strip()
  188. d = dimensions["Depth"].replace(" in", "").strip()
  189. h = dimensions["Height"].replace(" in", "").strip()
  190. extracted_data["Dimensions"] = f'{w}"w x {d}"d x {h}"h'
  191. # 3. Build Title Parts
  192. final_title_parts = []
  193. missing_mandatory = []
  194. for attr_name in config_sequence:
  195. val = extracted_data.get(attr_name)
  196. if not val or str(val).strip() == "":
  197. if attr_name in mandatory_fields:
  198. missing_mandatory.append(attr_name)
  199. continue
  200. final_title_parts.append(str(val))
  201. if missing_mandatory:
  202. return f"Could not found {', '.join(missing_mandatory)} on Product Details page"
  203. # Helper function to join parts: Brand PT, Param1, Param2
  204. def construct_string(parts):
  205. if len(parts) <= 2:
  206. return " ".join(parts)
  207. return f"{parts[0]} {parts[1]}, {', '.join(parts[2:])}"
  208. current_title = construct_string(final_title_parts)
  209. # 4. Length Reduction Logic (Step-by-Step)
  210. # Step 1: Change "Capacity" -> "Cap."
  211. if len(current_title) > 100:
  212. for i, part in enumerate(final_title_parts):
  213. if "Capacity" in part:
  214. final_title_parts[i] = part.replace("Capacity", "Cap.")
  215. current_title = construct_string(final_title_parts)
  216. # Step 2: Shorten Product Type (e.g., Stainless Steel -> SS)
  217. # Step B: Dynamic Product Type Acronym
  218. if len(current_title) > 100:
  219. pt_part = final_title_parts[1]
  220. words = pt_part.split()
  221. if len(words) > 1:
  222. # Takes first letter of every word in the Product Type
  223. final_title_parts[1] = "".join([w[0].upper() for w in words])
  224. current_title = construct_string(final_title_parts)
  225. # Step 3: Remove spaces from attributes starting from the back
  226. # Brand (0) and Product Type (1) are skipped
  227. if len(current_title) > 100:
  228. for i in range(len(final_title_parts) - 1, 1, -1):
  229. if len(current_title) <= 100:
  230. break
  231. # Remove white spaces from the current attribute part
  232. final_title_parts[i] = final_title_parts[i].replace(" ", "")
  233. current_title = construct_string(final_title_parts)
  234. return current_title
  235. def construct_dynamic_title(raw_data,selected_pt):
  236. try:
  237. product = raw_data.get("props", {}).get("pageProps", {}).get("product", {})
  238. if not product: return "Product data not found"
  239. return extract_title_or_error(product,selected_pt).strip()
  240. except Exception:
  241. return "Could not found attribute name on product details page"
  242. @login_required
  243. def title_creator_view(request):
  244. if request.method == 'POST' and request.FILES.get('file'):
  245. excel_file = request.FILES['file']
  246. selected_pt = request.POST.get('product_type')
  247. fs = FileSystemStorage()
  248. filename = fs.save(excel_file.name, excel_file)
  249. file_path = fs.path(filename)
  250. try:
  251. # 1. Read Excel
  252. df = pd.read_excel(file_path)
  253. # 2. Add the NEW COLUMN if it doesn't exist
  254. if 'New_Generated_Title' not in df.columns:
  255. df['New_Generated_Title'] = ""
  256. headers = {"User-Agent": "Mozilla/5.0"}
  257. results_for_ui = []
  258. # 3. Process each row
  259. for index, row in df.iterrows():
  260. url = row.get('URL') # Assumes your excel has a 'URL' column
  261. new_title = ""
  262. if pd.notna(url):
  263. try:
  264. resp = requests.get(url, headers=headers, timeout=10)
  265. soup = BeautifulSoup(resp.content, 'html.parser')
  266. script_tag = soup.find('script', id='__NEXT_DATA__')
  267. if script_tag:
  268. raw_data = json.loads(script_tag.string)
  269. new_title = construct_dynamic_title(raw_data,selected_pt)
  270. else:
  271. new_title = "Could not found attribute name on product details page"
  272. except:
  273. new_title = "Could not found attribute name on product details page"
  274. else:
  275. new_title = "URL Missing"
  276. # Update the DataFrame column for this row
  277. df.at[index, 'New_Generated_Title'] = new_title
  278. results_for_ui.append({
  279. "id" : index + 1,
  280. "url": url,
  281. "new_title": new_title,
  282. "status": True
  283. })
  284. time.sleep(1) # Safety delay
  285. # 4. Save the modified Excel to a new path
  286. output_filename = f"processed_{excel_file.name}"
  287. output_path = os.path.join(fs.location, output_filename)
  288. df.to_excel(output_path, index=False)
  289. return JsonResponse({
  290. 'success': True,
  291. 'results': results_for_ui,
  292. 'download_url': fs.url(output_filename)
  293. })
  294. finally:
  295. if os.path.exists(file_path): os.remove(file_path)
  296. # GET request: Fetch all product types for the dropdown
  297. product_types = TitleMapping.objects.all().values_list('product_type', flat=True)
  298. return render(request, 'title_creator_index.html', {'product_types': product_types})
  299. # return render(request, 'title_creator_index.html')
  300. def process_excel_task(file_path, selected_pt, task_id):
  301. # Retrieve the task record from the database
  302. scraper = cloudscraper.create_scraper() # This replaces requests.get
  303. task = ProcessingTask.objects.get(task_id=task_id)
  304. try:
  305. # 1. Read Excel
  306. df = pd.read_excel(file_path)
  307. # 2. Add the NEW COLUMN if it doesn't exist
  308. if 'New_Generated_Title' not in df.columns:
  309. df['New_Generated_Title'] = ""
  310. headers = {"User-Agent": "Mozilla/5.0"}
  311. # 3. Process each row
  312. for index, row in df.iterrows():
  313. url = row.get('URL')
  314. new_title = ""
  315. if pd.notna(url):
  316. try:
  317. # Scraping logic
  318. # resp = scraper.get(url, timeout=15)
  319. resp = requests.get(url, headers=headers, timeout=10)
  320. if resp.status_code == 200:
  321. soup = BeautifulSoup(resp.content, 'html.parser')
  322. script_tag = soup.find('script', id='__NEXT_DATA__')
  323. if script_tag:
  324. try:
  325. raw_data = json.loads(script_tag.string)
  326. # Calling your dynamic title helper
  327. new_title = construct_dynamic_title(raw_data, selected_pt)
  328. except Exception:
  329. new_title = "Data Parsing Error"
  330. else:
  331. new_title = "Could not found attribute name on product details page"
  332. else:
  333. new_title = f"HTTP Error: {resp.status_code}"
  334. except Exception:
  335. new_title = "Request Failed (Timeout/Connection)"
  336. else:
  337. new_title = "URL Missing"
  338. # Update the DataFrame
  339. df.at[index, 'New_Generated_Title'] = new_title
  340. # Optional: Sleep to prevent getting blocked by the server
  341. time.sleep(1)
  342. # 4. Save the modified Excel to the MEDIA folder
  343. output_filename = f"completed_{task_id}_{task.original_filename}"
  344. # Ensure media directory exists
  345. if not os.path.exists(settings.MEDIA_ROOT):
  346. os.makedirs(settings.MEDIA_ROOT)
  347. output_path = os.path.join(settings.MEDIA_ROOT, output_filename)
  348. df.to_excel(output_path, index=False)
  349. # 5. Final Status Update
  350. task.status = 'COMPLETED'
  351. # Construct the URL for the frontend to download
  352. task.download_url = f"{settings.MEDIA_URL}{output_filename}"
  353. task.save()
  354. except Exception as e:
  355. print(f"Critical Task Failure: {e}")
  356. task.status = 'FAILED'
  357. task.save()
  358. finally:
  359. # 6. Cleanup the temporary uploaded file
  360. if os.path.exists(file_path):
  361. os.remove(file_path)
  362. @login_required
  363. def title_creator_async_view(request):
  364. if request.method == 'POST' and request.FILES.get('file'):
  365. excel_file = request.FILES['file']
  366. selected_pt = request.POST.get('product_type')
  367. # 1. Save file temporarily
  368. fs = FileSystemStorage()
  369. filename = fs.save(f"temp_{uuid.uuid4().hex}_{excel_file.name}", excel_file)
  370. file_path = fs.path(filename)
  371. # 2. Create Task Record
  372. task_id = str(uuid.uuid4())
  373. ProcessingTask.objects.create(
  374. task_id=task_id,
  375. original_filename=excel_file.name,
  376. status='PENDING'
  377. )
  378. # 3. Start Background Thread
  379. thread = threading.Thread(
  380. target=process_excel_task,
  381. args=(file_path, selected_pt, task_id)
  382. )
  383. thread.start()
  384. return JsonResponse({
  385. 'status': 'started',
  386. 'task_id': task_id,
  387. 'message': 'File is processing in the background.'
  388. })
  389. return JsonResponse({'error': 'Invalid request'}, status=400)
  390. # 2. This view is called repeatedly by pollStatus() in your JS
  391. def check_status(request, task_id):
  392. # Look up the task in the database
  393. task = get_object_or_404(ProcessingTask, task_id=task_id)
  394. return JsonResponse({
  395. 'status': task.status, # 'PENDING', 'COMPLETED', or 'FAILED'
  396. 'file_name': task.original_filename,
  397. 'download_url': task.download_url # This will be null until status is COMPLETED
  398. })
  399. @login_required
  400. def title_creator_history_page(request):
  401. # Renders the HTML page
  402. return render(request, 'title_creator_history.html')
  403. @login_required
  404. def get_title_creator_tasks_json(request):
  405. # Returns the list of tasks as JSON for the history table
  406. tasks = ProcessingTask.objects.all().order_by('-created_at')[:50] # Latest 50 tasks
  407. data = []
  408. for t in tasks:
  409. data.append({
  410. 'task_id': t.task_id,
  411. 'filename': t.original_filename or "Unknown File",
  412. 'status': t.status,
  413. 'url': t.download_url,
  414. 'date': t.created_at.strftime("%d %b %Y, %I:%M %p")
  415. })
  416. return JsonResponse(data, safe=False)