import logging from datetime import timedelta from django.utils.translation import gettext as _ from django.utils import timezone from django.db import models from django.http.response import Http404 from django.db.models.signals import post_save from rest_framework.response import Response from rest_framework.permissions import IsAdminUser from social_django.models import UserSocialAuth from pingcap_sso.api.base import AdminEndpoint from pingcap_sso.api.permissions import MethodPermission from pingcap_sso.models import ( User, PortraitKeywordUser, UserTiDBUsage, FXCRMLeads, UserStaffAnnotation, UserProfileCollections, FXCRMStaffManualSyncLeads, UserHistory, UserInfoMisc, ) from pingcap_sso.lib.blog import blog_user_summary from pingcap_sso.lib.user import ( user_activities, user_profile_collections, high_frequency_addresses_by_track_events ) from pingcap_sso.utils import time as ptime from pingcap_sso.forms.admin_crm import ManualCreateCRMLeadsForm from pingcap_sso.forms.admin_edit_user_profile import AdminEditUserProfileForm from pingcap_sso.admin_audit_log.models import AdminAuditLog from pingcap_sso.signal_handlers.user_profile_collections import _update_primary_profile from pingcap_sso.tasks.user_profile_collections import update_user_primary_profile from pingcap_sso.discourse.lib import user_summary as asktug_user_summary logger = logging.getLogger(__name__) class FindUserEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, ) def get(self, request, *args, **kwargs): """audit_log: 搜索了用户""" search = request.GET.get('search', '') users = User.objects.filter(is_active=True) if search: users = users.filter( models.Q(username__icontains=search) | models.Q(email__icontains=search) | models.Q(phone__icontains=search)) return Response({ 'detail': _('success'), 'data': [{ 'id': u.id, 'username': u.username, 'email': u.email, 'phone': u.phone, 'openid': u.openid } for u in users[:20]] }) class UserListEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_user_list'] } sorted_fields = { 'experience': 'point_stat__experience', 'points': 'point_stat__points', 'last_login': 'last_login', 'date_joined': 'date_joined' } def get(self, request, *args, **kwargs): """audit_log: 查看了用户列表""" try: cur_page = int(request.GET.get('currentPage', 1)) per_page = int(request.GET.get('perPage', 10)) search = request.GET.get('search', '') sortBy = request.GET.get('sortBy', 'experience') sortDesc = request.GET.get('sortDesc', 'true') if sortBy == 'level': sortBy = 'experience' elif sortBy not in self.sorted_fields: sortBy = 'date_joined' sortByField = self.sorted_fields[sortBy] sortDesc = '' if sortDesc == 'false' else '-' except Exception: return Response({ 'detail': _('wrong params') }, 400) order_by = '{}{}'.format(sortDesc, sortByField) qs = User.objects.all().select_related('point_stat') if search: profile_qs = UserProfileCollections.objects.filter( models.Q(name__icontains=search) | models.Q(company__icontains=search) | models.Q(email__icontains=search) | models.Q(phone__icontains=search) | models.Q(wechat_id__icontains=search) | models.Q(github_id__icontains=search) ) qs = qs.filter( models.Q(username__icontains=search) | models.Q(openid=search) | models.Q(id__in=profile_qs.values('user_id')) ) qs = qs.order_by(order_by) total_num = qs.count() qs = qs[(cur_page - 1) * per_page:cur_page * per_page] user_ids = list(qs.values_list('id', flat=True)) profile_users = UserInfoMisc.objects.filter( user_id__in=user_ids ).values('user_id', 'name', 'company', 'phone', 'email', 'recent_half_year_ip_top3_region') user_profile_map = {p_u['user_id']: p_u for p_u in profile_users} data = list() for u in qs: user_data = { 'id': u.id, 'openid': u.openid, 'is_active': u.is_active, 'username': u.username, 'phone': user_profile_map.get(u.id, {'phone': u.phone})['phone'], 'email': user_profile_map.get(u.id, {'email': u.email})['email'], 'name': user_profile_map.get(u.id, {'name': u.name})['name'], 'company_name': user_profile_map.get(u.id, {'company': u.company_name})['company'], 'company_verified_status': u.company_verified_status, 'signup_channel': u.signup_channel, 'last_login': ptime.local_time_format(u.last_login, '%Y/%m/%d %H:%M'), 'recent_half_year_ip_top3_region': user_profile_map.get( u.id, { 'recent_half_year_ip_top3_region': [], 'all_ip_top3_region': [] } )['recent_half_year_ip_top3_region'], 'point_stat': None, } try: user_data['point_stat'] = { 'points': u.point_stat.points, 'experience': u.point_stat.experience, 'level': u.point_stat.level } except User.point_stat.RelatedObjectDoesNotExist: pass data.append(user_data) return Response({ 'detail': _('success'), 'data': data, 'total_num': total_num, 'per_page': per_page, 'cur_page': cur_page, 'order_by': order_by }) class UserDetailEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_user_profile'] } def get(self, request, username, *args, **kwargs): """audit_log: 查看了用户详情""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 can_view_user_contact_info = request.user.has_perm('pingcap_sso.view_user_contact_info') data = { 'id': user.id, 'openid': user.openid, 'is_active': user.is_active, 'username': user.username, 'avatar_url': user.avatar_url, 'name': user.name, 'bio': user.bio, 'date_of_birth': user.date_of_birth.strftime( '%Y-%m-%d') if user.date_of_birth else None, 'company_name': user.company_name, 'job_title_text': user.job_title_text, 'blog_user_summary': blog_user_summary(user), 'asktug_user_summary': asktug_user_summary(user), 'point_stat': None, 'is_contributor': user.is_contributor, 'pass_pcta': user.pass_pcta, 'pass_pctp': user.pass_pctp, 'pass_pcsd': user.pass_pcsd, 'pass_praczh': user.pass_praczh, 'github': None, 'wechat': None, 'tidb_usage': None, 'staff_annotation': None, 'crm_leads_maunal_sync_histories': [], 'crm_leads_count': FXCRMLeads.objects.filter(related_user=user).count(), 'staff_audit_logs_count': AdminAuditLog.objects.filter(user=user).count(), 'can_view_user_contact_info': can_view_user_contact_info, 'banned_details': None, } if not user.is_active: _banned_details = UserHistory.objects.filter( target_user=user, action=UserHistory.ACTION_BAN ).order_by('-created_at').first() if _banned_details: data['banned_details'] = { 'reason': _banned_details.details, 'created_by': _banned_details.acting_user.username, 'created_at': ptime.local_time_format(_banned_details.created_at) } if can_view_user_contact_info: data['phone'] = user.phone data['email'] = user.email data['email_verified'] = user.email_verified data['signup_email'] = user.signup_email data['signup_channel'] = user.signup_channel data['last_login'] = ptime.local_time_format(user.last_login) data['date_joined'] = ptime.local_time_format(user.date_joined, '%Y/%m/%d %H:%M') data['address'] = user.address data['gender_display'] = user.get_gender_display() data['is_tug_member'] = user.is_tug_member data['company_verified_status'] = user.company_verified_status data['company_verified_status_display'] = user.get_company_verified_status_display() data['profile_collections'] = user_profile_collections(user) try: github_usa = UserSocialAuth.objects.get(user=user, provider='github') data['github'] = { 'id': github_usa.uid, 'login': github_usa.extra_data.get('login') } except UserSocialAuth.DoesNotExist: pass try: wechat_usa = UserSocialAuth.objects.get(user=user, provider='wechat') data['wechat'] = { 'id': wechat_usa.uid, } except UserSocialAuth.DoesNotExist: pass portrait_ask_top_keywords = PortraitKeywordUser.objects.select_related( 'keyword' ).annotate( keyword_name=models.F('keyword__name'), num=models.F('ask_count') ).filter( user=user, ask_count__gt=0 ).values('id', 'keyword_name', 'num').order_by('-ask_count')[:20] portrait_reply_top_keywords = PortraitKeywordUser.objects.select_related( 'keyword' ).annotate( keyword_name=models.F('keyword__name'), num=models.F('reply_count') ).filter( user=user, reply_count__gt=0 ).values('id', 'keyword_name', 'num').order_by('-reply_count')[:20] portrait_read_top_keywords = PortraitKeywordUser.objects.select_related( 'keyword' ).annotate( keyword_name=models.F('keyword__name'), num=models.F('read_count') ).filter( user=user, read_count__gt=0 ).values('id', 'keyword_name', 'num').order_by('-read_count')[:20] data['portrait'] = { 'ask': portrait_ask_top_keywords, 'reply': portrait_reply_top_keywords, 'read': portrait_read_top_keywords, } activities_data, activities_date_range = user_activities(user) data['activities'] = { 'data': activities_data, 'date_range': activities_date_range } data['tidb_usage'] = UserTiDBUsage.objects.filter( user=user, deleted_at__isnull=True ).values('version').annotate( num=models.Count('id') ).order_by('-version') try: staff_annotation = user.userstaffannotation data['staff_annotation'] = { 'content': staff_annotation.content, 'editor': staff_annotation.editor.username, 'updated_at': ptime.local_time_format(staff_annotation.updated_at) } except User.userstaffannotation.RelatedObjectDoesNotExist: pass try: data['point_stat'] = { 'points': user.point_stat.points, 'experience': user.point_stat.experience, 'level': user.point_stat.level } except User.point_stat.RelatedObjectDoesNotExist: pass for cl in FXCRMStaffManualSyncLeads.objects.filter(user=user): data['crm_leads_maunal_sync_histories'].append({ 'id': cl.id, 'remark': cl.remark, 'creator': cl.creator.username, 'created_at': ptime.local_time_format(cl.created_at), 'leads': { 'id': cl.leads.id, 'crm_leads_object_id': cl.leads.crm_leads_object_id, 'sync_status': cl.leads.sync_status, 'sync_status_display': cl.leads.get_sync_status_display(), } }) return Response({ 'detail': _('success'), 'data': data }) class StaffAnnotationEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'PUT': ['pingcap_sso.view_user_profile'] } def put(self, request, username, *args, **kwargs): """audit_log: 更新了管理员备注""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 try: user.userstaffannotation.content = request.data.get('content') user.userstaffannotation.editor = request.user user.userstaffannotation.save() except User.userstaffannotation.RelatedObjectDoesNotExist: UserStaffAnnotation.objects.create( user=user, content=request.data.get('content'), editor=request.user ) return Response({ 'detail': _('success') }) class AdminEditProfileEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_user_profile'], 'PUT': ['pingcap_sso.view_user_profile'], } def get(self, request, username, *args, **kwargs): """audit_log: 获取管理员录入的用户信息""" try: user = User.objects.get(username=username) admin_edit_profile = UserProfileCollections.objects.get( user=user, source=UserProfileCollections.SOURCE_ADMIN_EDIT, deleted_at__isnull=True ) data = { 'name': admin_edit_profile.name, 'company': admin_edit_profile.company, 'job_title': admin_edit_profile.job_title, 'address': admin_edit_profile.address, 'phone': admin_edit_profile.phone, 'email': admin_edit_profile.email, 'wechat_id': admin_edit_profile.wechat_id, 'created_by_user': admin_edit_profile.created_by_user.username if admin_edit_profile.created_by_user else None, 'updated_at': ptime.local_time_format(admin_edit_profile.updated_at), } except User.DoesNotExist: raise Http404 except UserProfileCollections.DoesNotExist: data = { 'name': '', 'company': '', 'job_title': '', 'address': '', 'phone': '', 'email': '', 'wechat_id': '', 'created_by_user': '', 'updated_at': '', } return Response({ 'detail': _('success'), 'data': data }) def put(self, request, username, *args, **kwargs): """audit_log: 更新管理员录入的用户信息""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 try: admin_edit_profile = UserProfileCollections.objects.get( user=user, source=UserProfileCollections.SOURCE_ADMIN_EDIT, deleted_at__isnull=True ) except UserProfileCollections.DoesNotExist: admin_edit_profile = None form = AdminEditUserProfileForm( user=user, staff=request.user, data=request.data, instance=admin_edit_profile ) if not form.is_valid(): return Response({ 'detail': _('params error'), 'errors': form.errors }, status=400) post_save.disconnect(_update_primary_profile, sender=UserProfileCollections) form.save() update_user_primary_profile(user.id) post_save.connect(_update_primary_profile, sender=UserProfileCollections) return Response({ 'detail': _('success') }) class TiDBUsageEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_user_profile'] } def get(self, request, username, *args, **kwargs): """audit_log: 查看了用户的 TiDB 使用情况""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 data = [] for usage in UserTiDBUsage.objects.filter( user=user, topic_deleted_at__isnull=True, ).order_by('-topic_created_at'): data.append({ 'id': usage.id, 'topic_id': usage.asktug_topic_id, 'version': usage.version, 'cloud_provider': usage.cloud_provider, 'enviroment': usage.enviroment, 'deployment_opreation_system': usage.deployment_opreation_system, 'deployment_type': usage.deployment_type, 'tidb_cluster_data_size': usage.tidb_cluster_data_size, 'tidb_cluster_node_number': usage.tidb_cluster_node_number, 'created_at': ptime.local_time_format(usage.topic_created_at) }) return Response({ 'detail': _('success'), 'data': data }) class CRMLeadsListCreateEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_fxcrmleads'], 'POST': ['pingcap_sso.add_fxcrmstaffmanualsyncleads'] } def get(self, request, username, *args, **kwargs): """audit_log: 查看了用户的线索详情""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 data = [] for leads in FXCRMLeads.objects.filter( related_user=user ): data.append({ 'id': leads.id, 'leads_poll_id': leads.leads_poll_id, 'leads_poll_id_display': leads.get_leads_poll_id_display(), 'source': leads.source_text, 'company': leads.company, 'sync_status': leads.sync_status, 'sync_status_display': leads.get_sync_status_display(), 'sync_at': ptime.local_time_format(leads.sync_at), 'crm_leads_object_id': leads.crm_leads_object_id or '无' }) return Response({ 'detail': _('success'), 'data': data }) def post(self, request, username, *args, **kwargs): """audit_log: 为用户创建了 CRM 线索""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 form = ManualCreateCRMLeadsForm( user=user, staff=request.user, data=request.data) if not form.is_valid(): return Response({ 'detail': _('params error'), 'errors': form.errors }, status=400) form.save() return Response({ 'detail': _('success') }) class HighFrequencyAddressesByIPEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'GET': ['pingcap_sso.view_user_profile'] } def get(self, request, username, *args, **kwargs): """audit_log: 查看了用户的高频地址""" try: user = User.objects.get(username=username) except User.DoesNotExist: raise Http404 now = timezone.now() last_half_year = now - timedelta(days=31 * 6) return Response({ 'detail': _('success'), 'data': { 'all': high_frequency_addresses_by_track_events(user), 'last_half_year': high_frequency_addresses_by_track_events( user, last_half_year, now) } }) class BanUserEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'POST': ['pingcap_sso.delete_user'] } def post(self, request, username, *args, **kwargs): """audit_log: 封禁了用户""" reason = request.data.get('reason', '') user = User.objects.get(username=username) user.is_active = False user.save(update_fields=['is_active']) UserHistory.objects.create( target_user=user, acting_user=request.user, action=UserHistory.ACTION_BAN, details=reason ) return Response({ 'detail': _('success') }) class UnbanUserEndpoint(AdminEndpoint): permission_classes = (IsAdminUser, MethodPermission) perms_map = { 'POST': ['pingcap_sso.delete_user'] } def post(self, request, username, *args, **kwargs): """audit_log: 解封了用户""" reason = request.data.get('reason', '') user = User.objects.get(username=username) user.is_active = True user.save(update_fields=['is_active']) UserHistory.objects.create( target_user=user, acting_user=request.user, action=UserHistory.ACTION_UNBAN, details=reason ) return Response({ 'detail': _('success') })