- Добавлен OverlayCanvas для отрисовки штампа поверх контента - Изменено позиционирование штампа - Добавлены необходимые импорты для работы с Canvas
1209 lines
50 KiB
Python
1209 lines
50 KiB
Python
from flask import Flask, render_template, send_from_directory, make_response, request, redirect, session, g, json, send_file, abort, jsonify
|
||
import os
|
||
import random
|
||
import socket
|
||
import oracledb
|
||
import traceback
|
||
import requests
|
||
from logging.config import dictConfig
|
||
import secrets
|
||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||
import io
|
||
import json
|
||
from expiring_dict import ExpiringDict
|
||
import uuid
|
||
from flask_swagger_ui import get_swaggerui_blueprint
|
||
import datetime
|
||
from reportlab.pdfgen import canvas
|
||
from reportlab.lib.pagesizes import A4
|
||
from reportlab.lib import colors
|
||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
|
||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||
from reportlab.lib.units import inch, mm
|
||
from reportlab.pdfgen.canvas import Canvas
|
||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||
from reportlab.platypus import Frame
|
||
import sys
|
||
import logging
|
||
|
||
|
||
|
||
capcha_score: float = 0.1
|
||
capcha_site = '6LcJpHMgAAAAAMQLNY_g8J2Kv_qmCGureRN_lbGl'
|
||
capcha_site_sec = '6LcJpHMgAAAAAIUf4Jg_7NvawQKZoLoVypDU6-d8'
|
||
capcha_site_url='https://www.google.com/recaptcha/api/siteverify'
|
||
site = 'salvagedb.com'
|
||
app_path = os.path.dirname(os.path.realpath(__file__))
|
||
|
||
|
||
app = Flask(__name__)
|
||
|
||
app_debug : bool = os.environ.get('APP_DEBUG',False)
|
||
app.debug = os.environ.get('APP_DEBUG',False)
|
||
app.secret_key = secrets.token_hex()
|
||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
|
||
|
||
os.environ['NLS_LANG'] = 'American_America.AL32UTF8'
|
||
|
||
|
||
app.cache = ExpiringDict(60*60*24)
|
||
|
||
# Swagger UI
|
||
SWAGGER_URL = '/api/docs'
|
||
API_URL = '/api/swagger.yaml'
|
||
swaggerui_blueprint = get_swaggerui_blueprint(
|
||
SWAGGER_URL,
|
||
API_URL,
|
||
config={
|
||
'app_name': "SalvageDB API"
|
||
}
|
||
)
|
||
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
|
||
|
||
try:
|
||
gips = open('gips.txt').read().split('\n')
|
||
except:
|
||
gips=[]
|
||
|
||
if app_debug:
|
||
print(gips)
|
||
|
||
def save_request(request):
|
||
req_data = {}
|
||
req_data['endpoint'] = request.endpoint
|
||
req_data['method'] = request.method
|
||
req_data['cookies'] = request.cookies
|
||
req_data['data'] = request.data
|
||
req_data['headers'] = dict(request.headers)
|
||
req_data['headers'].pop('Cookie', None)
|
||
req_data['args'] = request.args
|
||
req_data['form'] = request.form
|
||
req_data['remote_addr'] = request.remote_addr
|
||
return req_data
|
||
|
||
# Создаем директорию для логов если её нет
|
||
log_dir = os.path.join(app_path, 'logs')
|
||
if not os.path.exists(log_dir):
|
||
os.makedirs(log_dir)
|
||
|
||
# Определяем уровень логирования в зависимости от окружения
|
||
log_level = 'DEBUG' if app_debug else 'INFO'
|
||
|
||
dictConfig({
|
||
"version": 1,
|
||
"disable_existing_loggers": False,
|
||
"formatters": {
|
||
"default": {
|
||
"format": "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] %(message)s",
|
||
"datefmt": "%Y-%m-%d %H:%M:%S"
|
||
},
|
||
"error": {
|
||
"format": "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] [%(pathname)s] %(message)s",
|
||
"datefmt": "%Y-%m-%d %H:%M:%S"
|
||
}
|
||
},
|
||
"handlers": {
|
||
"console": {
|
||
"class": "logging.StreamHandler",
|
||
"formatter": "default",
|
||
"stream": "ext://sys.stdout"
|
||
},
|
||
"file": {
|
||
"class": "logging.handlers.RotatingFileHandler",
|
||
"formatter": "default",
|
||
"filename": os.path.join(log_dir, "app.log"),
|
||
"maxBytes": 10485760, # 10MB
|
||
"backupCount": 10,
|
||
"encoding": "utf8"
|
||
},
|
||
"error_file": {
|
||
"class": "logging.handlers.RotatingFileHandler",
|
||
"formatter": "error",
|
||
"filename": os.path.join(log_dir, "error.log"),
|
||
"maxBytes": 10485760, # 10MB
|
||
"backupCount": 10,
|
||
"encoding": "utf8",
|
||
"level": "ERROR"
|
||
}
|
||
},
|
||
"loggers": {
|
||
"werkzeug": {
|
||
"handlers": ["console", "file"],
|
||
"level": log_level,
|
||
"propagate": False
|
||
},
|
||
"flask": {
|
||
"handlers": ["console", "file"],
|
||
"level": log_level,
|
||
"propagate": False
|
||
},
|
||
"app": {
|
||
"handlers": ["console", "file", "error_file"],
|
||
"level": log_level,
|
||
"propagate": False
|
||
}
|
||
},
|
||
"root": {
|
||
"level": log_level,
|
||
"handlers": ["console", "file", "error_file"]
|
||
}
|
||
})
|
||
|
||
# Создаем логгер для приложения
|
||
logger = logging.getLogger("app")
|
||
|
||
class OverlayCanvas(Canvas):
|
||
def __init__(self, *args, **kwargs):
|
||
Canvas.__init__(self, *args, **kwargs)
|
||
self._saved_page_states = []
|
||
|
||
def showPage(self):
|
||
self._saved_page_states.append(dict(self.__dict__))
|
||
self._startPage()
|
||
|
||
def save(self):
|
||
for state in self._saved_page_states:
|
||
self.__dict__.update(state)
|
||
self.draw_overlay() # теперь рисуем поверх
|
||
Canvas.showPage(self)
|
||
Canvas.save(self)
|
||
|
||
def draw_overlay(self):
|
||
stamp_path = os.path.join(app_path, 'static', 'stamp256.png')
|
||
if os.path.exists(stamp_path):
|
||
self.drawImage(
|
||
stamp_path,
|
||
x=400, # настроить по ширине
|
||
y=400, # настроить по высоте
|
||
width=100,
|
||
height=100,
|
||
mask='auto'
|
||
)
|
||
|
||
@app.after_request
|
||
def after_request(response):
|
||
if request.cookies.get('user_id', None) == None:
|
||
response.set_cookie('user_id', str(random.randint(0, 10000000000)), max_age=60 * 60 * 24 * 365 * 10)
|
||
return response
|
||
|
||
|
||
def init_session(connection, requestedTag_ignored):
|
||
pass
|
||
# cursor = connection.cursor()
|
||
# cursor.execute("""
|
||
# ALTER SESSION SET
|
||
# TIME_ZONE = 'UTC'
|
||
# NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI'""")
|
||
|
||
def start_pool():
|
||
pool_min = 4
|
||
pool_max = 4
|
||
pool_inc = 0
|
||
|
||
if app_debug:
|
||
print('Connecting to {}:{}/{}'.format(os.environ.get("DB_HOST"), os.environ.get("DB_PORT"), os.environ.get("DB_DBNAME")))
|
||
|
||
pool = oracledb.create_pool(
|
||
user=os.environ.get("DB_USERNAME"),
|
||
password=os.environ.get("DB_PASSWORD"),
|
||
dsn='{}:{}/{}'.format(os.environ.get("DB_HOST"), os.environ.get("DB_PORT"), os.environ.get("DB_DBNAME")),
|
||
#params=sample_env.get_pool_params(),
|
||
min=pool_min,
|
||
max=pool_max,
|
||
increment=pool_inc,
|
||
session_callback=init_session,
|
||
)
|
||
|
||
return pool
|
||
|
||
@app.route("/sitemap.xml")
|
||
def sitemap():
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
cur.execute('select distinct trunc(pg/40000)+1 nm from mv_pages');
|
||
nm = cur.fetchall()
|
||
return render_template('sitemap.xml', site=site, siten=nm)
|
||
|
||
@app.route("/sitemaps/<int:sitemap_id>.xml")
|
||
def sitemaps_xml(sitemap_id):
|
||
try:
|
||
id: int = int(sitemap_id)
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
cur.execute('select pg from mv_pages where sitemap_number = :p1', {'p1': id})
|
||
sitemap_pages = cur.fetchall()
|
||
return render_template('sitemaps.xml', site=site, sitemap_pages=sitemap_pages)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/")
|
||
def index_html():
|
||
try:
|
||
if 'maxnum' in app.cache:
|
||
if app_debug:
|
||
print('Find in cache max_num={}'.format(app.cache['maxnum']))
|
||
cnt = app.cache['maxnum']
|
||
else:
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
cur.execute('select max(num) from salvagedb')
|
||
cnt = "{:,}".format(cur.fetchone()[0])
|
||
app.cache['maxnum'] = cnt
|
||
return render_template('index.html', site=site, cnt=cnt ,capcha_site=capcha_site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/privacy.html")
|
||
def privacy_html():
|
||
try:
|
||
|
||
return render_template('privacy.html', site=site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/robots.txt")
|
||
def robot_txt():
|
||
try:
|
||
return render_template('robots.txt', site=site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/decode", methods = ['POST'])
|
||
def decode():
|
||
try:
|
||
vin = request.form.get('q').strip()
|
||
g_respone = request.form['g-recaptcha-response']
|
||
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
|
||
if capcha_check['success'] == False or capcha_check['score'] <capcha_score:
|
||
logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
|
||
logger.info(f'Bad google answer: {capcha_check}')
|
||
abort(401)
|
||
return redirect(f'/detail/{vin}.html', 301)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
|
||
@app.route("/decodevin.html")
|
||
def decodevin_html():
|
||
try:
|
||
return render_template('decodevin.html', site=site,capcha_site=capcha_site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/database/page<int:page_num>.html")
|
||
def database_page_prc(page_num):
|
||
try:
|
||
pg = int(page_num)
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
try:
|
||
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
|
||
except:
|
||
print(request)
|
||
logger.error(traceback.format_exc())
|
||
|
||
cur.execute('select max(pg) from mv_pages')
|
||
max_page = int(cur.fetchall()[0][0])
|
||
cur.execute("""select rownum,s.vin,
|
||
COALESCE ((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
|
||
COALESCE ((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
|
||
COALESCE ((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year
|
||
from salvagedb s
|
||
where num between (select mi from mv_pages where pg = :p1) and (select ma from mv_pages where pg = :p1)""",
|
||
{'p1': pg})
|
||
pgf = cur.fetchall()
|
||
return render_template('database.html', site=site, cur_page=pg, max_page=max_page, pg=pgf)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/detail/<vin>.html")
|
||
def detail_vin(vin):
|
||
try:
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
try:
|
||
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 0, 0, 1])
|
||
except:
|
||
print(request)
|
||
logger.error(traceback.format_exc())
|
||
|
||
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='5'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Body Class'),'UNKNOWN') body,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='13'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Model'),'UNKNOWN') engine,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='9'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Number of Cylinders'),'UNKNOWN') celindr,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='15'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Drive Type'),'-') drive
|
||
from (select substr(:p1,1,10) svin, :p1 vin from dual) s """, {'p1': vin})
|
||
res = cur.fetchall()
|
||
return render_template('details.html', site=site, vin=vin, det=res, capcha_site=capcha_site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
|
||
@app.route('/search', methods = ['POST'])
|
||
def search():
|
||
try:
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
vin = request.form.get('q')
|
||
ua = request.headers.get('User-Agent')
|
||
logger.info(f'AgeNt: {ua}')
|
||
|
||
g_respone = request.form.get('g-recaptcha-response')
|
||
|
||
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
|
||
if capcha_check['success'] == False or capcha_check['score'] <capcha_score:
|
||
logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
|
||
logger.info(f'Bad google answer: {capcha_check}')
|
||
if app_debug==True:
|
||
req_data = save_request(request)
|
||
logger.info(json.dumps(req_data, indent=4, default=str))
|
||
return 'google recaptcha req low score', 401
|
||
|
||
|
||
if len(vin) != 17:
|
||
return 'bad vin!', 500
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 0, 1, 0])
|
||
|
||
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='5'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Body Class'),'UNKNOWN') body,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='13'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Model'),'UNKNOWN') engine,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='9'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Number of Cylinders'),'UNKNOWN') celindr,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='15'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Drive Type'),'-') drive
|
||
from (select substr(:p1,1,10) svin, :p1 vin from dual) s """, {'p1': vin})
|
||
res = cur.fetchall()
|
||
cur.execute("""select rownum, t.vin, t.title, t.odo, t.odos, t.dem1, t.dem2, t.year||'/'||t.month from salvagedb t where vin = :p1 and svin = substr(:p1,1,10) """, {'p1': vin})
|
||
his = cur.fetchall()
|
||
|
||
# Сохраняем VIN в сессии
|
||
session['last_searched_vin'] = vin
|
||
session['last_search_time'] = datetime.datetime.now().timestamp()
|
||
|
||
return render_template('search.html', site=site, vin=vin, det=res, his=his)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
## API
|
||
@app.route("/api/search")
|
||
def api_search():
|
||
try:
|
||
access_code = request.args.get('access_code', None)
|
||
vin = request.args.get('vin', None)
|
||
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
|
||
# базовые проверки входящих аргументов
|
||
if len(vin) != 17 or vin is None:
|
||
ret = {'status': 'incorrect vin'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
if len(access_code) > 16 or access_code is None:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
|
||
## проверяем access_code
|
||
cur.execute(
|
||
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
|
||
{'p1': str(access_code)})
|
||
res = cur.fetchone()
|
||
summ = res[0]
|
||
found_price = res[1]
|
||
notfound_price = res[2]
|
||
decode_price = res[3]
|
||
|
||
### не достаточно средств
|
||
if summ <= 0:
|
||
ret = {'status': 'You have insufficient balance.'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
## ищем в истории
|
||
cur.execute('select t.odo,t.odos,t.title,t.dem1,t.dem2,t.year,t.month from salvagedb t where vin =:p1 and svin = :p2',
|
||
{'p1': vin.upper(), 'p2': vin.upper()[:10]})
|
||
res = cur.fetchall()
|
||
|
||
if len(res) > 0:
|
||
# found
|
||
dat = []
|
||
for it in res:
|
||
dat.append({
|
||
'odometer': it[0],
|
||
'odometer_status': it[1],
|
||
'title': it[2],
|
||
'damage1': it[3],
|
||
'damage2': it[4],
|
||
'add_to_db': '{}-{}'.format(it[5], it[6])
|
||
})
|
||
ret = {
|
||
'status': 'found',
|
||
'vin': vin.upper(),
|
||
'cost': found_price,
|
||
'records': dat
|
||
}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
|
||
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
|
||
conn.commit()
|
||
return response
|
||
|
||
else:
|
||
# nor found
|
||
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
|
||
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'NOT FOUND', 'p5': notfound_price})
|
||
conn.commit()
|
||
return response
|
||
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
|
||
@app.route("/api/restfact")
|
||
def api_restfact():
|
||
access_code = request.args.get('access_code', None)
|
||
if len(access_code) > 16 or access_code is None:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
cur.callproc('make_billing')
|
||
cur.execute(
|
||
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
|
||
{'p1': str(access_code)})
|
||
res = cur.fetchone()
|
||
summ = res[0]
|
||
if len(res) <= 0:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
else:
|
||
ret = {
|
||
'access_code': access_code,
|
||
'restfact': summ
|
||
}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
@app.route("/api/restfact_detail")
|
||
def restfact_detail():
|
||
access_code = request.args.get('access_code', None)
|
||
if len(access_code) > 16 or access_code is None:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
cur.callproc('make_billing')
|
||
cur.execute('select rownum from restfact t where access_code = :p1', {'p1': str(access_code)})
|
||
res = cur.fetchall()
|
||
if len(res) <= 0:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
cur.execute("select vin, ip, status, dt, cost from billing where access_code = :p1 and dt > CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE) - 45 and made = 1 order by id", {'p1': str(access_code)})
|
||
res = cur.fetchall()
|
||
if len(res) <= 0:
|
||
ret = {'status': 'billing not found'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
else:
|
||
dat = []
|
||
for it in res:
|
||
dat.append(
|
||
{
|
||
'VIN': it[0],
|
||
'IP': it[1],
|
||
'STATUS': it[2],
|
||
'DATETIME': it[3].isoformat(),
|
||
'COST': it[4]
|
||
}
|
||
)
|
||
ret = {
|
||
'access_code': access_code,
|
||
'report': dat
|
||
}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
## API V2
|
||
|
||
@app.route("/api/v2/search")
|
||
def api_search_v2():
|
||
try:
|
||
access_code = request.args.get('access_code', None)
|
||
vin = request.args.get('vin', None)
|
||
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
|
||
# базовые проверки входящих аргументов
|
||
if len(vin) != 17 or vin is None:
|
||
ret = {'status': 'incorrect vin'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
if len(access_code) > 16 or access_code is None:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
|
||
## проверяем access_code
|
||
cur.execute(
|
||
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
|
||
{'p1': str(access_code)})
|
||
res = cur.fetchone()
|
||
summ = res[0]
|
||
found_price = res[1]
|
||
notfound_price = res[2]
|
||
decode_price = res[3]
|
||
|
||
### не достаточно средств
|
||
if summ <= 0:
|
||
ret = {'status': 'You have insufficient balance.'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
## ищем в истории
|
||
cur.execute('''select s.odo,s.odos,s.title,s.dem1,s.dem2,s.year,s.month, json_value(i.jdata, '$.Runs_Drive') RD_Status, json_value(i.jdata, '$.Locate') Sale_Location, json_value(i.jdata, '$.RepCost') as Repear_cost,
|
||
(select count(1) from salvage_images si where si.vin = s.vin and fn = 1) image_count from salvagedb.salvagedb s left join addinfo i on s.num = i.numid where vin =:p1 and svin = :p2''',
|
||
{'p1': vin.upper(), 'p2': vin.upper()[:10]})
|
||
res = cur.fetchall()
|
||
|
||
if len(res) > 0:
|
||
# found
|
||
dat = []
|
||
for it in res:
|
||
dat.append({
|
||
'odometer': it[0],
|
||
'odometer_status': it[1],
|
||
'title': it[2],
|
||
'damage1': it[3],
|
||
'damage2': it[4],
|
||
'add_to_db': '{}-{}'.format(it[5], it[6]),
|
||
'RD_Status':it[7],
|
||
'Sale_Location':it[8],
|
||
'Repear_Cost':it[9],
|
||
'Photo_Count':it[10]
|
||
})
|
||
ret = {
|
||
'status': 'found',
|
||
'vin': vin.upper(),
|
||
'cost': found_price,
|
||
'records': dat
|
||
}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
|
||
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
|
||
conn.commit()
|
||
return response
|
||
|
||
else:
|
||
# nor found
|
||
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
|
||
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'NOT FOUND', 'p5': notfound_price})
|
||
conn.commit()
|
||
return response
|
||
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route("/api/v2/reqphoto")
|
||
def api_reqimage():
|
||
try:
|
||
access_code = request.args.get('access_code', None)
|
||
vin = request.args.get('vin', None)
|
||
|
||
user_ip = get_ip(request) ## определение ip клиента
|
||
|
||
# базовые проверки входящих аргументов
|
||
if len(vin) != 17 or vin is None:
|
||
ret = {'status': 'incorrect vin'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
if len(access_code) > 16 or access_code is None:
|
||
ret = {'status': 'incorrect access_code'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
|
||
## проверяем access_code
|
||
cur.execute(
|
||
'select t.summ, t.found_price, t.notfound_price, t.decode_price from restfact t where access_code = :p1',
|
||
{'p1': str(access_code)})
|
||
res = cur.fetchone()
|
||
summ = res[0]
|
||
found_price = res[1]
|
||
notfound_price = res[2]
|
||
decode_price = res[3]
|
||
|
||
if summ <= 0:
|
||
ret = {'status': 'You have insufficient balance.'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
|
||
cur.execute('select count(*) from salvagedb.salvage_images where vin = :p1 and fn = 1' , {'p1':vin.upper()})
|
||
res = cur.fetchone()
|
||
img_count = int(res[0])
|
||
if img_count<1:
|
||
ret = {'status': 'Photos not found for this vin.'}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
if app.debug:
|
||
logger.debug(json.dumps(ret))
|
||
return response
|
||
else:
|
||
cur.execute("""select GenImages('{}','{}') as ui from dual""".format(access_code, vin.upper()))
|
||
res = cur.fetchall()
|
||
req_id = res[0][0]
|
||
cur.execute('select fake_name from salvage_images_req where req_id = :p1', {'p1':req_id})
|
||
res = cur.fetchall()
|
||
images = []
|
||
nm = 0
|
||
for it in res:
|
||
images.append({
|
||
'num':nm,
|
||
'url':'https://salimages.salvagedb/{}/{}'.format(req_id, it[0])
|
||
})
|
||
nm = nm + 1
|
||
ret = {'status': 'Image_found', 'Images':images}
|
||
response = app.response_class(
|
||
response=json.dumps(ret),
|
||
status=200,
|
||
mimetype='application/json'
|
||
)
|
||
return response
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
|
||
@app.route("/ads.txt")
|
||
def ads_txt():
|
||
try:
|
||
return render_template('ads.txt')
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route('/favicon.ico')
|
||
def logo():
|
||
return send_file(app_path+"/static/favicon.ico")
|
||
|
||
@app.route('/limit')
|
||
def rate_limit():
|
||
try:
|
||
return render_template('rate_limit.html', site=site)
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
return 'bad request!', 500
|
||
|
||
@app.route('/donate')
|
||
def donate():
|
||
return render_template('donate.html')
|
||
|
||
|
||
def get_ip(req) -> str:
|
||
if 'X-Forwarded-For' in req.headers:
|
||
proxy_data = req.headers['X-Forwarded-For']
|
||
ip_list = proxy_data.split(',')
|
||
user_ip = req.headers.get("Cf-Connecting-Ip") or ip_list[0] # first address in list is User IP
|
||
else:
|
||
user_ip = req.headers.get("Cf-Connecting-Ip") or req.remote_addr
|
||
return user_ip
|
||
|
||
def get_addr(req) -> str:
|
||
try:
|
||
return socket.gethostbyaddr(req)[0]
|
||
except:
|
||
return req
|
||
|
||
@app.route('/api/swagger.yaml')
|
||
def swagger_yaml():
|
||
return send_from_directory('api', 'swagger.yaml')
|
||
|
||
@app.route('/static/<path:filename>')
|
||
def serve_static(filename):
|
||
try:
|
||
# Проверка расширения файла
|
||
allowed_extensions = {'.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.json'}
|
||
file_ext = os.path.splitext(filename)[1].lower()
|
||
|
||
if file_ext not in allowed_extensions:
|
||
logger.warning(f'Attempt to access forbidden file type: {filename}')
|
||
return 'Access denied', 403
|
||
|
||
# Проверка пути на directory traversal
|
||
safe_path = os.path.normpath(os.path.join('static', filename))
|
||
if not safe_path.startswith('static'):
|
||
logger.warning(f'Attempt to access file outside static directory: {filename}')
|
||
return 'Access denied', 403
|
||
|
||
# Определение MIME-типа
|
||
mime_types = {
|
||
'.css': 'text/css',
|
||
'.js': 'application/javascript',
|
||
'.json': 'application/json',
|
||
'.png': 'image/png',
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.gif': 'image/gif',
|
||
'.ico': 'image/x-icon',
|
||
'.svg': 'image/svg+xml'
|
||
}
|
||
|
||
mime_type = mime_types.get(file_ext, 'application/octet-stream')
|
||
|
||
# Логирование доступа
|
||
logger.info(f'Access to static file: {filename}')
|
||
|
||
response = make_response(send_from_directory('static', filename))
|
||
response.headers['Content-Type'] = mime_type
|
||
|
||
# Специальные заголовки для PWA файлов
|
||
if filename == 'manifest.json':
|
||
response.headers['Content-Type'] = 'application/manifest+json'
|
||
elif filename == 'sw.js':
|
||
response.headers['Service-Worker-Allowed'] = '/'
|
||
|
||
return response
|
||
|
||
except Exception as e:
|
||
logger.error(f'Error accessing file {filename}: {str(e)}')
|
||
return 'File not found', 404
|
||
|
||
@app.route("/salvagereport/<string:vin>")
|
||
def generate_pdf_report(vin):
|
||
try:
|
||
# Проверяем наличие VIN в сессии и время последнего поиска
|
||
if 'last_searched_vin' not in session or session['last_searched_vin'] != vin:
|
||
logger.warning(f'Direct access attempt to report generation for VIN: {vin}')
|
||
return 'Access denied', 403
|
||
|
||
# Проверяем время последнего поиска (не более 5 минут)
|
||
if datetime.datetime.now().timestamp() - session['last_search_time'] > 300:
|
||
logger.warning(f'Report generation attempt expired for VIN: {vin}')
|
||
return 'Access denied', 403
|
||
|
||
conn = pool.acquire()
|
||
cur = conn.cursor()
|
||
user_ip = get_ip(request)
|
||
|
||
try:
|
||
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
|
||
except:
|
||
logger.error(traceback.format_exc())
|
||
|
||
# Get vehicle details
|
||
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='5'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Body Class'),'UNKNOWN') body,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='13'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Model'),'UNKNOWN') engine,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='9'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Number of Cylinders'),'UNKNOWN') celindr,
|
||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='15'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Drive Type'),'-') drive
|
||
from (select substr(:p1,1,10) svin, :p1 vin from dual) s """, {'p1': vin})
|
||
det = cur.fetchall()
|
||
|
||
# Get salvage history
|
||
cur.execute("""select rownum, t.vin, t.title, t.odo, t.odos, t.dem1, t.dem2, t.year||'/'||t.month from salvagedb t where vin = :p1 and svin = substr(:p1,1,10) """, {'p1': vin})
|
||
his = cur.fetchall()
|
||
|
||
# Generate current date and report ID
|
||
now = datetime.datetime.now()
|
||
report_date = now.strftime("%Y-%m-%d") # Только дата без времени
|
||
report_id = uuid.uuid4().hex[:8].upper()
|
||
current_year = now.year
|
||
|
||
try:
|
||
# Создаем буфер для PDF
|
||
buffer = io.BytesIO()
|
||
|
||
# Создаем PDF документ
|
||
doc = SimpleDocTemplate(
|
||
buffer,
|
||
pagesize=A4,
|
||
rightMargin=20*mm,
|
||
leftMargin=20*mm,
|
||
topMargin=15*mm,
|
||
bottomMargin=20*mm
|
||
)
|
||
|
||
# Создаем стили для текста
|
||
styles = getSampleStyleSheet()
|
||
styles.add(ParagraphStyle(
|
||
name='ReportTitle',
|
||
parent=styles['Heading1'],
|
||
fontSize=16,
|
||
alignment=TA_CENTER
|
||
))
|
||
styles.add(ParagraphStyle(
|
||
name='ReportSubtitle',
|
||
parent=styles['Normal'],
|
||
fontSize=12,
|
||
alignment=TA_CENTER,
|
||
textColor=colors.HexColor('#0066CC') # Синий цвет для акцента
|
||
))
|
||
styles.add(ParagraphStyle(
|
||
name='Right',
|
||
parent=styles['Normal'],
|
||
fontSize=10,
|
||
alignment=TA_RIGHT
|
||
))
|
||
styles.add(ParagraphStyle(
|
||
name='Disclaimer',
|
||
parent=styles['Normal'],
|
||
fontSize=8,
|
||
textColor=colors.gray
|
||
))
|
||
styles.add(ParagraphStyle(
|
||
name='SalvageDBFooter',
|
||
parent=styles['Normal'],
|
||
fontSize=9,
|
||
alignment=TA_CENTER,
|
||
textColor=colors.HexColor('#0066CC'), # Синий цвет для акцента
|
||
fontName='Helvetica-Bold'
|
||
))
|
||
|
||
# Список элементов для PDF
|
||
elements = []
|
||
|
||
# Путь к логотипу
|
||
# logo_path = os.path.join(app_path, 'static', 'icons', 'icon-144x144.png')
|
||
|
||
# Добавляем логотип, если файл существует
|
||
# if os.path.exists(logo_path):
|
||
# logo_img = Image(logo_path, width=30*mm, height=30*mm)
|
||
# logo_img.hAlign = 'CENTER'
|
||
# elements.append(logo_img)
|
||
# elements.append(Spacer(1, 5*mm))
|
||
|
||
# Заголовок отчета с акцентом на SALVAGEDB.COM
|
||
elements.append(Paragraph("Vehicle History Report", styles['ReportTitle']))
|
||
elements.append(Paragraph("SALVAGEDB.COM - Comprehensive Vehicle History Check", styles['ReportSubtitle']))
|
||
elements.append(Spacer(1, 10*mm))
|
||
|
||
# Информация об отчете
|
||
elements.append(Paragraph(f"Report Date: {report_date}", styles['Right']))
|
||
elements.append(Paragraph(f"Report ID: {report_id}", styles['Right']))
|
||
elements.append(Spacer(1, 15*mm))
|
||
|
||
# Информация о транспортном средстве
|
||
elements.append(Paragraph("Vehicle Information", styles['Heading2']))
|
||
|
||
# Создаем таблицу с информацией о транспортном средстве
|
||
vehicle_data = [
|
||
["VIN", vin],
|
||
["Make", det[0][1]],
|
||
["Model", det[0][2]],
|
||
["Year", det[0][3]],
|
||
["Body Style", det[0][4]],
|
||
["Engine", det[0][5]],
|
||
["Cylinders", det[0][6]],
|
||
["Drive", det[0][7]]
|
||
]
|
||
|
||
vehicle_table = Table(vehicle_data, colWidths=[100, 350])
|
||
vehicle_table.setStyle(TableStyle([
|
||
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
||
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
||
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||
('FONTSIZE', (0, 0), (0, -1), 10),
|
||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||
('BACKGROUND', (1, 0), (-1, -1), colors.white),
|
||
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
|
||
]))
|
||
|
||
elements.append(vehicle_table)
|
||
elements.append(Spacer(1, 10*mm))
|
||
|
||
# Информация об истории
|
||
if his:
|
||
elements.append(Paragraph("Salvage History", styles['Heading2']))
|
||
|
||
# Создаем стиль для ячеек с переносом текста
|
||
table_style = TableStyle([
|
||
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
|
||
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||
('FONTSIZE', (0, 0), (-1, 0), 9),
|
||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
|
||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||
('LEFTPADDING', (0, 0), (-1, -1), 3),
|
||
('RIGHTPADDING', (0, 0), (-1, -1), 3),
|
||
('FONTSIZE', (0, 1), (-1, -1), 8),
|
||
('WORDWRAP', (0, 0), (-1, -1), True),
|
||
])
|
||
|
||
# Оптимизированные заголовки без VIN
|
||
history_headers = ["Title", "Mileage", "Odometer Status", "Primary\nDamage", "Secondary\nDamage"]
|
||
|
||
# Формируем данные для таблицы истории
|
||
history_data = [history_headers]
|
||
|
||
for item in his:
|
||
title_value = item[2]
|
||
primary_damage = item[5] if item[5] else "N/A"
|
||
secondary_damage = item[6] if item[6] else "N/A"
|
||
|
||
# Добавляем строку с данными, убрав VIN
|
||
history_data.append([
|
||
title_value,
|
||
str(item[3]),
|
||
item[4],
|
||
primary_damage,
|
||
secondary_damage
|
||
])
|
||
|
||
# Устанавливаем ширину колонок оптимально для A4
|
||
available_width = doc.width # Доступная ширина страницы
|
||
col_widths = [
|
||
available_width * 0.20, # 20% для Title
|
||
available_width * 0.12, # 12% для Mileage
|
||
available_width * 0.18, # 18% для Odometer Status
|
||
available_width * 0.25, # 25% для Primary Damage
|
||
available_width * 0.25 # 25% для Secondary Damage
|
||
]
|
||
|
||
# Создаем таблицу с правильными размерами
|
||
history_table = Table(history_data, colWidths=col_widths, repeatRows=1)
|
||
history_table.setStyle(table_style)
|
||
|
||
elements.append(history_table)
|
||
else:
|
||
elements.append(Paragraph("Salvage History", styles['Heading2']))
|
||
elements.append(Paragraph("Salvage history not found.", styles['Normal']))
|
||
|
||
elements.append(Spacer(1, 15*mm))
|
||
|
||
# Добавляем штамп поверх таблицы
|
||
# stamp_path = os.path.join(app_path, 'static', 'stamp256.png')
|
||
# if os.path.exists(stamp_path):
|
||
# stamp_img = Image(stamp_path, width=100, height=100)
|
||
# stamp_img.hAlign = 'RIGHT'
|
||
# stamp_img.vAlign = 'TOP'
|
||
# # Размещаем штамп поверх таблицы
|
||
# elements.append(stamp_img)
|
||
|
||
# elements.append(Spacer(1, 15*mm))
|
||
|
||
|
||
|
||
# Дисклеймер
|
||
disclaimer_text = """
|
||
Disclaimer: This report provides information about salvage or junk vehicles; damage from hail, flood or fire;
|
||
mileage discrepancies or odometer rollback; and gray market vehicles. We do not claim that the car got in our
|
||
databank has salvage title, but the fact that it has been damaged for sure. Our site helps people avoid buying
|
||
a damaged vehicle in the past.
|
||
"""
|
||
elements.append(Paragraph(disclaimer_text, styles['Disclaimer']))
|
||
|
||
# Футер с акцентом на SALVAGEDB.COM
|
||
elements.append(Spacer(1, 15*mm))
|
||
elements.append(Paragraph(f"© {current_year} SALVAGEDB.COM - All Rights Reserved", styles['SalvageDBFooter']))
|
||
elements.append(Paragraph("Visit SALVAGEDB.COM for more information about vehicle history", styles['SalvageDBFooter']))
|
||
elements.append(Paragraph('"This report is provided as is without any guarantees or warranty."', styles['Disclaimer']))
|
||
|
||
# Строим PDF документ
|
||
# doc.build(elements, onFirstPage=add_stamp_overlay, onLaterPages=add_stamp_overlay)
|
||
doc.build(elements, canvasmaker=OverlayCanvas)
|
||
|
||
# Получаем содержимое буфера
|
||
pdf_data = buffer.getvalue()
|
||
buffer.close()
|
||
|
||
# Создаем HTTP-ответ с PDF
|
||
response = make_response(pdf_data)
|
||
response.headers['Content-Type'] = 'application/pdf'
|
||
response.headers['Content-Disposition'] = f'attachment; filename=vehicle_report_{vin}.pdf'
|
||
|
||
return response
|
||
|
||
except Exception as pdf_error:
|
||
# Если возникла ошибка при генерации PDF, логируем ее
|
||
app.logger.error(f"PDF generation error: {str(pdf_error)}")
|
||
app.logger.error(traceback.format_exc())
|
||
|
||
# Показываем печатную версию в браузере
|
||
return render_template(
|
||
'report_printable.html',
|
||
vin=vin,
|
||
det=det,
|
||
his=his,
|
||
report_date=report_date,
|
||
report_id=report_id,
|
||
current_year=current_year,
|
||
error_message=str(pdf_error)
|
||
)
|
||
|
||
except Exception as e:
|
||
app.logger.error(traceback.format_exc())
|
||
return 'Report generation failed: ' + str(e), 500
|
||
|
||
if __name__ == '__main__':
|
||
# Start a pool of connections
|
||
pool = start_pool()
|
||
app.run(port=int(os.environ.get('PORT', '8080')))
|
||
|