views.py 28 KB

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