views.py 27 KB

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