mirror of https://github.com/jumpserver/jumpserver
				
				
				
			[Update] Rename app
							parent
							
								
									edce831e46
								
							
						
					
					
						commit
						08775551c2
					
				| 
						 | 
				
			
			@ -11,7 +11,7 @@ from ..models import LoginConfirmSetting
 | 
			
		|||
from ..serializers import LoginConfirmSettingSerializer
 | 
			
		||||
from .. import errors
 | 
			
		||||
 | 
			
		||||
__all__ = ['LoginConfirmSettingUpdateApi', 'UserOrderAcceptAuthApi']
 | 
			
		||||
__all__ = ['LoginConfirmSettingUpdateApi', 'UserTicketAcceptAuthApi']
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,27 +30,42 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
 | 
			
		|||
        return s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserOrderAcceptAuthApi(APIView):
 | 
			
		||||
class UserTicketAcceptAuthApi(APIView):
 | 
			
		||||
    permission_classes = ()
 | 
			
		||||
 | 
			
		||||
    def get(self, request, *args, **kwargs):
 | 
			
		||||
        from orders.models import LoginConfirmOrder
 | 
			
		||||
        order_id = self.request.session.get("auth_order_id")
 | 
			
		||||
        logger.debug('Login confirm order id: {}'.format(order_id))
 | 
			
		||||
        if not order_id:
 | 
			
		||||
            order = None
 | 
			
		||||
        from tickets.models import LoginConfirmTicket
 | 
			
		||||
        ticket_id = self.request.session.get("auth_ticket_id")
 | 
			
		||||
        logger.debug('Login confirm ticket id: {}'.format(ticket_id))
 | 
			
		||||
        if not ticket_id:
 | 
			
		||||
            ticket = None
 | 
			
		||||
        else:
 | 
			
		||||
            order = get_object_or_none(LoginConfirmOrder, pk=order_id)
 | 
			
		||||
            ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
 | 
			
		||||
        try:
 | 
			
		||||
            if not order:
 | 
			
		||||
                raise errors.LoginConfirmOrderNotFound(order_id)
 | 
			
		||||
            if order.status == order.STATUS_ACCEPTED:
 | 
			
		||||
            if not ticket:
 | 
			
		||||
                raise errors.LoginConfirmTicketNotFound(ticket_id)
 | 
			
		||||
            if ticket.action == LoginConfirmTicket.ACTION_APPROVE:
 | 
			
		||||
                self.request.session["auth_confirm"] = "1"
 | 
			
		||||
                return Response({"msg": "ok"})
 | 
			
		||||
            elif order.status == order.STATUS_REJECTED:
 | 
			
		||||
                raise errors.LoginConfirmRejectedError(order_id)
 | 
			
		||||
            elif ticket.action == LoginConfirmTicket.ACTION_REJECT:
 | 
			
		||||
                raise errors.LoginConfirmRejectedError(ticket_id)
 | 
			
		||||
            else:
 | 
			
		||||
                raise errors.LoginConfirmWaitError(order_id)
 | 
			
		||||
                raise errors.LoginConfirmWaitError(ticket_id)
 | 
			
		||||
        except errors.AuthFailedError as e:
 | 
			
		||||
            data = e.as_data()
 | 
			
		||||
            return Response(data, status=400)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserTicketCancelAuthApi(APIView):
 | 
			
		||||
    permission_classes = ()
 | 
			
		||||
 | 
			
		||||
    def get(self, request, *args, **kwargs):
 | 
			
		||||
        from tickets.models import LoginConfirmTicket
 | 
			
		||||
        ticket_id = self.request.session.get("auth_ticket_id")
 | 
			
		||||
        logger.debug('Login confirm ticket id: {}'.format(ticket_id))
 | 
			
		||||
        if not ticket_id:
 | 
			
		||||
            ticket = None
 | 
			
		||||
        else:
 | 
			
		||||
            ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
 | 
			
		||||
        if not ticket:
 | 
			
		||||
            ticket.status = "close"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,9 +47,9 @@ mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
 | 
			
		|||
 | 
			
		||||
mfa_required_msg = _("MFA required")
 | 
			
		||||
login_confirm_required_msg = _("Login confirm required")
 | 
			
		||||
login_confirm_wait_msg = _("Wait login confirm order for accept")
 | 
			
		||||
login_confirm_rejected_msg = _("Login confirm order was rejected")
 | 
			
		||||
login_confirm_order_not_found_msg = _("Order not found")
 | 
			
		||||
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
 | 
			
		||||
login_confirm_rejected_msg = _("Login confirm ticket was rejected")
 | 
			
		||||
login_confirm_ticket_not_found_msg = _("Ticket not found")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthFailedNeedLogMixin:
 | 
			
		||||
| 
						 | 
				
			
			@ -155,8 +155,8 @@ class LoginConfirmError(AuthFailedError):
 | 
			
		|||
    msg = login_confirm_wait_msg
 | 
			
		||||
    error = 'login_confirm_wait'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, order_id, **kwargs):
 | 
			
		||||
        self.order_id = order_id
 | 
			
		||||
    def __init__(self, ticket_id, **kwargs):
 | 
			
		||||
        self.ticket_id = ticket_id
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def as_data(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -164,7 +164,7 @@ class LoginConfirmError(AuthFailedError):
 | 
			
		|||
            "error": self.error,
 | 
			
		||||
            "msg": self.msg,
 | 
			
		||||
            "data": {
 | 
			
		||||
                "order_id": self.order_id
 | 
			
		||||
                "ticket_id": self.ticket_id
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,6 +179,6 @@ class LoginConfirmRejectedError(LoginConfirmError):
 | 
			
		|||
    error = 'login_confirm_rejected'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderNotFound(LoginConfirmError):
 | 
			
		||||
    msg = login_confirm_order_not_found_msg
 | 
			
		||||
    error = 'login_confirm_order_not_found'
 | 
			
		||||
class LoginConfirmTicketNotFound(LoginConfirmError):
 | 
			
		||||
    msg = login_confirm_ticket_not_found_msg
 | 
			
		||||
    error = 'login_confirm_ticket_not_found'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,30 +91,30 @@ class AuthMixin:
 | 
			
		|||
        raise errors.MFAFailedError(username=user.username, request=self.request)
 | 
			
		||||
 | 
			
		||||
    def check_user_login_confirm_if_need(self, user):
 | 
			
		||||
        from orders.models import LoginConfirmOrder
 | 
			
		||||
        from tickets.models import LoginConfirmTicket
 | 
			
		||||
        confirm_setting = user.get_login_confirm_setting()
 | 
			
		||||
        if self.request.session.get('auth_confirm') or not confirm_setting:
 | 
			
		||||
            return
 | 
			
		||||
        order = None
 | 
			
		||||
        if self.request.session.get('auth_order_id'):
 | 
			
		||||
            order_id = self.request.session['auth_order_id']
 | 
			
		||||
            order = get_object_or_none(LoginConfirmOrder, pk=order_id)
 | 
			
		||||
        if not order:
 | 
			
		||||
            order = confirm_setting.create_confirm_order(self.request)
 | 
			
		||||
            self.request.session['auth_order_id'] = str(order.id)
 | 
			
		||||
        ticket = None
 | 
			
		||||
        if self.request.session.get('auth_ticket_id'):
 | 
			
		||||
            ticket_id = self.request.session['auth_ticket_id']
 | 
			
		||||
            ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
 | 
			
		||||
        if not ticket:
 | 
			
		||||
            ticket = confirm_setting.create_confirm_ticket(self.request)
 | 
			
		||||
            self.request.session['auth_ticket_id'] = str(ticket.id)
 | 
			
		||||
 | 
			
		||||
        if order.status == "accepted":
 | 
			
		||||
        if ticket.status == "accepted":
 | 
			
		||||
            return
 | 
			
		||||
        elif order.status == "rejected":
 | 
			
		||||
            raise errors.LoginConfirmRejectedError(order.id)
 | 
			
		||||
        elif ticket.status == "rejected":
 | 
			
		||||
            raise errors.LoginConfirmRejectedError(ticket.id)
 | 
			
		||||
        else:
 | 
			
		||||
            raise errors.LoginConfirmWaitError(order.id)
 | 
			
		||||
            raise errors.LoginConfirmWaitError(ticket.id)
 | 
			
		||||
 | 
			
		||||
    def clear_auth_mark(self):
 | 
			
		||||
        self.request.session['auth_password'] = ''
 | 
			
		||||
        self.request.session['auth_mfa'] = ''
 | 
			
		||||
        self.request.session['auth_confirm'] = ''
 | 
			
		||||
        self.request.session['auth_order_id'] = ''
 | 
			
		||||
        self.request.session['auth_ticket_id'] = ''
 | 
			
		||||
 | 
			
		||||
    def send_auth_signal(self, success=True, user=None, username='', reason=''):
 | 
			
		||||
        if success:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,8 +48,8 @@ class LoginConfirmSetting(CommonModelMixin):
 | 
			
		|||
    def get_user_confirm_setting(cls, user):
 | 
			
		||||
        return get_object_or_none(cls, user=user)
 | 
			
		||||
 | 
			
		||||
    def create_confirm_order(self, request=None):
 | 
			
		||||
        from orders.models import LoginConfirmOrder
 | 
			
		||||
    def create_confirm_ticket(self, request=None):
 | 
			
		||||
        from tickets.models import LoginConfirmTicket
 | 
			
		||||
        title = _('User login confirm: {}').format(self.user)
 | 
			
		||||
        if request:
 | 
			
		||||
            remote_addr = get_request_ip(request)
 | 
			
		||||
| 
						 | 
				
			
			@ -58,20 +58,20 @@ class LoginConfirmSetting(CommonModelMixin):
 | 
			
		|||
                self.user, remote_addr, city, timezone.now()
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            city = ''
 | 
			
		||||
            remote_addr = ''
 | 
			
		||||
            city = 'Localhost'
 | 
			
		||||
            remote_addr = '127.0.0.1'
 | 
			
		||||
            body = ''
 | 
			
		||||
        reviewer = self.reviewers.all()
 | 
			
		||||
        reviewer_names = ','.join([u.name for u in reviewer])
 | 
			
		||||
        order = LoginConfirmOrder.objects.create(
 | 
			
		||||
        ticket = LoginConfirmTicket.objects.create(
 | 
			
		||||
            user=self.user, user_display=str(self.user),
 | 
			
		||||
            title=title, body=body,
 | 
			
		||||
            city=city, ip=remote_addr,
 | 
			
		||||
            assignees_display=reviewer_names,
 | 
			
		||||
            type=LoginConfirmOrder.TYPE_LOGIN_CONFIRM,
 | 
			
		||||
            type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM,
 | 
			
		||||
        )
 | 
			
		||||
        order.assignees.set(reviewer)
 | 
			
		||||
        return order
 | 
			
		||||
        ticket.assignees.set(reviewer)
 | 
			
		||||
        return ticket
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{} confirm'.format(self.user.username)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ urlpatterns = [
 | 
			
		|||
    path('connection-token/',
 | 
			
		||||
         api.UserConnectionTokenApi.as_view(), name='connection-token'),
 | 
			
		||||
    path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
 | 
			
		||||
    path('order/auth/', api.UserOrderAcceptAuthApi.as_view(), name='user-order-auth'),
 | 
			
		||||
    path('order/auth/', api.UserTicketAcceptAuthApi.as_view(), name='user-order-auth'),
 | 
			
		||||
    path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -126,8 +126,8 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
 | 
			
		|||
            return self.format_redirect_url(self.login_otp_url)
 | 
			
		||||
        confirm_setting = user.get_login_confirm_setting()
 | 
			
		||||
        if confirm_setting and not self.request.session.get('auth_confirm'):
 | 
			
		||||
            order = confirm_setting.create_confirm_order(self.request)
 | 
			
		||||
            self.request.session['auth_order_id'] = str(order.id)
 | 
			
		||||
            ticket = confirm_setting.create_confirm_ticket(self.request)
 | 
			
		||||
            self.request.session['auth_ticket_id'] = str(ticket.id)
 | 
			
		||||
            url = self.format_redirect_url(self.login_confirm_url)
 | 
			
		||||
            return url
 | 
			
		||||
        self.login_success(user)
 | 
			
		||||
| 
						 | 
				
			
			@ -159,26 +159,26 @@ class UserLoginWaitConfirmView(TemplateView):
 | 
			
		|||
    template_name = 'authentication/login_wait_confirm.html'
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        from orders.models import LoginConfirmOrder
 | 
			
		||||
        order_id = self.request.session.get("auth_order_id")
 | 
			
		||||
        if not order_id:
 | 
			
		||||
            order = None
 | 
			
		||||
        from tickets.models import LoginConfirmTicket
 | 
			
		||||
        ticket_id = self.request.session.get("auth_ticket_id")
 | 
			
		||||
        if not ticket_id:
 | 
			
		||||
            ticket = None
 | 
			
		||||
        else:
 | 
			
		||||
            order = get_object_or_none(LoginConfirmOrder, pk=order_id)
 | 
			
		||||
            ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        if order:
 | 
			
		||||
            order_detail_url = reverse('orders:login-confirm-order-detail', kwargs={'pk': order_id})
 | 
			
		||||
            timestamp_created = datetime.datetime.timestamp(order.date_created)
 | 
			
		||||
        if ticket:
 | 
			
		||||
            ticket_detail_url = reverse('tickets:login-confirm-ticket-detail', kwargs={'pk': ticket_id})
 | 
			
		||||
            timestamp_created = datetime.datetime.timestamp(ticket.date_created)
 | 
			
		||||
            msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
 | 
			
		||||
                  Don't close this page""").format(order.assignees_display)
 | 
			
		||||
                  Don't close this page""").format(ticket.assignees_display)
 | 
			
		||||
        else:
 | 
			
		||||
            timestamp_created = 0
 | 
			
		||||
            order_detail_url = ''
 | 
			
		||||
            msg = _("No order found")
 | 
			
		||||
            ticket_detail_url = ''
 | 
			
		||||
            msg = _("No ticket found")
 | 
			
		||||
        context.update({
 | 
			
		||||
            "msg": msg,
 | 
			
		||||
            "timestamp": timestamp_created,
 | 
			
		||||
            "order_detail_url": order_detail_url
 | 
			
		||||
            "ticket_detail_url": ticket_detail_url
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ INSTALLED_APPS = [
 | 
			
		|||
    'audits.apps.AuditsConfig',
 | 
			
		||||
    'authentication.apps.AuthenticationConfig',  # authentication
 | 
			
		||||
    'applications.apps.ApplicationsConfig',
 | 
			
		||||
    'orders.apps.OrdersConfig',
 | 
			
		||||
    'tickets.apps.TicketsConfig',
 | 
			
		||||
    'rest_framework',
 | 
			
		||||
    'rest_framework_swagger',
 | 
			
		||||
    'drf_yasg',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ api_v1 = [
 | 
			
		|||
    path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
 | 
			
		||||
    path('common/', include('common.urls.api_urls', namespace='api-common')),
 | 
			
		||||
    path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
 | 
			
		||||
    path('orders/', include('orders.urls.api_urls', namespace='api-orders')),
 | 
			
		||||
    path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
api_v2 = [
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ app_view_patterns = [
 | 
			
		|||
    path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
 | 
			
		||||
    path('auth/', include('authentication.urls.view_urls'), name='auth'),
 | 
			
		||||
    path('applications/', include('applications.urls.views_urls', namespace='applications')),
 | 
			
		||||
    path('orders/', include('orders.urls.views_urls', namespace='orders')),
 | 
			
		||||
    path('tickets/', include('tickets.urls.views_urls', namespace='tickets')),
 | 
			
		||||
    re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,39 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import viewsets, generics
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
 | 
			
		||||
from common.permissions import IsValidUser
 | 
			
		||||
from common.mixins import CommonApiMixin
 | 
			
		||||
from . import serializers
 | 
			
		||||
from .models import LoginConfirmOrder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderViewSet(CommonApiMixin, viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = serializers.LoginConfirmOrderSerializer
 | 
			
		||||
    permission_classes = (IsValidUser,)
 | 
			
		||||
    filter_fields = ['status', 'title']
 | 
			
		||||
    search_fields = ['user_display', 'title', 'ip', 'city']
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = LoginConfirmOrder.objects.all()\
 | 
			
		||||
            .filter(assignees=self.request.user)
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderCreateActionApi(generics.CreateAPIView):
 | 
			
		||||
    permission_classes = (IsValidUser,)
 | 
			
		||||
    serializer_class = serializers.LoginConfirmOrderActionSerializer
 | 
			
		||||
 | 
			
		||||
    def get_order(self):
 | 
			
		||||
        order_id = self.kwargs.get('pk')
 | 
			
		||||
        queryset = LoginConfirmOrder.objects.all()\
 | 
			
		||||
            .filter(assignees=self.request.user)
 | 
			
		||||
        order = get_object_or_404(queryset, id=order_id)
 | 
			
		||||
        return order
 | 
			
		||||
 | 
			
		||||
    def get_serializer_context(self):
 | 
			
		||||
        context = super().get_serializer_context()
 | 
			
		||||
        order = self.get_order()
 | 
			
		||||
        context['order'] = order
 | 
			
		||||
        return context
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrdersConfig(AppConfig):
 | 
			
		||||
    name = 'orders'
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        from . import signals_handler
 | 
			
		||||
        return super().ready()
 | 
			
		||||
| 
						 | 
				
			
			@ -1,72 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .models import LoginConfirmOrder, Comment
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = LoginConfirmOrder
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'user', 'user_display', 'title', 'body',
 | 
			
		||||
            'ip', 'city', 'assignees', 'assignees_display',
 | 
			
		||||
            'type', 'status', 'date_created', 'date_updated',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderActionSerializer(serializers.Serializer):
 | 
			
		||||
    ACTION_CHOICES = (
 | 
			
		||||
        ('accept', _('Accept')),
 | 
			
		||||
        ('reject', _('Reject')),
 | 
			
		||||
        ('comment', _('Comment'))
 | 
			
		||||
    )
 | 
			
		||||
    action = serializers.ChoiceField(choices=ACTION_CHOICES)
 | 
			
		||||
    comment = serializers.CharField(allow_blank=True)
 | 
			
		||||
 | 
			
		||||
    def update(self, instance, validated_data):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def create_comments(self, order, user, validated_data):
 | 
			
		||||
        comment_data = validated_data.get('comment')
 | 
			
		||||
        action = validated_data.get('action')
 | 
			
		||||
        comments_data = []
 | 
			
		||||
        if comment_data:
 | 
			
		||||
            comments_data.append(comment_data)
 | 
			
		||||
            Comment.objects.create(
 | 
			
		||||
                order_id=order.id, body=comment_data, user=user,
 | 
			
		||||
                user_display=str(user)
 | 
			
		||||
            )
 | 
			
		||||
        if action != "comment":
 | 
			
		||||
            action_display = dict(self.ACTION_CHOICES).get(action)
 | 
			
		||||
            comment_data = '{} {} {}'.format(user, action_display, _("this order"))
 | 
			
		||||
            comments_data.append(comment_data)
 | 
			
		||||
        comments = [
 | 
			
		||||
            Comment(order_id=order.id, body=data, user=user, user_display=str(user))
 | 
			
		||||
            for data in comments_data
 | 
			
		||||
        ]
 | 
			
		||||
        Comment.objects.bulk_create(comments)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def perform_action(order, user, validated_data):
 | 
			
		||||
        action = validated_data.get('action')
 | 
			
		||||
        if action == "accept":
 | 
			
		||||
            status = "accepted"
 | 
			
		||||
        elif action == "reject":
 | 
			
		||||
            status = "rejected"
 | 
			
		||||
        else:
 | 
			
		||||
            status = None
 | 
			
		||||
 | 
			
		||||
        if status:
 | 
			
		||||
            order.status = status
 | 
			
		||||
            order.assignee = user
 | 
			
		||||
            order.assignee_display = str(user)
 | 
			
		||||
            order.save()
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        order = self.context['order']
 | 
			
		||||
        user = self.context['request'].user
 | 
			
		||||
        self.create_comments(order, user, validated_data)
 | 
			
		||||
        self.perform_action(order, user, validated_data)
 | 
			
		||||
        return validated_data
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.db.models.signals import m2m_changed, post_save
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from .models import LoginConfirmOrder
 | 
			
		||||
from .utils import (
 | 
			
		||||
    send_login_confirm_order_mail_to_assignees,
 | 
			
		||||
    send_login_confirm_action_mail_to_user
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(m2m_changed, sender=LoginConfirmOrder.assignees.through)
 | 
			
		||||
def on_login_confirm_order_assignees_set(sender, instance=None, action=None,
 | 
			
		||||
                                        model=None, pk_set=None, **kwargs):
 | 
			
		||||
    if action == 'post_add':
 | 
			
		||||
        logger.debug('New order create, send mail: {}'.format(instance.id))
 | 
			
		||||
        assignees = model.objects.filter(pk__in=pk_set)
 | 
			
		||||
        send_login_confirm_order_mail_to_assignees(instance, assignees)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=LoginConfirmOrder)
 | 
			
		||||
def on_login_confirm_order_status_change(sender, instance=None, created=False, **kwargs):
 | 
			
		||||
    if created or instance.status == "pending":
 | 
			
		||||
        return
 | 
			
		||||
    logger.debug('Order changed, send mail: {}'.format(instance.id))
 | 
			
		||||
    send_login_confirm_action_mail_to_user(instance)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,20 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from rest_framework.routers import DefaultRouter
 | 
			
		||||
 | 
			
		||||
from .. import api
 | 
			
		||||
 | 
			
		||||
app_name = 'orders'
 | 
			
		||||
router = DefaultRouter()
 | 
			
		||||
 | 
			
		||||
router.register('login-confirm-orders', api.LoginConfirmOrderViewSet, 'login-confirm-order')
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login-confirm-order/<uuid:pk>/actions/',
 | 
			
		||||
         api.LoginConfirmOrderCreateActionApi.as_view(),
 | 
			
		||||
         name='login-confirm-order-create-action'
 | 
			
		||||
         ),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
urlpatterns += router.urls
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from .. import views
 | 
			
		||||
 | 
			
		||||
app_name = 'orders'
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login-confirm-orders/', views.LoginConfirmOrderListView.as_view(), name='login-confirm-order-list'),
 | 
			
		||||
    path('login-confirm-orders/<uuid:pk>/', views.LoginConfirmOrderDetailView.as_view(), name='login-confirm-order-detail')
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger, reverse
 | 
			
		||||
from common.tasks import send_mail_async
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_login_confirm_order_mail_to_assignees(order, assignees):
 | 
			
		||||
    recipient_list = [user.email for user in assignees]
 | 
			
		||||
    user = order.user
 | 
			
		||||
    if not recipient_list:
 | 
			
		||||
        logger.error("Order not has assignees: {}".format(order.id))
 | 
			
		||||
        return
 | 
			
		||||
    subject = '{}: {}'.format(_("New order"), order.title)
 | 
			
		||||
    detail_url = reverse('orders:login-confirm-order-detail',
 | 
			
		||||
                         kwargs={'pk': order.id}, external=True)
 | 
			
		||||
    message = _("""
 | 
			
		||||
        <div>
 | 
			
		||||
            <p>Your has a new order</p>
 | 
			
		||||
            <div>
 | 
			
		||||
                <b>Title:</b> {order.title}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>User:</b> {user}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Assignees:</b> {order.assignees_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>City:</b> {order.city}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>IP:</b> {order.ip}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <a href={url}>click here to review</a> 
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    """).format(order=order, user=user, url=detail_url)
 | 
			
		||||
    send_mail_async.delay(subject, message, recipient_list, html_message=message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_login_confirm_action_mail_to_user(order):
 | 
			
		||||
    if not order.user:
 | 
			
		||||
        logger.error("Order not has user: {}".format(order.id))
 | 
			
		||||
        return
 | 
			
		||||
    user = order.user
 | 
			
		||||
    recipient_list = [user.email]
 | 
			
		||||
    subject = '{}: {}'.format(_("Order has been reply"), order.title)
 | 
			
		||||
    message = _("""
 | 
			
		||||
        <div>
 | 
			
		||||
            <p>Your order has been replay</p>
 | 
			
		||||
            <div>
 | 
			
		||||
                <b>Title:</b> {order.title}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Assignee:</b> {order.assignee_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Status:</b> {order.status_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
     """).format(order=order)
 | 
			
		||||
    send_mail_async.delay(subject, message, recipient_list, html_message=message)
 | 
			
		||||
| 
						 | 
				
			
			@ -554,3 +554,7 @@ span.select2-selection__placeholder {
 | 
			
		|||
    height: 22px;
 | 
			
		||||
    max-width: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.table-striped.table-bordered {
 | 
			
		||||
    width: 100% !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,8 @@
 | 
			
		|||
<style>
 | 
			
		||||
    li.dropdown-submenu {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
<ul class="dropdown-menu multi-level search-help" role="menu" aria-labelledby="dropdownMenu">
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,12 +122,12 @@
 | 
			
		|||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE %}
 | 
			
		||||
    <li id="orders">
 | 
			
		||||
    <li id="tickets">
 | 
			
		||||
        <a>
 | 
			
		||||
            <i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Orders' %}</span><span class="fa arrow"></span>
 | 
			
		||||
            <i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Tickets' %}</span><span class="fa arrow"></span>
 | 
			
		||||
        </a>
 | 
			
		||||
        <ul class="nav nav-second-level">
 | 
			
		||||
            <li id="login-confirm-orders"><a href="{% url 'orders:login-confirm-order-list' %}">{% trans 'Login confirm' %}</a></li>
 | 
			
		||||
            <li id="login-confirm-orders"><a href="{% url 'tickets:login-confirm-ticket-list' %}">{% trans 'Login confirm' %}</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </li>
 | 
			
		||||
{% endif %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from .base import *
 | 
			
		||||
from .login_confirm import *
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import viewsets, generics
 | 
			
		||||
 | 
			
		||||
from .. import serializers, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = serializers.TicketSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = models.Ticket.objects.all().none()
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = serializers.CommentSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = models.Comment.objects.none()
 | 
			
		||||
        return queryset
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import viewsets, generics
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
 | 
			
		||||
from common.permissions import IsValidUser
 | 
			
		||||
from common.mixins import CommonApiMixin
 | 
			
		||||
from .. import serializers
 | 
			
		||||
from ..models import LoginConfirmTicket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmTicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = serializers.LoginConfirmTicketSerializer
 | 
			
		||||
    permission_classes = (IsValidUser,)
 | 
			
		||||
    filter_fields = ['status', 'title']
 | 
			
		||||
    search_fields = ['user_display', 'title', 'ip', 'city']
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = LoginConfirmTicket.objects.all()\
 | 
			
		||||
            .filter(assignees=self.request.user)
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmTicketsCreateActionApi(generics.CreateAPIView):
 | 
			
		||||
    permission_classes = (IsValidUser,)
 | 
			
		||||
    serializer_class = serializers.LoginConfirmTicketActionSerializer
 | 
			
		||||
 | 
			
		||||
    def get_ticket(self):
 | 
			
		||||
        ticket_id = self.kwargs.get('pk')
 | 
			
		||||
        queryset = LoginConfirmTicket.objects.all()\
 | 
			
		||||
            .filter(assignees=self.request.user)
 | 
			
		||||
        ticket = get_object_or_404(queryset, id=ticket_id)
 | 
			
		||||
        return ticket
 | 
			
		||||
 | 
			
		||||
    def get_serializer_context(self):
 | 
			
		||||
        context = super().get_serializer_context()
 | 
			
		||||
        ticket = self.get_ticket()
 | 
			
		||||
        context['ticket'] = ticket
 | 
			
		||||
        return context
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketsConfig(AppConfig):
 | 
			
		||||
    name = 'tickets'
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
# Generated by Django 2.2.5 on 2019-10-31 10:23
 | 
			
		||||
# Generated by Django 2.2.5 on 2019-11-07 08:02
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ class Migration(migrations.Migration):
 | 
			
		|||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='LoginConfirmOrder',
 | 
			
		||||
            name='Ticket',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
 | 
			
		||||
                ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
 | 
			
		||||
| 
						 | 
				
			
			@ -27,18 +27,28 @@ class Migration(migrations.Migration):
 | 
			
		|||
                ('body', models.TextField(verbose_name='Body')),
 | 
			
		||||
                ('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
 | 
			
		||||
                ('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
 | 
			
		||||
                ('type', models.CharField(choices=[('login_confirm', 'Login confirm')], max_length=16, verbose_name='Type')),
 | 
			
		||||
                ('status', models.CharField(choices=[('accepted', 'Accepted'), ('rejected', 'Rejected'), ('pending', 'Pending')], default='pending', max_length=16)),
 | 
			
		||||
                ('ip', models.GenericIPAddressField(blank=True, null=True)),
 | 
			
		||||
                ('city', models.CharField(blank=True, default='', max_length=16)),
 | 
			
		||||
                ('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
 | 
			
		||||
                ('assignees', models.ManyToManyField(related_name='loginconfirmorder_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')),
 | 
			
		||||
                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='loginconfirmorder_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')),
 | 
			
		||||
                ('type', models.CharField(default='general', max_length=16, verbose_name='Type')),
 | 
			
		||||
                ('status', models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16)),
 | 
			
		||||
                ('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
 | 
			
		||||
                ('assignees', models.ManyToManyField(related_name='ticket_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')),
 | 
			
		||||
                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'ordering': ('-date_created',),
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='LoginConfirmTicket',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('ticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tickets.Ticket')),
 | 
			
		||||
                ('ip', models.GenericIPAddressField(blank=True, null=True)),
 | 
			
		||||
                ('city', models.CharField(blank=True, default='', max_length=16)),
 | 
			
		||||
                ('action', models.CharField(blank=True, choices=[('approve', 'Approve'), ('reject', 'Reject')], default='', max_length=16)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
            bases=('tickets.ticket',),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Comment',
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +57,9 @@ class Migration(migrations.Migration):
 | 
			
		|||
                ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
 | 
			
		||||
                ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
 | 
			
		||||
                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
 | 
			
		||||
                ('order_id', models.UUIDField()),
 | 
			
		||||
                ('user_display', models.CharField(max_length=128, verbose_name='User display name')),
 | 
			
		||||
                ('body', models.TextField(verbose_name='Body')),
 | 
			
		||||
                ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.Ticket')),
 | 
			
		||||
                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from .base import *
 | 
			
		||||
from .login_confirm import *
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +1,26 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from common.mixins.models import CommonModelMixin
 | 
			
		||||
 | 
			
		||||
__all__ = ['LoginConfirmOrder', 'Comment']
 | 
			
		||||
__all__ = ['Ticket', 'Comment']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Comment(CommonModelMixin):
 | 
			
		||||
    order_id = models.UUIDField()
 | 
			
		||||
    user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
 | 
			
		||||
    user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
 | 
			
		||||
    body = models.TextField(verbose_name=_("Body"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ('date_created', )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseOrder(CommonModelMixin):
 | 
			
		||||
    STATUS_ACCEPTED = 'accepted'
 | 
			
		||||
    STATUS_REJECTED = 'rejected'
 | 
			
		||||
    STATUS_PENDING = 'pending'
 | 
			
		||||
class Ticket(CommonModelMixin):
 | 
			
		||||
    STATUS_OPEN = 'open'
 | 
			
		||||
    STATUS_CLOSED = 'closed'
 | 
			
		||||
    STATUS_CHOICES = (
 | 
			
		||||
        (STATUS_ACCEPTED, _("Accepted")),
 | 
			
		||||
        (STATUS_REJECTED, _("Rejected")),
 | 
			
		||||
        (STATUS_PENDING, _("Pending"))
 | 
			
		||||
        (STATUS_OPEN, _("Open")),
 | 
			
		||||
        (STATUS_CLOSED, _("Closed"))
 | 
			
		||||
    )
 | 
			
		||||
    TYPE_GENERAL = 'general'
 | 
			
		||||
    TYPE_LOGIN_CONFIRM = 'login_confirm'
 | 
			
		||||
    TYPE_CHOICES = (
 | 
			
		||||
        (TYPE_LOGIN_CONFIRM, 'Login confirm'),
 | 
			
		||||
        (TYPE_GENERAL, _("General")),
 | 
			
		||||
        (TYPE_LOGIN_CONFIRM, _("Login confirm"))
 | 
			
		||||
    )
 | 
			
		||||
    user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
 | 
			
		||||
    user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
 | 
			
		||||
| 
						 | 
				
			
			@ -38,8 +31,8 @@ class BaseOrder(CommonModelMixin):
 | 
			
		|||
    assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
 | 
			
		||||
    assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
 | 
			
		||||
    assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
 | 
			
		||||
    type = models.CharField(choices=TYPE_CHOICES, max_length=16, verbose_name=_('Type'))
 | 
			
		||||
    status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='pending')
 | 
			
		||||
    type = models.CharField(max_length=16, default='general', verbose_name=_("Type"))
 | 
			
		||||
    status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{}: {}'.format(self.user_display, self.title)
 | 
			
		||||
| 
						 | 
				
			
			@ -57,10 +50,16 @@ class BaseOrder(CommonModelMixin):
 | 
			
		|||
        return self.get_status_display()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
        ordering = ('-date_created',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrder(BaseOrder):
 | 
			
		||||
    ip = models.GenericIPAddressField(blank=True, null=True)
 | 
			
		||||
    city = models.CharField(max_length=16, blank=True, default='')
 | 
			
		||||
class Comment(CommonModelMixin):
 | 
			
		||||
    ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
 | 
			
		||||
    user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
 | 
			
		||||
    user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
 | 
			
		||||
    body = models.TextField(verbose_name=_("Body"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ('date_created', )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .base import Ticket
 | 
			
		||||
 | 
			
		||||
__all__ = ['LoginConfirmTicket']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmTicket(Ticket):
 | 
			
		||||
    ACTION_APPROVE = 'approve'
 | 
			
		||||
    ACTION_REJECT = 'reject'
 | 
			
		||||
    ACTION_CHOICES = (
 | 
			
		||||
        (ACTION_APPROVE, _('Approve')),
 | 
			
		||||
        (ACTION_REJECT, _('Reject')),
 | 
			
		||||
    )
 | 
			
		||||
    ip = models.GenericIPAddressField(blank=True, null=True)
 | 
			
		||||
    city = models.CharField(max_length=16, blank=True, default='')
 | 
			
		||||
    action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from .base import *
 | 
			
		||||
from .login_confirm import *
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .. import models
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketSerializer', 'CommentSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Ticket
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'user', 'user_display', 'title', 'body',
 | 
			
		||||
            'assignees', 'assignees_display',
 | 
			
		||||
            'status', 'date_created', 'date_updated',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'user_display', 'assignees_display',
 | 
			
		||||
            'date_created', 'date_updated',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Comment
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'ticket', 'body', 'user', 'user_display',
 | 
			
		||||
            'date_created', 'date_updated'
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'user_display', 'date_created', 'date_updated'
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .base import TicketSerializer
 | 
			
		||||
from ..models import LoginConfirmTicket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['LoginConfirmTicketSerializer', 'LoginConfirmTicketActionSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmTicketSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = LoginConfirmTicket
 | 
			
		||||
        fields = TicketSerializer.Meta.fields + [
 | 
			
		||||
            'ip', 'city', 'action'
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = TicketSerializer.Meta.read_only_fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmTicketActionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    comment = serializers.CharField(allow_blank=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = LoginConfirmTicket
 | 
			
		||||
        fields = ['action', 'comment']
 | 
			
		||||
 | 
			
		||||
    def update(self, instance, validated_data):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        pass
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.db.models.signals import m2m_changed, post_save
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from .models import LoginConfirmTicket
 | 
			
		||||
from .utils import (
 | 
			
		||||
    send_login_confirm_ticket_mail_to_assignees,
 | 
			
		||||
    send_login_confirm_action_mail_to_user
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(m2m_changed, sender=LoginConfirmTicket.assignees.through)
 | 
			
		||||
def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None,
 | 
			
		||||
                                        model=None, pk_set=None, **kwargs):
 | 
			
		||||
    if action == 'post_add':
 | 
			
		||||
        logger.debug('New ticket create, send mail: {}'.format(instance.id))
 | 
			
		||||
        assignees = model.objects.filter(pk__in=pk_set)
 | 
			
		||||
        send_login_confirm_ticket_mail_to_assignees(instance, assignees)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=LoginConfirmTicket)
 | 
			
		||||
def on_login_confirm_ticket_status_change(sender, instance=None, created=False, **kwargs):
 | 
			
		||||
    if created or instance.status == "pending":
 | 
			
		||||
        return
 | 
			
		||||
    logger.debug('Ticket changed, send mail: {}'.format(instance.id))
 | 
			
		||||
    send_login_confirm_action_mail_to_user(instance)
 | 
			
		||||
| 
						 | 
				
			
			@ -112,9 +112,9 @@
 | 
			
		|||
{% endblock %}
 | 
			
		||||
{% block custom_foot_js %}
 | 
			
		||||
<script>
 | 
			
		||||
var orderId = "{{ object.id }}";
 | 
			
		||||
var ticketId = "{{ object.id }}";
 | 
			
		||||
var status = "{{ object.status }}";
 | 
			
		||||
var actionCreateUrl = "{% url 'api-orders:login-confirm-order-create-action' pk=object.id %}";
 | 
			
		||||
var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=object.id %}";
 | 
			
		||||
$(document).ready(function () {
 | 
			
		||||
    if (status !== "pending") {
 | 
			
		||||
        $('.btn-update').attr('disabled', '1')
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
{% block custom_head_css_js %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% block table_container %}
 | 
			
		||||
<table class="table table-striped table-bordered table-hover " id="login_confirm_order_list_table" >
 | 
			
		||||
<table class="table table-striped table-bordered table-hover " id="login_confirm_ticket_list_table" >
 | 
			
		||||
    <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th class="text-center">
 | 
			
		||||
| 
						 | 
				
			
			@ -27,15 +27,15 @@
 | 
			
		|||
{% block content_bottom_left %}{% endblock %}
 | 
			
		||||
{% block custom_foot_js %}
 | 
			
		||||
<script>
 | 
			
		||||
var orderTable = 0;
 | 
			
		||||
var ticketTable = 0;
 | 
			
		||||
function initTable() {
 | 
			
		||||
     var options = {
 | 
			
		||||
        ele: $('#login_confirm_order_list_table'),
 | 
			
		||||
        oSearch: {sSearch: "status:pending"},
 | 
			
		||||
        ele: $('#login_confirm_ticket_list_table'),
 | 
			
		||||
        oSearch: {sSearch: "status:open"},
 | 
			
		||||
        columnDefs: [
 | 
			
		||||
            {targets: 1, createdCell: function (td, cellData, rowData) {
 | 
			
		||||
                cellData = htmlEscape(cellData);
 | 
			
		||||
                var detailBtn = '<a href="{% url "orders:login-confirm-order-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
 | 
			
		||||
                var detailBtn = '<a href="{% url "tickets:login-confirm-ticket-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
 | 
			
		||||
                $(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
 | 
			
		||||
            }},
 | 
			
		||||
            {targets: 3, createdCell: function (td, cellData, rowData) {
 | 
			
		||||
| 
						 | 
				
			
			@ -43,12 +43,14 @@ function initTable() {
 | 
			
		|||
                $(td).html(d)
 | 
			
		||||
            }},
 | 
			
		||||
            {targets: 4, createdCell: function (td, cellData, rowData) {
 | 
			
		||||
                if (cellData === "accepted") {
 | 
			
		||||
                if (cellData === "approval") {
 | 
			
		||||
                    $(td).html('<i class="fa fa-check text-navy"></i>')
 | 
			
		||||
                } else if (cellData === "rejected") {
 | 
			
		||||
                    $(td).html('<i class="fa fa-times text-danger"></i>')
 | 
			
		||||
                } else if (cellData === "pending") {
 | 
			
		||||
                } else if (cellData === "open") {
 | 
			
		||||
                    $(td).html('<i class="fa fa-spinner text-info"></i>')
 | 
			
		||||
                } else {
 | 
			
		||||
                    $(td).html('<i class="fa fa-circle text-info"></i>')
 | 
			
		||||
                }
 | 
			
		||||
             }},
 | 
			
		||||
            {targets: 5, createdCell: function (td, cellData) {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,25 +64,25 @@ function initTable() {
 | 
			
		|||
                    rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData);
 | 
			
		||||
                var acceptBtnRef = $(acceptBtn);
 | 
			
		||||
                var rejectBtnRef = $(rejectBtn);
 | 
			
		||||
                if (rowData.status !== "pending") {
 | 
			
		||||
                if (rowData.action !== "") {
 | 
			
		||||
                    acceptBtnRef.attr('disabled', 'disabled');
 | 
			
		||||
                    rejectBtnRef.attr('disabled', 'disabled');
 | 
			
		||||
                }
 | 
			
		||||
                var btn = acceptBtnRef.prop('outerHTML') + ' ' + rejectBtnRef.prop('outerHTML');
 | 
			
		||||
                $(td).html(btn)
 | 
			
		||||
             }}],
 | 
			
		||||
        ajax_url: '{% url "api-orders:login-confirm-order-list" %}',
 | 
			
		||||
        ajax_url: '{% url "api-tickets:login-confirm-ticket-list" %}',
 | 
			
		||||
        columns: [
 | 
			
		||||
            {data: "id"}, {data: "title", className: "text-left"},
 | 
			
		||||
            {data: "id"}, {data: "title"},
 | 
			
		||||
            {data: "user_display"}, {data: "ip"},
 | 
			
		||||
            {data: "status", orderable: false, width: "30px"},
 | 
			
		||||
            {data: "status", ticketable: false},
 | 
			
		||||
            {data: "date_created", width: "120px"},
 | 
			
		||||
            {data: "id", orderable: false, width: "100px"}
 | 
			
		||||
            {data: "id", ticketable: false}
 | 
			
		||||
        ],
 | 
			
		||||
        op_html: $('#actions').html()
 | 
			
		||||
    };
 | 
			
		||||
    orderTable = jumpserver.initServerSideDataTable(options);
 | 
			
		||||
    return orderTable
 | 
			
		||||
    ticketTable = jumpserver.initServerSideDataTable(options);
 | 
			
		||||
    return ticketTable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).ready(function(){
 | 
			
		||||
| 
						 | 
				
			
			@ -89,16 +91,19 @@ $(document).ready(function(){
 | 
			
		|||
        {title: "IP", value: "ip"},
 | 
			
		||||
        {title: "{% trans 'Title' %}", value: "title"},
 | 
			
		||||
        {title: "{% trans 'Status' %}", value: "status", submenu: [
 | 
			
		||||
                {title: "{% trans 'Pending' %}", value: "pending"},
 | 
			
		||||
                {title: "{% trans 'Accepted' %}", value: "accepted"},
 | 
			
		||||
                {title: "{% trans 'Rejected' %}", value: "rejected"}
 | 
			
		||||
        ]}
 | 
			
		||||
                {title: "{% trans 'Open' %}", value: "open"},
 | 
			
		||||
                {title: "{% trans 'Closed' %}", value: "closed"},
 | 
			
		||||
        ]},
 | 
			
		||||
        {title: "{% trans 'Action' %}", value: "action", submenu: [
 | 
			
		||||
                {title: "{% trans 'Approve' %}", value: "approve"},
 | 
			
		||||
                {title: "{% trans 'Reject' %}", value: "reject"},
 | 
			
		||||
        ]},
 | 
			
		||||
    ];
 | 
			
		||||
    initTableFilterDropdown('#login_confirm_order_list_table_filter input', menu)
 | 
			
		||||
    initTableFilterDropdown('#login_confirm_ticket_list_table_filter input', menu)
 | 
			
		||||
}).on('click', '.btn-action', function () {
 | 
			
		||||
    var actionCreateUrl = "{% url 'api-orders:login-confirm-order-create-action' pk=DEFAULT_PK %}";
 | 
			
		||||
    var orderId = $(this).data('uid');
 | 
			
		||||
    actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", orderId);
 | 
			
		||||
    var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=DEFAULT_PK %}";
 | 
			
		||||
    var ticketId = $(this).data('uid');
 | 
			
		||||
    actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", ticketId);
 | 
			
		||||
    var action = $(this).data('action');
 | 
			
		||||
    var comment = '';
 | 
			
		||||
    var data = {
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from rest_framework.routers import DefaultRouter
 | 
			
		||||
 | 
			
		||||
from .. import api
 | 
			
		||||
 | 
			
		||||
app_name = 'tickets'
 | 
			
		||||
router = DefaultRouter()
 | 
			
		||||
 | 
			
		||||
router.register('tickets', api.TicketViewSet, 'ticket')
 | 
			
		||||
router.register('login-confirm-tickets', api.LoginConfirmTicketViewSet, 'login-confirm-ticket')
 | 
			
		||||
router.register('tickets/<uuid:ticket_id>/comments/', api.CommentViewSet, 'ticket-comment')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login-confirm-tickets/<uuid:pk>/actions/',
 | 
			
		||||
         api.LoginConfirmTicketsCreateActionApi.as_view(),
 | 
			
		||||
         name='login-confirm-ticket-create-action'
 | 
			
		||||
    ),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
urlpatterns += router.urls
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from .. import views
 | 
			
		||||
 | 
			
		||||
app_name = 'tickets'
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('login-confirm-tickets/', views.LoginConfirmTicketListView.as_view(), name='login-confirm-ticket-list'),
 | 
			
		||||
    path('login-confirm-tickets/<uuid:pk>/', views.LoginConfirmTicketDetailView.as_view(), name='login-confirm-ticket-detail')
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger, reverse
 | 
			
		||||
from common.tasks import send_mail_async
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_login_confirm_ticket_mail_to_assignees(ticket, assignees):
 | 
			
		||||
    recipient_list = [user.email for user in assignees]
 | 
			
		||||
    user = ticket.user
 | 
			
		||||
    if not recipient_list:
 | 
			
		||||
        logger.error("Ticket not has assignees: {}".format(ticket.id))
 | 
			
		||||
        return
 | 
			
		||||
    subject = '{}: {}'.format(_("New ticket"), ticket.title)
 | 
			
		||||
    detail_url = reverse('tickets:login-confirm-ticket-detail',
 | 
			
		||||
                         kwargs={'pk': ticket.id}, external=True)
 | 
			
		||||
    message = _("""
 | 
			
		||||
        <div>
 | 
			
		||||
            <p>Your has a new ticket</p>
 | 
			
		||||
            <div>
 | 
			
		||||
                <b>Title:</b> {ticket.title}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>User:</b> {user}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Assignees:</b> {ticket.assignees_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>City:</b> {ticket.city}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>IP:</b> {ticket.ip}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <a href={url}>click here to review</a> 
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    """).format(ticket=ticket, user=user, url=detail_url)
 | 
			
		||||
    send_mail_async.delay(subject, message, recipient_list, html_message=message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_login_confirm_action_mail_to_user(ticket):
 | 
			
		||||
    if not ticket.user:
 | 
			
		||||
        logger.error("Ticket not has user: {}".format(ticket.id))
 | 
			
		||||
        return
 | 
			
		||||
    user = ticket.user
 | 
			
		||||
    recipient_list = [user.email]
 | 
			
		||||
    subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title)
 | 
			
		||||
    message = _("""
 | 
			
		||||
        <div>
 | 
			
		||||
            <p>Your ticket has been replay</p>
 | 
			
		||||
            <div>
 | 
			
		||||
                <b>Title:</b> {ticket.title}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Assignee:</b> {ticket.assignee_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
                <b>Status:</b> {ticket.status_display}
 | 
			
		||||
                <br/>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
     """).format(ticket=ticket)
 | 
			
		||||
    send_mail_async.delay(subject, message, recipient_list, html_message=message)
 | 
			
		||||
| 
						 | 
				
			
			@ -2,33 +2,33 @@ from django.views.generic import TemplateView, DetailView
 | 
			
		|||
from django.utils.translation import ugettext as _
 | 
			
		||||
 | 
			
		||||
from common.permissions import PermissionsMixin, IsOrgAdmin
 | 
			
		||||
from .models import LoginConfirmOrder
 | 
			
		||||
from .models import LoginConfirmTicket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderListView(PermissionsMixin, TemplateView):
 | 
			
		||||
    template_name = 'orders/login_confirm_order_list.html'
 | 
			
		||||
class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
 | 
			
		||||
    template_name = 'tickets/login_confirm_ticket_list.html'
 | 
			
		||||
    permission_classes = (IsOrgAdmin,)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context.update({
 | 
			
		||||
            'app': _("Orders"),
 | 
			
		||||
            'action': _("Login confirm order list")
 | 
			
		||||
            'app': _("Tickets"),
 | 
			
		||||
            'action': _("Login confirm ticket list")
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginConfirmOrderDetailView(PermissionsMixin, DetailView):
 | 
			
		||||
    template_name = 'orders/login_confirm_order_detail.html'
 | 
			
		||||
class LoginConfirmTicketDetailView(PermissionsMixin, DetailView):
 | 
			
		||||
    template_name = 'tickets/login_confirm_ticket_detail.html'
 | 
			
		||||
    permission_classes = (IsOrgAdmin,)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return LoginConfirmOrder.objects.filter(assignees=self.request.user)
 | 
			
		||||
        return LoginConfirmTicket.objects.filter(assignees=self.request.user)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context.update({
 | 
			
		||||
            'app': _("Orders"),
 | 
			
		||||
            'action': _("Login confirm order detail")
 | 
			
		||||
            'app': _("Tickets"),
 | 
			
		||||
            'action': _("Login confirm ticket detail")
 | 
			
		||||
        })
 | 
			
		||||
        return context
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,7 @@
 | 
			
		|||
{% endblock %}
 | 
			
		||||
{% block table_container %}
 | 
			
		||||
<div class="uc pull-left m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
 | 
			
		||||
<table class="table table-striped table-bordered table-hover " id="user_list_table" >
 | 
			
		||||
<table class="table table-striped table-bordered table-hover " id="user_list_table">
 | 
			
		||||
    <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th class="text-center">
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ function initTable() {
 | 
			
		|||
            {data: "groups_display", orderable: false},
 | 
			
		||||
            {data: "source"},
 | 
			
		||||
            {data: "is_valid", orderable: false},
 | 
			
		||||
            {data: "id", orderable: false, width: "100px"}
 | 
			
		||||
            {data: "id", orderable: false}
 | 
			
		||||
        ],
 | 
			
		||||
        op_html: $('#actions').html()
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue