327 lines
13 KiB
Python
327 lines
13 KiB
Python
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django import forms
|
|
from django.http import HttpResponseRedirect
|
|
from django.contrib import messages
|
|
from django.urls import reverse
|
|
from .models import Person, AccessLog, WithdrawalRequest
|
|
from .withdrawal_utils import process_withdrawal_approval, reject_withdrawal_request
|
|
|
|
class PersonAdminForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Person
|
|
fields = '__all__'
|
|
widgets = {
|
|
'사진': forms.FileInput(attrs={
|
|
'style': 'border: 1px solid #ccc; padding: 5px; border-radius: 3px;'
|
|
})
|
|
}
|
|
|
|
@admin.register(Person)
|
|
class PersonAdmin(admin.ModelAdmin):
|
|
form = PersonAdminForm
|
|
list_display = ['SEQUENCE', '이름', '소속', '직책', '연락처', 'user', '모든사람보기권한', '비밀번호설정필요', '가입일시', '사진']
|
|
list_filter = ['모든사람보기권한', '비밀번호설정필요', '소속', '직책']
|
|
search_fields = ['이름', '소속', '직책', '연락처', 'keyword1', '소개글']
|
|
readonly_fields = ['수정일시', '사진미리보기', '가입일시']
|
|
list_editable = ['SEQUENCE']
|
|
list_display_links = ['이름']
|
|
ordering = ['이름']
|
|
|
|
fieldsets = (
|
|
('기본 정보', {
|
|
'fields': ('이름', '연락처', 'user')
|
|
}),
|
|
('상세 정보', {
|
|
'fields': ('소속', '직책', '주소', '생년월일')
|
|
}),
|
|
('미디어', {
|
|
'fields': ('사진', '사진미리보기')
|
|
}),
|
|
('추가 정보', {
|
|
'fields': ('keyword1', '소개글')
|
|
}),
|
|
('설정', {
|
|
'fields': ('모든사람보기권한', '비밀번호설정필요', 'TITLE', 'SEQUENCE', '가입일시')
|
|
}),
|
|
)
|
|
|
|
class Media:
|
|
css = {
|
|
'all': ('admin/css/custom_admin.css',)
|
|
}
|
|
|
|
def 사진미리보기(self, obj):
|
|
if obj.사진:
|
|
return format_html(
|
|
'<img src="{}" style="max-width: 100px; max-height: 100px; border-radius: 5px;" />',
|
|
obj.사진.url
|
|
)
|
|
return "사진 없음"
|
|
사진미리보기.short_description = '사진 미리보기'
|
|
|
|
def 모든사람보기권한(self, obj):
|
|
if obj.모든사람보기권한:
|
|
return format_html('<span style="color: green;">✓ 모든 사람 보기</span>')
|
|
else:
|
|
return format_html('<span style="color: blue;">👤 회원가입자만 보기</span>')
|
|
모든사람보기권한.short_description = '보기 권한'
|
|
|
|
def 비밀번호설정필요(self, obj):
|
|
if obj.비밀번호설정필요:
|
|
return format_html('<span style="color: red;">⚠️ 비밀번호 설정 필요</span>')
|
|
else:
|
|
return format_html('<span style="color: green;">✓ 비밀번호 설정 완료</span>')
|
|
비밀번호설정필요.short_description = '비밀번호 설정 상태'
|
|
|
|
def 수정일시(self, obj):
|
|
return obj.user.date_joined if obj.user else 'N/A'
|
|
수정일시.short_description = '수정일시'
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
return request.user.is_superuser
|
|
|
|
def has_add_permission(self, request):
|
|
return request.user.is_superuser
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
return request.user.is_superuser
|
|
|
|
def has_view_permission(self, request, obj=None):
|
|
return request.user.is_superuser
|
|
|
|
|
|
@admin.register(AccessLog)
|
|
class AccessLogAdmin(admin.ModelAdmin):
|
|
list_display = ['timestamp', '사용자명', 'action_display', 'ip_address', '기기정보', '브라우저정보', 'description']
|
|
list_filter = ['action', 'timestamp', 'ip_address']
|
|
search_fields = ['user__username', 'person__이름', 'description', 'ip_address']
|
|
readonly_fields = ['timestamp', 'user', 'person', 'action', 'description', 'ip_address', 'user_agent', 'session_key', 'metadata', '변경사항_상세보기', '기기정보', '브라우저정보']
|
|
date_hierarchy = 'timestamp'
|
|
ordering = ['-timestamp']
|
|
|
|
# 페이지당 표시할 항목 수
|
|
list_per_page = 50
|
|
|
|
def 사용자명(self, obj):
|
|
if obj.person:
|
|
return format_html('<strong>{}</strong>', obj.person.이름)
|
|
elif obj.user:
|
|
return format_html('<span style="color: #666;">{}</span>', obj.user.username)
|
|
else:
|
|
return format_html('<span style="color: #ccc;">익명</span>')
|
|
사용자명.short_description = '사용자'
|
|
사용자명.admin_order_field = 'person__이름'
|
|
|
|
def action_display(self, obj):
|
|
action_colors = {
|
|
'LOGIN': '#28a745', # 초록
|
|
'LOGOUT': '#6c757d', # 회색
|
|
'SIGNUP': '#007bff', # 파랑
|
|
'PROFILE_UPDATE': '#ffc107', # 노랑
|
|
'PASSWORD_CHANGE': '#fd7e14', # 주황
|
|
'PHONE_VERIFICATION': '#20c997', # 청록
|
|
'SEARCH': '#6f42c1', # 보라
|
|
'VIEW_PROFILE': '#17a2b8', # 하늘
|
|
'MAIN_ACCESS': '#343a40', # 어두운 회색
|
|
'ERROR': '#dc3545', # 빨강
|
|
'OTHER': '#6c757d', # 회색
|
|
}
|
|
color = action_colors.get(obj.action, '#6c757d')
|
|
return format_html(
|
|
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px;">{}</span>',
|
|
color, obj.get_action_display()
|
|
)
|
|
action_display.short_description = '활동'
|
|
action_display.admin_order_field = 'action'
|
|
|
|
def 기기정보(self, obj):
|
|
if obj.metadata and 'device_info' in obj.metadata:
|
|
device_info = obj.metadata['device_info']
|
|
# 기기별 아이콘 추가
|
|
icons = {
|
|
'iPhone': '📱',
|
|
'iPad': '📱',
|
|
'Android 폰': '📱',
|
|
'Android 태블릿': '📱',
|
|
'Windows 10/11': '💻',
|
|
'Windows 8.1': '💻',
|
|
'Windows 7': '💻',
|
|
'Windows': '💻',
|
|
'Mac': '💻',
|
|
'Linux': '💻',
|
|
}
|
|
icon = icons.get(device_info, '🖥️')
|
|
return format_html('{} {}', icon, device_info)
|
|
return format_html('<span style="color: #ccc;">알 수 없음</span>')
|
|
기기정보.short_description = '기기'
|
|
|
|
def 브라우저정보(self, obj):
|
|
if obj.metadata and 'browser_info' in obj.metadata:
|
|
browser_info = obj.metadata['browser_info']
|
|
# 브라우저별 색상 추가
|
|
colors = {
|
|
'Chrome': '#4285f4',
|
|
'Firefox': '#ff7139',
|
|
'Safari': '#1b88ca',
|
|
'Microsoft Edge': '#0078d4',
|
|
'Opera': '#ff1b2d',
|
|
}
|
|
color = colors.get(browser_info, '#6c757d')
|
|
return format_html(
|
|
'<span style="color: {}; font-weight: bold;">{}</span>',
|
|
color, browser_info
|
|
)
|
|
return format_html('<span style="color: #ccc;">알 수 없음</span>')
|
|
브라우저정보.short_description = '브라우저'
|
|
|
|
fieldsets = (
|
|
('기본 정보', {
|
|
'fields': ('timestamp', 'user', 'person', 'action', 'description')
|
|
}),
|
|
('접속 정보', {
|
|
'fields': ('ip_address', '기기정보', '브라우저정보', 'user_agent', 'session_key')
|
|
}),
|
|
('상세 변경사항', {
|
|
'fields': ('변경사항_상세보기',),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('추가 정보 (JSON)', {
|
|
'fields': ('metadata',),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def 변경사항_상세보기(self, obj):
|
|
"""변경사항을 보기 좋게 표시"""
|
|
if obj.action == 'PROFILE_UPDATE' and obj.metadata.get('field_changes'):
|
|
changes = obj.metadata['field_changes']
|
|
html_parts = ['<div style="font-family: monospace; background: #f8f9fa; padding: 10px; border-radius: 5px;">']
|
|
|
|
for field_name, change_data in changes.items():
|
|
old_value = change_data.get('old', '')
|
|
new_value = change_data.get('new', '')
|
|
|
|
html_parts.append(f'<div style="margin-bottom: 8px;">')
|
|
html_parts.append(f'<strong>{field_name}:</strong><br>')
|
|
html_parts.append(f'<span style="color: #dc3545;">이전: "{old_value}"</span><br>')
|
|
html_parts.append(f'<span style="color: #28a745;">이후: "{new_value}"</span>')
|
|
html_parts.append('</div>')
|
|
|
|
html_parts.append('</div>')
|
|
return format_html(''.join(html_parts))
|
|
else:
|
|
return '변경사항 없음'
|
|
|
|
변경사항_상세보기.short_description = '필드별 변경사항'
|
|
|
|
def has_add_permission(self, request):
|
|
return False # 로그는 시스템에서만 생성
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
return False # 로그는 수정 불가
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
return request.user.is_superuser # 슈퍼유저만 삭제 가능
|
|
|
|
|
|
@admin.register(WithdrawalRequest)
|
|
class WithdrawalRequestAdmin(admin.ModelAdmin):
|
|
list_display = ['request_date', '사용자명', 'status_display', 'approved_by', 'approved_date']
|
|
list_filter = ['status', 'request_date', 'approved_date']
|
|
search_fields = ['user__username', 'person__이름', 'reason']
|
|
readonly_fields = ['request_date', 'user', 'person', 'reason', 'backup_data']
|
|
date_hierarchy = 'request_date'
|
|
ordering = ['-request_date']
|
|
|
|
# 페이지당 표시할 항목 수
|
|
list_per_page = 30
|
|
|
|
def 사용자명(self, obj):
|
|
if obj.user:
|
|
return format_html('<strong>{}</strong> ({})', obj.person.이름, obj.user.username)
|
|
else:
|
|
# 탈퇴 승인된 경우 백업 데이터에서 정보 가져오기
|
|
username = obj.backup_data.get('user_info', {}).get('username', '탈퇴됨')
|
|
return format_html('<strong>{}</strong> (<span style="color: #dc3545;">{}</span>)', obj.person.이름, username)
|
|
사용자명.short_description = '사용자'
|
|
사용자명.admin_order_field = 'person__이름'
|
|
|
|
def status_display(self, obj):
|
|
status_colors = {
|
|
'PENDING': '#ffc107', # 노랑
|
|
'APPROVED': '#28a745', # 초록
|
|
'REJECTED': '#dc3545', # 빨강
|
|
}
|
|
color = status_colors.get(obj.status, '#6c757d')
|
|
return format_html(
|
|
'<span style="background-color: {}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px;">{}</span>',
|
|
color, obj.get_status_display()
|
|
)
|
|
status_display.short_description = '상태'
|
|
status_display.admin_order_field = 'status'
|
|
|
|
fieldsets = (
|
|
('기본 정보', {
|
|
'fields': ('request_date', 'user', 'person', 'status')
|
|
}),
|
|
('탈퇴 요청 내용', {
|
|
'fields': ('reason',)
|
|
}),
|
|
('승인 정보', {
|
|
'fields': ('approved_by', 'approved_date', 'admin_notes')
|
|
}),
|
|
('백업 데이터', {
|
|
'fields': ('backup_data',),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
actions = ['approve_withdrawal', 'reject_withdrawal']
|
|
|
|
def approve_withdrawal(self, request, queryset):
|
|
"""탈퇴 요청 승인"""
|
|
approved_count = 0
|
|
failed_count = 0
|
|
|
|
for withdrawal_request in queryset.filter(status='PENDING'):
|
|
try:
|
|
if process_withdrawal_approval(withdrawal_request, request.user, '관리자 일괄 승인'):
|
|
approved_count += 1
|
|
else:
|
|
failed_count += 1
|
|
except Exception as e:
|
|
failed_count += 1
|
|
self.message_user(request, f'{withdrawal_request.person.이름} 탈퇴 처리 실패: {e}', level=messages.ERROR)
|
|
|
|
if approved_count > 0:
|
|
self.message_user(request, f'{approved_count}건의 탈퇴 요청을 승인했습니다.', level=messages.SUCCESS)
|
|
if failed_count > 0:
|
|
self.message_user(request, f'{failed_count}건의 탈퇴 처리에 실패했습니다.', level=messages.WARNING)
|
|
|
|
approve_withdrawal.short_description = '선택된 탈퇴 요청 승인'
|
|
|
|
def reject_withdrawal(self, request, queryset):
|
|
"""탈퇴 요청 거부"""
|
|
rejected_count = 0
|
|
|
|
for withdrawal_request in queryset.filter(status='PENDING'):
|
|
if reject_withdrawal_request(withdrawal_request, request.user, '관리자 일괄 거부'):
|
|
rejected_count += 1
|
|
|
|
if rejected_count > 0:
|
|
self.message_user(request, f'{rejected_count}건의 탈퇴 요청을 거부했습니다.', level=messages.SUCCESS)
|
|
|
|
reject_withdrawal.short_description = '선택된 탈퇴 요청 거부'
|
|
|
|
def has_add_permission(self, request):
|
|
return False # 탈퇴 요청은 사용자가 직접 생성
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
# 승인 대기 중인 요청만 수정 가능
|
|
if obj and obj.status != 'PENDING':
|
|
return False
|
|
return request.user.is_superuser
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
return request.user.is_superuser |