final stage -- still some bugs
This commit is contained in:
@ -631,17 +631,12 @@ class GpsCheckin(models.Model):
|
||||
|
||||
class Meta:
|
||||
db_table = 'gps_checkins'
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['zekken_number', 'event_code', 'path_order'],
|
||||
name='unique_gps_checkin'
|
||||
)
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=['zekken_number', 'event_code','path_order'], name='idx_zekken_event'),
|
||||
models.Index(fields=['zekken_number', 'event_code', 'path_order'], name='idx_zekken_event'),
|
||||
models.Index(fields=['create_at'], name='idx_create_at'),
|
||||
]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.event_code}-{self.zekken_number}-{self.path_order}-buy:{self.buy_flag}-valid:{self.validate_location}-point:{self.points}"
|
||||
|
||||
|
||||
501
rog/views.py
501
rog/views.py
@ -5,6 +5,10 @@ User = get_user_model()
|
||||
import traceback
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
import subprocess # subprocessモジュールを追加
|
||||
import tempfile # tempfileモジュールを追加
|
||||
import shutil # shutilモジュールを追加
|
||||
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
@ -90,7 +94,7 @@ from io import BytesIO
|
||||
from django.urls import get_resolver
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.http import HttpResponse
|
||||
from sumaexcel import SumasenExcel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -2538,6 +2542,80 @@ def get_checkins(request, *args, **kwargs):
|
||||
|
||||
@api_view(['POST'])
|
||||
def update_checkins(request):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
update_base = request.data
|
||||
logger.info(f"Processing update data: {update_base}")
|
||||
zekken_number = update_base['zekken_number']
|
||||
event_code = update_base['event_code']
|
||||
|
||||
# 既存レコードの更新
|
||||
for update in update_base['checkins']:
|
||||
if 'id' in update and int(update['id']) > 0:
|
||||
try:
|
||||
checkin = GpsCheckin.objects.get(id=update['id'])
|
||||
logger.info(f"Updating existing checkin: {checkin}")
|
||||
|
||||
# 既存レコードの更新
|
||||
checkin.path_order = update['order']
|
||||
checkin.buy_flag = update.get('buy_flag', False)
|
||||
checkin.validate_location = update.get('validation', False)
|
||||
checkin.points = update.get('points', 0)
|
||||
checkin.update_at = timezone.now()
|
||||
checkin.update_user = request.user.email if request.user.is_authenticated else None
|
||||
checkin.save()
|
||||
logger.info(f"Updated existing checkin result: {checkin}")
|
||||
|
||||
except GpsCheckin.DoesNotExist:
|
||||
logger.error(f"Checkin with id {update['id']} not found")
|
||||
continue # エラーを無視して次のレコードの処理を継続
|
||||
|
||||
# 新規レコードの作成
|
||||
for update in update_base['checkins']:
|
||||
if 'id' in update and int(update['id']) == 0:
|
||||
logger.info(f"Creating new checkin: {update}")
|
||||
try:
|
||||
checkin = GpsCheckin.objects.create(
|
||||
zekken_number=zekken_number,
|
||||
event_code=event_code,
|
||||
path_order=update['order'],
|
||||
cp_number=update['cp_number'],
|
||||
validate_location=update.get('validation', False),
|
||||
buy_flag=update.get('buy_flag', False),
|
||||
points=update.get('points', 0),
|
||||
create_at=timezone.now(),
|
||||
update_at=timezone.now(),
|
||||
create_user=request.user.email if request.user.is_authenticated else None,
|
||||
update_user=request.user.email if request.user.is_authenticated else None
|
||||
)
|
||||
logger.info(f"Created new checkin: {checkin}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating new checkin: {str(e)}")
|
||||
continue # エラーを無視して次のレコードの処理を継続
|
||||
|
||||
# 更新後のデータを順序付けて取得
|
||||
updated_checkins = GpsCheckin.objects.filter(
|
||||
zekken_number=zekken_number,
|
||||
event_code=event_code
|
||||
).order_by('path_order')
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': 'Checkins updated successfully',
|
||||
'data': [{'id': c.id, 'path_order': c.path_order} for c in updated_checkins]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in update_checkins: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": "Failed to update checkins", "detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def update_checkins_old(request):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
update_base = request.data
|
||||
@ -2605,43 +2683,402 @@ def update_checkins(request):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def export_excel(request, zekken_number):
|
||||
def export_excel(request, zekken_number, event_code):
|
||||
temp_dir = None
|
||||
try:
|
||||
# パラメータを文字列型に変換
|
||||
zekken_number = str(zekken_number)
|
||||
event_code = str(event_code)
|
||||
|
||||
# 初期化
|
||||
variables = {
|
||||
"zekken_number":sekken_number,
|
||||
"event_code":request["FC岐阜"],
|
||||
"db":"rogdb",
|
||||
"username":"admin",
|
||||
"password":"admin123456",
|
||||
"host":"localhost",
|
||||
"port":"5432"
|
||||
}
|
||||
excel = SumasenExcel(document="test", variables=variables, docbase="./docbase")
|
||||
# ./docbase/certificate.ini の定義をベースに、
|
||||
# ./docbase/certificate_template.xlsxを読み込み
|
||||
# ./docbase/certificate_(zekken_number).xlsxを作成する
|
||||
logger.info(f"Exporting Excel/PDF for zekken_number: {zekken_number}, event_code: {event_code}")
|
||||
|
||||
# シート初期化
|
||||
ret = excel.make_report(variables=variables)
|
||||
if ret["status"]==True:
|
||||
filepath=ret["filepath"]
|
||||
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
|
||||
else:
|
||||
message = ret.get("message", "No message provided")
|
||||
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
|
||||
# 入力値の検証
|
||||
if not zekken_number or not event_code:
|
||||
logger.error("Missing required parameters")
|
||||
return Response(
|
||||
{"error": "Both zekken_number and event_code are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# docbaseディレクトリのパスを絶対パスで設定
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
docbase_path = os.path.join(base_dir, 'docbase')
|
||||
|
||||
# ディレクトリ存在確認と作成
|
||||
os.makedirs(docbase_path, exist_ok=True)
|
||||
|
||||
# 設定ファイルのパス
|
||||
template_path = os.path.join(docbase_path, 'certificate_template.xlsx')
|
||||
ini_path = os.path.join(docbase_path, 'certificate.ini')
|
||||
|
||||
# テンプレートと設定ファイルの存在確認
|
||||
if not os.path.exists(template_path):
|
||||
logger.error(f"Template file not found: {template_path}")
|
||||
return Response(
|
||||
{"error": "Excel template file missing"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
if not os.path.exists(ini_path):
|
||||
logger.error(f"INI file not found: {ini_path}")
|
||||
return Response(
|
||||
{"error": "Configuration file missing"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# Docker環境用のデータベース設定を使用
|
||||
db_settings = settings.DATABASES['default']
|
||||
|
||||
# 初期化
|
||||
variables = {
|
||||
"zekken_number": str(zekken_number),
|
||||
"event_code": str(event_code),
|
||||
"db": str(db_settings['NAME']),
|
||||
"username": str(db_settings['USER']),
|
||||
"password": str(db_settings['PASSWORD']),
|
||||
"host": str(db_settings['HOST']),
|
||||
"port": str(db_settings['PORT']),
|
||||
"template_path": template_path
|
||||
}
|
||||
|
||||
try:
|
||||
excel = SumasenExcel(document="certificate", variables=variables, docbase=docbase_path)
|
||||
ret = excel.make_report(variables=variables)
|
||||
|
||||
if ret["status"] != True:
|
||||
message = ret.get("message", "No message provided")
|
||||
logger.error(f"Excelファイル作成失敗 : ret.message={message}")
|
||||
return Response(
|
||||
{"error": f"Excel generation failed: {message}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
excel_path = ret.get("filepath")
|
||||
if not excel_path or not os.path.exists(excel_path):
|
||||
logger.error(f"Output file not found: {excel_path}")
|
||||
return Response(
|
||||
{"error": "Generated file not found"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# フォーマット指定(excel or pdf)
|
||||
format_type = request.query_params.get('format', 'pdf')
|
||||
|
||||
if format_type.lower() == 'pdf':
|
||||
try:
|
||||
# 一時ディレクトリを作成
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_excel = os.path.join(temp_dir, f'certificate_{zekken_number}.xlsx')
|
||||
temp_pdf = os.path.join(temp_dir, f'certificate_{zekken_number}.pdf')
|
||||
|
||||
# Excelファイルを一時ディレクトリにコピー
|
||||
shutil.copy2(excel_path, temp_excel)
|
||||
|
||||
# 一時ディレクトリのパーミッションを設定
|
||||
os.chmod(temp_dir, 0o777)
|
||||
os.chmod(temp_excel, 0o666)
|
||||
|
||||
logger.info(f"Converting Excel to PDF in temp directory: {temp_dir}")
|
||||
|
||||
# LibreOfficeを使用してExcelをPDFに変換
|
||||
conversion_command = [
|
||||
'soffice', # LibreOfficeの代替コマンド
|
||||
'--headless',
|
||||
'--convert-to',
|
||||
'pdf',
|
||||
'--outdir',
|
||||
temp_dir,
|
||||
temp_excel
|
||||
]
|
||||
|
||||
logger.debug(f"Running conversion command: {' '.join(conversion_command)}")
|
||||
|
||||
# 環境変数を設定
|
||||
env = os.environ.copy()
|
||||
env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定
|
||||
|
||||
# 変換プロセスを実行
|
||||
process = subprocess.run(
|
||||
conversion_command,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
logger.debug(f"Conversion output: {process.stdout}")
|
||||
|
||||
# PDFファイルの存在確認
|
||||
if not os.path.exists(temp_pdf):
|
||||
logger.error("PDF conversion failed - output file not found")
|
||||
return Response(
|
||||
{"error": "PDF conversion failed - output file not found"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# PDFファイルを読み込んでレスポンスを返す
|
||||
with open(temp_pdf, 'rb') as pdf_file:
|
||||
pdf_content = pdf_file.read()
|
||||
|
||||
response = HttpResponse(pdf_content, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.pdf"'
|
||||
return response
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Error converting to PDF: {str(e)}\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}")
|
||||
return Response(
|
||||
{"error": f"PDF conversion failed: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
finally:
|
||||
# 一時ディレクトリの削除
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
logger.debug(f"Temporary directory removed: {temp_dir}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove temporary directory: {str(e)}")
|
||||
|
||||
else: # Excel形式の場合
|
||||
with open(excel_path, 'rb') as excel_file:
|
||||
response = HttpResponse(
|
||||
excel_file.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.xlsx"'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Excel/PDF generation: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": f"File generation failed: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in export_excel: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": "Failed to export file", "detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
finally:
|
||||
# 確実に一時ディレクトリを削除
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove temporary directory in finally block: {str(e)}")
|
||||
|
||||
@api_view(['GET'])
|
||||
def export_exceli_old2(request,zekken_number, event_code):
|
||||
|
||||
try:
|
||||
# パラメータを文字列型に変換
|
||||
zekken_number = str(zekken_number)
|
||||
event_code = str(event_code)
|
||||
|
||||
logger.info(f"Exporting Excel for zekken_number: {zekken_number}, event_code: {event_code}")
|
||||
|
||||
# 入力値の検証
|
||||
if not zekken_number or not event_code:
|
||||
logger.error("Missing required parameters")
|
||||
return Response(
|
||||
{"error": "Both zekken_number and event_code are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# docbaseディレクトリのパスを絶対パスで設定
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
docbase_path = os.path.join(base_dir, 'docbase')
|
||||
|
||||
# ディレクトリ存在確認と作成
|
||||
os.makedirs(docbase_path, exist_ok=True)
|
||||
|
||||
# 設定ファイルのパス
|
||||
template_path = os.path.join(docbase_path, 'certificate_template.xlsx')
|
||||
ini_path = os.path.join(docbase_path, 'certificate.ini')
|
||||
|
||||
# テンプレートと設定ファイルの存在確認
|
||||
if not os.path.exists(template_path):
|
||||
logger.error(f"Template file not found: {template_path}")
|
||||
return Response(
|
||||
{"error": "Excel template file missing"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
if not os.path.exists(ini_path):
|
||||
logger.error(f"INI file not found: {ini_path}")
|
||||
return Response(
|
||||
{"error": "Configuration file missing"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# Docker環境用のデータベース設定を使用
|
||||
db_settings = settings.DATABASES['default']
|
||||
|
||||
|
||||
|
||||
# レスポンスの生成
|
||||
output.seek(0)
|
||||
response = HttpResponse(
|
||||
output.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename=./docbase/certificate_{zekken_number}.xlsx'
|
||||
return response
|
||||
# 初期化
|
||||
variables = {
|
||||
"zekken_number":str(zekken_number),
|
||||
"event_code":str(event_code),
|
||||
"db":str(db_settings['NAME']), #"rogdb",
|
||||
"username":str(db_settings['USER']), #"admin",
|
||||
"password":str(db_settings['PASSWORD']), #"admin123456",
|
||||
"host":str(db_settings['HOST']), # Docker Composeのサービス名を使用 # "localhost",
|
||||
"port":str(db_settings['PORT']), #"5432",
|
||||
"template_path": template_path
|
||||
}
|
||||
|
||||
# データベース接続情報のログ出力(パスワードは除く)
|
||||
logger.info(f"Attempting database connection to {variables['host']}:{variables['port']} "
|
||||
f"with user {variables['username']} and database {variables['db']}")
|
||||
|
||||
try:
|
||||
excel = SumasenExcel(document="certificate", variables=variables, docbase=docbase_path)
|
||||
# ./docbase/certificate.ini の定義をベースに、
|
||||
# ./docbase/certificate_template.xlsxを読み込み
|
||||
# ./docbase/certificate_(zekken_number).xlsxを作成する
|
||||
|
||||
# シート初期化
|
||||
logger.info("Generating report with variables: %s",
|
||||
{k: v for k, v in variables.items() if k != 'password'}) # パスワードを除外
|
||||
|
||||
ret = excel.make_report(variables=variables)
|
||||
if ret["status"]==True:
|
||||
filepath=ret["filepath"]
|
||||
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
|
||||
else:
|
||||
message = ret.get("message", "No message provided")
|
||||
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
|
||||
|
||||
output_path = ret.get("filepath")
|
||||
if not output_path or not os.path.exists(output_path):
|
||||
logger.error(f"Output file not found: {output_path}")
|
||||
return Response(
|
||||
{"error": "Generated file not found"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
excel_path = output_path
|
||||
|
||||
# PDFのファイル名を生成
|
||||
pdf_filename = f'certificate_{zekken_number}_{event_code}.pdf'
|
||||
pdf_path = os.path.join(docbase_path, pdf_filename)
|
||||
|
||||
# フォーマット指定(excel or pdf)
|
||||
format_type = request.query_params.get('format', 'pdf') # 'excel'
|
||||
|
||||
if format_type.lower() == 'pdf':
|
||||
try:
|
||||
# 一時ディレクトリを作成
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_excel = os.path.join(temp_dir, f'certificate_{zekken_number}.xlsx')
|
||||
temp_pdf = os.path.join(temp_dir, f'certificate_{zekken_number}.pdf')
|
||||
|
||||
# Excelファイルを一時ディレクトリにコピー
|
||||
shutil.copy2(excel_path, temp_excel)
|
||||
|
||||
# 一時ディレクトリのパーミッションを設定
|
||||
os.chmod(temp_dir, 0o777)
|
||||
os.chmod(temp_excel, 0o666)
|
||||
|
||||
logger.info(f"Converting Excel to PDF in temp directory: {temp_dir}")
|
||||
|
||||
|
||||
# LibreOfficeを使用してExcelをPDFに変換
|
||||
conversion_command = [
|
||||
'soffice',
|
||||
'--headless',
|
||||
'--convert-to',
|
||||
'pdf',
|
||||
'--outdir',
|
||||
temp_dir,
|
||||
temp_excel
|
||||
]
|
||||
|
||||
logger.debug(f"Running conversion command: {' '.join(conversion_command)}")
|
||||
|
||||
# 環境変数を設定
|
||||
env = os.environ.copy()
|
||||
env['HOME'] = temp_dir # LibreOfficeの設定ディレクトリを一時ディレクトリに設定
|
||||
|
||||
# 変換プロセスを実行
|
||||
process = subprocess.run(
|
||||
conversion_command,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
logger.debug(f"Conversion output: {process.stdout}")
|
||||
|
||||
# PDFファイルの存在確認
|
||||
if not os.path.exists(temp_pdf):
|
||||
logger.error("PDF conversion failed - output file not found")
|
||||
return Response(
|
||||
{"error": "PDF conversion failed - output file not found"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# PDFファイルを読み込んでレスポンスを返す
|
||||
with open(temp_pdf, 'rb') as pdf_file:
|
||||
pdf_content = pdf_file.read()
|
||||
|
||||
response = HttpResponse(pdf_content, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="certificate_{zekken_number}_{event_code}.pdf"'
|
||||
return response
|
||||
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Error converting to PDF: {str(e)}\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}")
|
||||
return Response(
|
||||
{"error": "PDF conversion failed"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
finally:
|
||||
# 一時ディレクトリの削除
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
logger.debug(f"Temporary directory removed: {temp_dir}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove temporary directory: {str(e)}")
|
||||
|
||||
|
||||
else: # Excel形式の場合
|
||||
with open(excel_path, 'rb') as excel_file:
|
||||
response = HttpResponse(
|
||||
excel_file.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
excel_filename = f'certificate_{zekken_number}_{event_code}.xlsx'
|
||||
response['Content-Disposition'] = f'attachment; filename="{excel_filename}"'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Excel/PDF generation: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": f"File generation failed: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in export_excel: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": "Failed to export file", "detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
finally:
|
||||
# 確実に一時ディレクトリを削除
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remove temporary directory in finally block: {str(e)}")
|
||||
|
||||
|
||||
# ----- for Supervisor -----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user