views.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  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. import random
  21. from rest_framework import status
  22. from rest_framework.views import APIView
  23. from django.utils import timezone
  24. import logging
  25. from zoneinfo import ZoneInfo
  26. logger = logging.getLogger(__name__)
  27. # To login
  28. def login_view(request):
  29. if request.method == "POST":
  30. email = request.POST.get("username")
  31. password = request.POST.get("password")
  32. print("Email: ", email)
  33. print("Password: ", password)
  34. # Authenticate the user
  35. user = authenticate(request, username=email, password=password)
  36. print("user",user)
  37. if user is not None:
  38. print("User authenticated successfully.")
  39. login(request, user)
  40. request.session['user_email'] = user.email
  41. # request.session = user
  42. # request.session['full_name'] = f"{user.firstName} {user.lastName or ''}".strip()
  43. # # Store both human-readable role and code
  44. # request.session['role'] = user.get_role_display() # 'Super Admin', 'Admin', 'RTA'
  45. # request.session['role_code'] = user.role # '0', '1', '2'
  46. # request.session['joining_date'] = user.createdDate.strftime("%b, %Y")
  47. # request.session['userId'] = user.userId
  48. # 📌 Store client_id if user has a client associated
  49. # request.session['client_id'] = user.client.clientId if user.client else None
  50. return redirect('title_creator_home')
  51. else:
  52. print("Invalid credentials.")
  53. messages.error(request, "Invalid email or password.")
  54. return redirect('login')
  55. print("Rendering login page.")
  56. return render(request, 'login.html')
  57. # To logout
  58. @login_required
  59. def logout_view(request):
  60. logout(request)
  61. messages.success(request, "You have been logged out successfully.")
  62. return redirect('login')
  63. @login_required
  64. def master_config_view(request):
  65. if request.method == 'POST':
  66. action = request.POST.get('action')
  67. # Part 1: Add New Attribute
  68. if action == 'add_attribute':
  69. name = request.POST.get('attr_name')
  70. is_m = request.POST.get('is_mandatory') == 'on'
  71. if name:
  72. AttributeMaster.objects.get_or_create(name=name.strip(), defaults={'is_mandatory': is_m})
  73. # Part 2: Add New Title Mapping (Product Type)
  74. # --- MAPPING ACTIONS (CREATE & UPDATE) ---
  75. elif action in ['add_mapping', 'update_mapping']:
  76. pt = request.POST.get('pt_name')
  77. seq = request.POST.get('sequence')
  78. edit_id = request.POST.get('edit_id')
  79. if action == 'update_mapping' and edit_id:
  80. # Update existing
  81. mapping = get_object_or_404(TitleMapping, id=edit_id)
  82. mapping.product_type = pt.strip()
  83. mapping.format_sequence = seq
  84. mapping.save()
  85. else:
  86. # Create new (using get_or_create to prevent exact duplicates)
  87. if pt:
  88. TitleMapping.objects.get_or_create(
  89. product_type=pt.strip(),
  90. defaults={'format_sequence': seq}
  91. )
  92. # --- MAPPING DELETE ---
  93. elif action == 'delete_mapping':
  94. mapping_id = request.POST.get('id')
  95. TitleMapping.objects.filter(id=mapping_id).delete()
  96. # Part 3: Delete functionality
  97. elif action == 'delete_attribute':
  98. AttributeMaster.objects.filter(id=request.POST.get('id')).delete()
  99. return redirect('title_creator_master')
  100. # GET: Load all data
  101. context = {
  102. 'attributes': AttributeMaster.objects.all().order_by('name'),
  103. 'mappings': TitleMapping.objects.all().order_by('product_type'),
  104. }
  105. return render(request, 'title_creator_master.html', context)
  106. def save_config_api(request):
  107. if request.method == 'POST':
  108. try:
  109. data = json.loads(request.body)
  110. # Update Mandatory Attributes
  111. # Expected data: { "mandatory_ids": [1, 3, 5] }
  112. AttributeMaster.objects.all().update(is_mandatory=False)
  113. AttributeMaster.objects.filter(id__in=data.get('mandatory_ids', [])).update(is_mandatory=True)
  114. # Update Title Sequences
  115. # Expected data: { "mappings": [{"id": 1, "sequence": "Brand,Color"}] }
  116. for m in data.get('mappings', []):
  117. TitleMapping.objects.filter(id=m['id']).update(format_sequence=m['sequence'])
  118. return JsonResponse({'success': True})
  119. except Exception as e:
  120. return JsonResponse({'success': False, 'error': str(e)})
  121. # def extract_title_or_error(product,selected_pt):
  122. # # 1. Identify Product Type from JSON to fetch the correct Mapping
  123. # pt_name = selected_pt
  124. # try:
  125. # mapping = TitleMapping.objects.get(product_type=pt_name)
  126. # config_sequence = mapping.get_sequence_list()
  127. # except TitleMapping.DoesNotExist:
  128. # return f"No Title Configuration found for Product Type: {pt_name}"
  129. # # 2. Get Mandatory list from DB
  130. # mandatory_fields = list(AttributeMaster.objects.filter(is_mandatory=True).values_list('name', flat=True))
  131. # # 3. Data Extraction (Your logic)
  132. # extracted_data = {
  133. # "Brand": product.get("brand"),
  134. # "Product Type": pt_name
  135. # }
  136. # dimensions = {}
  137. # for group in product.get("attributeGroups", []):
  138. # for attr in group.get("attributes", []):
  139. # desc = attr.get("attributeDesc")
  140. # value = attr.get("attributeValue")
  141. # if desc == "Capacity":
  142. # extracted_data[desc] = f"Capacity {value}"
  143. # if desc in ["Door Type", "Capacity", "Color"]:
  144. # extracted_data[desc] = value
  145. # elif desc in ["Width", "Depth", "Height"]:
  146. # dimensions[desc] = value
  147. # if {"Width", "Depth", "Height"}.issubset(dimensions):
  148. # # extracted_data["Dimensions"] = f'{dimensions["Width"]} x {dimensions["Depth"]} x {dimensions["Height"]}'
  149. # w, d, h = dimensions["Width"], dimensions["Depth"], dimensions["Height"]
  150. # extracted_data["Dimensions"] = f'{w}"w x {d}"d x {h}"h'
  151. # # 4. Build Title and Check Mandatory Rules from DB
  152. # final_title_parts = []
  153. # missing_mandatory = []
  154. # for attr_name in config_sequence:
  155. # val = extracted_data.get(attr_name)
  156. # if not val or str(val).strip() == "":
  157. # # If DB says it's mandatory, track the error
  158. # if attr_name in mandatory_fields:
  159. # missing_mandatory.append(attr_name)
  160. # continue
  161. # final_title_parts.append(str(val))
  162. # # 5. Result
  163. # if missing_mandatory:
  164. # return f"Could not found {', '.join(missing_mandatory)} on Product Details page"
  165. # return " ".join(final_title_parts)
  166. def extract_title_or_error(product, selected_pt):
  167. # 1. Identify Product Type
  168. pt_name = selected_pt
  169. logger.info(f"IN extract_title_or_error")
  170. try:
  171. mapping = TitleMapping.objects.get(product_type=pt_name)
  172. config_sequence = mapping.get_sequence_list()
  173. except TitleMapping.DoesNotExist:
  174. return None,f"No Title Configuration found for Product Type: {pt_name}"
  175. mandatory_fields = list(AttributeMaster.objects.filter(is_mandatory=True).values_list('name', flat=True))
  176. # Loop through each group (e.g., Weights & Dimensions, Product Details)
  177. product_type = None
  178. # for product type
  179. for group in product.get("attributeGroups", []):
  180. # Loop through each attribute in that group
  181. for attr in group.get("attributes", []):
  182. if attr.get("attributeDesc") == "Type":
  183. product_type = attr.get("attributeValue")
  184. break # Stop searching once found
  185. # 2. Data Extraction
  186. extracted_data = {
  187. "Brand": product.get("brand"),
  188. "Product Type": product_type
  189. }
  190. dimensions = {}
  191. for group in product.get("attributeGroups", []):
  192. for attr in group.get("attributes", []):
  193. desc = attr.get("attributeDesc")
  194. val = attr.get("attributeValue")
  195. if desc == "Capacity":
  196. extracted_data[desc] = f"{val} Capacity"
  197. elif desc in ["Door Type", "Color"]:
  198. extracted_data[desc] = val
  199. elif desc in ["Width", "Depth", "Height"]:
  200. dimensions[desc] = val
  201. if {"Width", "Depth", "Height"}.issubset(dimensions):
  202. w, d, h = dimensions["Width"], dimensions["Depth"], dimensions["Height"]
  203. # We use .replace(" in", "") to remove the existing unit before adding the " symbol
  204. w = dimensions["Width"].replace(" in", "").strip()
  205. d = dimensions["Depth"].replace(" in", "").strip()
  206. h = dimensions["Height"].replace(" in", "").strip()
  207. extracted_data["Dimensions"] = f'{w}"W x {d}"D x {h}"H'
  208. # 3. Build Title Parts
  209. final_title_parts = []
  210. missing_mandatory = []
  211. for attr_name in config_sequence:
  212. val = extracted_data.get(attr_name)
  213. if not val or str(val).strip() == "":
  214. if attr_name in mandatory_fields:
  215. missing_mandatory.append(attr_name)
  216. continue
  217. final_title_parts.append(str(val))
  218. comment = None
  219. if missing_mandatory:
  220. comment = f"Could not found {', '.join(missing_mandatory)} on Product Details page"
  221. # return f"Could not found {', '.join(missing_mandatory)} on Product Details page"
  222. # Helper function to join parts: Brand PT, Param1, Param2
  223. def construct_string(parts):
  224. if len(parts) <= 2:
  225. return " ".join(parts)
  226. return f"{parts[0]} {parts[1]}, {', '.join(parts[2:])}"
  227. current_title = construct_string(final_title_parts)
  228. # 4. Length Reduction Logic (Step-by-Step)
  229. print("Current Title 1 ######## ",current_title,len(current_title))
  230. logger.info(f"Current Title 1 Initial ########,{current_title},{len(current_title)}")
  231. # Step 1: Change "Capacity" -> "Cap."
  232. if len(current_title) > 100:
  233. for i, part in enumerate(final_title_parts):
  234. if "Capacity" in part:
  235. final_title_parts[i] = part.replace("Capacity", "Cap.")
  236. current_title = construct_string(final_title_parts)
  237. print("Current Title 2 ########",current_title,len(current_title))
  238. logger.info(f"Current Title 2 shorting capacity ########,{current_title},{len(current_title)}")
  239. # Step 2: Shorten Product Type (e.g., Stainless Steel -> SS)
  240. # Step B: Dynamic Product Type Acronym
  241. # if len(current_title) > 100:
  242. # pt_part = final_title_parts[1]
  243. # words = pt_part.split()
  244. # if len(words) > 1:
  245. # # Takes first letter of every word in the Product Type
  246. # final_title_parts[1] = "".join([w[0].upper() for w in words])
  247. # current_title = construct_string(final_title_parts)
  248. # print("Current Title 3 ########",current_title,len(current_title))
  249. # logger.info(f"Current Title 3 change the title ########,{current_title},{len(current_title)}")
  250. # Step 3: Remove spaces from attributes starting from the back
  251. # Brand (0) and Product Type (1) are skipped
  252. if len(current_title) > 100:
  253. for i in range(len(final_title_parts) - 1, 1, -1):
  254. if len(current_title) <= 100:
  255. break
  256. # Remove white spaces from the current attribute part
  257. final_title_parts[i] = final_title_parts[i].replace(" ", "")
  258. current_title = construct_string(final_title_parts)
  259. print("Current Title 4 ########",current_title,len(current_title))
  260. logger.info(f"Current Title 4 Removing space ########,{current_title},{len(current_title)}")
  261. return current_title,comment
  262. def construct_dynamic_title(raw_data,selected_pt):
  263. try:
  264. product = raw_data.get("props", {}).get("pageProps", {}).get("product", {})
  265. if not product: return "Product data not found"
  266. logger.info(f"IN construct_dynamic_title")
  267. return extract_title_or_error(product,selected_pt)
  268. except Exception:
  269. return None,"Could not found attribute name on product details page"
  270. @login_required
  271. def title_creator_view(request):
  272. if request.method == 'POST' and request.FILES.get('file'):
  273. fresh_token = get_fresh_token()
  274. logger.info(f"fresh_token Value: {fresh_token}")
  275. if not fresh_token:
  276. fresh_token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJERVYifQ.uOFB7h7_Aw6jbA1HSqVJ44tKMO7E1ljz1kV_JddeKL64YCOH57-l1ZX2Lly-Jnhdnxk3xMAeW5FawAgymEaMKA"
  277. excel_file = request.FILES['file']
  278. selected_pt = request.POST.get('product_type')
  279. fs = FileSystemStorage()
  280. filename = fs.save(excel_file.name, excel_file)
  281. file_path = fs.path(filename)
  282. try:
  283. # 1. Read Excel
  284. df = pd.read_excel(file_path)
  285. # 2. Add the NEW COLUMN if it doesn't exist
  286. if 'New_Generated_Title' not in df.columns:
  287. df['New_Generated_Title'] = ""
  288. if 'Comment' not in df.columns:
  289. df['Comment'] = ""
  290. headers = {"User-Agent": "Mozilla/5.0"}
  291. results_for_ui = []
  292. # Specific Headers for the Item# API
  293. api_headers = {
  294. "accept": "application/json, text/plain, */*",
  295. "authorization": fresh_token,#"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJERVYifQ.uOFB7h7_Aw6jbA1HSqVJ44tKMO7E1ljz1kV_JddeKL64YCOH57-l1ZX2Lly-Jnhdnxk3xMAeW5FawAgymEaMKA",
  296. "client_id": "GEC",
  297. "referer": "https://www.globalindustrial.com/"
  298. }
  299. # 3. Process each row
  300. for index, row in df.iterrows():
  301. url = row.get('URL') # Assumes your excel has a 'URL' column
  302. item_number = row.get('Item#')
  303. new_title = ""
  304. final_url = None
  305. comment = ""
  306. # Step 1: Resolve the URL
  307. if pd.notna(url) and str(url).startswith('http'):
  308. final_url = url
  309. elif pd.notna(item_number):
  310. # Call API to get URL from Item#
  311. api_url = f"https://www.globalindustrial.com/catalogApis/catalog/autosuggest?key={item_number}&features=true"
  312. try:
  313. api_resp = requests.get(api_url, headers=api_headers, timeout=10)
  314. if api_resp.status_code == 200:
  315. data = api_resp.json()
  316. final_url = data.get('exactMatch', {}).get('canonicalLink')
  317. except Exception as e:
  318. new_title = f"API Error for Item# {item_number}"
  319. if pd.notna(final_url):
  320. try:
  321. resp = requests.get(final_url, headers=headers, timeout=10)
  322. soup = BeautifulSoup(resp.content, 'html.parser')
  323. script_tag = soup.find('script', id='__NEXT_DATA__')
  324. if script_tag:
  325. raw_data = json.loads(script_tag.string)
  326. new_title,comment = construct_dynamic_title(raw_data,selected_pt)
  327. else:
  328. new_title,comment = "Could not found attribute name on product details page",None
  329. except:
  330. new_title,comment = "Could not found attribute name on product details page",None
  331. else:
  332. new_title,comment = "URL Missing",None
  333. # Update the DataFrame column for this row
  334. df.at[index, 'New_Generated_Title'] = new_title
  335. df.at[index, 'Comment'] = comment
  336. results_for_ui.append({
  337. "id" : index + 1,
  338. "url": final_url,
  339. "new_title": new_title,
  340. "comment": comment,
  341. "status": True
  342. })
  343. # Generates a random float between 3.0 and 7.0
  344. time.sleep(random.uniform(3, 7))
  345. # time.sleep(1) # Safety delay
  346. # 4. Save the modified Excel to a new path
  347. output_filename = f"processed_{excel_file.name}"
  348. output_path = os.path.join(fs.location, output_filename)
  349. df.to_excel(output_path, index=False)
  350. return JsonResponse({
  351. 'success': True,
  352. 'results': results_for_ui,
  353. 'download_url': fs.url(output_filename)
  354. })
  355. finally:
  356. if os.path.exists(file_path): os.remove(file_path)
  357. # GET request: Fetch all product types for the dropdown
  358. product_types = TitleMapping.objects.all().values_list('product_type', flat=True)
  359. return render(request, 'title_creator_index.html', {'product_types': product_types})
  360. # return render(request, 'title_creator_index.html')
  361. def get_fresh_token():
  362. """Hits the homepage to extract the latest Bearer token."""
  363. base_url = "https://www.globalindustrial.com"
  364. try:
  365. # Use a session to persist cookies
  366. session = requests.Session()
  367. response = session.get(base_url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
  368. # 1. Try Cookies
  369. token = session.cookies.get('Authorization')
  370. if token:
  371. return token if "Bearer" in token else f"Bearer {token}"
  372. # 2. Try NEXT_DATA
  373. soup = BeautifulSoup(response.content, 'html.parser')
  374. script_tag = soup.find('script', id='__NEXT_DATA__')
  375. if script_tag:
  376. data = json.loads(script_tag.string)
  377. token = data.get('props', {}).get('pageProps', {}).get('token')
  378. if token:
  379. return f"Bearer {token}"
  380. except Exception as e:
  381. print(f"Token retrieval failed: {e}")
  382. return None
  383. def process_excel_task(file_path, selected_pt, task_id):
  384. print("process excel task started.")
  385. # Retrieve the task record from the database
  386. # scraper = cloudscraper.create_scraper() # This replaces requests.get
  387. task = ProcessingTask.objects.get(task_id=task_id)
  388. fresh_token = get_fresh_token()
  389. logger.info(f"fresh_token Value: {fresh_token}")
  390. if not fresh_token:
  391. fresh_token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJERVYifQ.uOFB7h7_Aw6jbA1HSqVJ44tKMO7E1ljz1kV_JddeKL64YCOH57-l1ZX2Lly-Jnhdnxk3xMAeW5FawAgymEaMKA"
  392. try:
  393. # 1. Read Excel
  394. df = pd.read_excel(file_path)
  395. # 2. Add the NEW COLUMN if it doesn't exist
  396. if 'New_Generated_Title' not in df.columns:
  397. df['New_Generated_Title'] = ""
  398. if 'Comment' not in df.columns:
  399. df['Comment'] = ""
  400. headers = {"User-Agent": "Mozilla/5.0"}
  401. # dynamic_token = await get_fresh_token(scraper)
  402. # Specific Headers for the Item# API
  403. api_headers = {
  404. "accept": "application/json, text/plain, */*",
  405. "authorization": fresh_token, #"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJERVYifQ.uOFB7h7_Aw6jbA1HSqVJ44tKMO7E1ljz1kV_JddeKL64YCOH57-l1ZX2Lly-Jnhdnxk3xMAeW5FawAgymEaMKA", #f"Bearer {dynamic_token}",
  406. "client_id": "GEC",
  407. "referer": "https://www.globalindustrial.com/"
  408. }
  409. # 3. Process each row
  410. for index, row in df.iterrows():
  411. logger.info(f"STARTED: {index}")
  412. url = row.get('URL')
  413. new_title = ""
  414. comment = ""
  415. item_number = row.get('Item#')
  416. final_url = None
  417. # Step 1: Resolve the URL
  418. if pd.notna(url) and str(url).startswith('http'):
  419. final_url = url
  420. elif pd.notna(item_number):
  421. # Call API to get URL from Item#
  422. api_url = f"https://www.globalindustrial.com/catalogApis/catalog/autosuggest?key={item_number}&features=true"
  423. try:
  424. api_resp = requests.get(api_url, headers=api_headers, timeout=10)
  425. if api_resp.status_code == 200:
  426. data = api_resp.json()
  427. final_url = data.get('exactMatch', {}).get('canonicalLink')
  428. except Exception as e:
  429. new_title,comment = f"API Error for Item# {item_number}"
  430. if pd.notna(final_url):
  431. try:
  432. # Scraping logic
  433. # resp = scraper.get(url, timeout=15)
  434. resp = requests.get(final_url, headers=headers, timeout=10)
  435. if resp.status_code == 200:
  436. soup = BeautifulSoup(resp.content, 'html.parser')
  437. script_tag = soup.find('script', id='__NEXT_DATA__')
  438. if script_tag:
  439. try:
  440. raw_data = json.loads(script_tag.string)
  441. # Calling your dynamic title helper
  442. new_title,comment = construct_dynamic_title(raw_data, selected_pt)
  443. except Exception:
  444. new_title,comment = "Data Parsing Error",None
  445. else:
  446. new_title,comment = "Could not found attribute name on product details page",None
  447. else:
  448. new_title,comment = f"HTTP Error: {resp.status_code}",None
  449. except Exception:
  450. new_title,comment = "Request Failed (Timeout/Connection)",None
  451. else:
  452. new_title,comment = "URL Missing",None
  453. # Update the DataFrame
  454. df.at[index, 'New_Generated_Title'] = new_title
  455. df.at[index, 'Comment'] = comment
  456. # Optional: Sleep to prevent getting blocked by the server
  457. # Generates a random float between 3.0 and 7.0
  458. time.sleep(random.uniform(3, 7))
  459. logger.info(f"ENDED: {index}")
  460. # time.sleep(1)
  461. # 4. Save the modified Excel to the MEDIA folder
  462. output_filename = f"completed_{task_id}_{task.original_filename}"
  463. # Ensure media directory exists
  464. if not os.path.exists(settings.MEDIA_ROOT):
  465. os.makedirs(settings.MEDIA_ROOT)
  466. output_path = os.path.join(settings.MEDIA_ROOT, output_filename)
  467. df.to_excel(output_path, index=False)
  468. # 5. Final Status Update
  469. task.status = 'COMPLETED'
  470. # Construct the URL for the frontend to download
  471. task.download_url = f"{settings.MEDIA_URL}{output_filename}"
  472. task.completed_at = timezone.now() # Sets the completion time to NOW (IST)
  473. task.save()
  474. print("process excel task ended.")
  475. except Exception as e:
  476. print(f"Critical Task Failure: {e}")
  477. task.status = 'FAILED'
  478. task.save()
  479. finally:
  480. # 6. Cleanup the temporary uploaded file
  481. if os.path.exists(file_path):
  482. os.remove(file_path)
  483. @login_required
  484. def title_creator_async_view(request):
  485. if request.method == 'POST' and request.FILES.get('file'):
  486. excel_file = request.FILES['file']
  487. selected_pt = request.POST.get('product_type')
  488. # 1. Save file temporarily
  489. fs = FileSystemStorage()
  490. filename = fs.save(f"temp_{uuid.uuid4().hex}_{excel_file.name}", excel_file)
  491. file_path = fs.path(filename)
  492. # 2. Create Task Record
  493. task_id = str(uuid.uuid4())
  494. ProcessingTask.objects.create(
  495. task_id=task_id,
  496. original_filename=excel_file.name,
  497. status='PENDING'
  498. )
  499. # 3. Start Background Thread
  500. thread = threading.Thread(
  501. target=process_excel_task,
  502. args=(file_path, selected_pt, task_id)
  503. )
  504. thread.start()
  505. return JsonResponse({
  506. 'status': 'started',
  507. 'task_id': task_id,
  508. 'message': 'File is processing in the background.'
  509. })
  510. return JsonResponse({'error': 'Invalid request'}, status=400)
  511. # 2. This view is called repeatedly by pollStatus() in your JS
  512. def check_status(request, task_id):
  513. # Look up the task in the database
  514. task = get_object_or_404(ProcessingTask, task_id=task_id)
  515. return JsonResponse({
  516. 'status': task.status, # 'PENDING', 'COMPLETED', or 'FAILED'
  517. 'file_name': task.original_filename,
  518. 'download_url': task.download_url # This will be null until status is COMPLETED
  519. })
  520. @login_required
  521. def title_creator_history_page(request):
  522. # Renders the HTML page
  523. return render(request, 'title_creator_history.html')
  524. @login_required
  525. def get_title_creator_tasks_json(request):
  526. # Returns the list of tasks as JSON for the history table
  527. tasks = ProcessingTask.objects.all().order_by('-created_at')[:50] # Latest 50 tasks
  528. data = []
  529. for t in tasks:
  530. data.append({
  531. 'task_id': t.task_id,
  532. 'filename': t.original_filename or "Unknown File",
  533. 'status': t.status,
  534. 'url': t.download_url,
  535. 'date': t.created_at.astimezone(ZoneInfo("Asia/Kolkata")).strftime("%d %b %Y, %I:%M %p"),
  536. # Use a conditional (ternary) operator to handle the null case
  537. 'completed_at': t.completed_at.astimezone(ZoneInfo("Asia/Kolkata")).strftime("%d %b %Y, %I:%M %p") if t.completed_at else ""
  538. # 'completed_at': t.completed_at.strftime("%d %b %Y, %I:%M %p")
  539. })
  540. print("data",data)
  541. return JsonResponse(data, safe=False)
  542. class TokenFetcherAPI(APIView):
  543. def get(self, request):
  544. token = fetch_global_industrial_token()
  545. if token:
  546. return JsonResponse({
  547. "status": "success",
  548. "token": token
  549. }, status=status.HTTP_200_OK)
  550. return JsonResponse({
  551. "status": "error",
  552. "message": "Could not retrieve token"
  553. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  554. def fetch_global_industrial_token():
  555. """Logic to scrape the token."""
  556. base_url = "https://www.globalindustrial.com"
  557. # Using cloudscraper to handle potential bot detection
  558. scraper = cloudscraper.create_scraper()
  559. try:
  560. response = scraper.get(base_url, timeout=15)
  561. # 1. Check Cookies
  562. token = scraper.cookies.get('Authorization')
  563. if token:
  564. return token.replace('Bearer ', '').strip()
  565. # 2. Check __NEXT_DATA__
  566. soup = BeautifulSoup(response.content, 'html.parser')
  567. script_tag = soup.find('script', id='__NEXT_DATA__')
  568. if script_tag:
  569. data = json.loads(script_tag.string)
  570. token = data.get('props', {}).get('pageProps', {}).get('token')
  571. if token:
  572. return token
  573. except Exception as e:
  574. return None
  575. return None