Skip to content

Commit

Permalink
perf: finish this feat (jumpserver#14079)
Browse files Browse the repository at this point in the history
* perf: basic finished

* perf: finish this feat

* perf: add datetime demo

---------

Co-authored-by: ibuler <[email protected]>
  • Loading branch information
fit2bot and ibuler authored Sep 9, 2024
1 parent cf1dc79 commit 763fe77
Show file tree
Hide file tree
Showing 12 changed files with 660 additions and 178 deletions.
22 changes: 21 additions & 1 deletion apps/assets/serializers/asset/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=0)

def get_render_help_text(self):
if self.parent and self.parent.many:
return _('Protocols, format is ["protocol/port"]')
else:
return _('Protocol, format is name/port')

def to_file_representation(self, data):
return '{name}/{port}'.format(**data)

Expand Down Expand Up @@ -97,6 +103,9 @@ def validate(self, attrs):
attrs = super().validate(attrs)
return self.set_secret(attrs)

def get_render_help_text(self):
return _('Accounts, format [{"name": "x", "username": "x", "secret": "x", "secret_type": "password"}]')

class Meta(AccountSerializer.Meta):
fields = [
f for f in AccountSerializer.Meta.fields
Expand All @@ -121,12 +130,23 @@ class Meta:
}


class NodeDisplaySerializer(serializers.ListField):
def get_render_help_text(self):
return _('Node path, format ["/org_name/node_name"], if node not exist, will create it')

def to_internal_value(self, data):
return data

def to_representation(self, data):
return data


class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
_accounts = None

class Meta:
Expand Down
8 changes: 7 additions & 1 deletion apps/common/api/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ def match(self, request, *args, **kwargs):
class RenderToJsonMixin:
@action(methods=[POST, PUT], detail=False, url_path='render-to-json')
def render_to_json(self, request: Request, *args, **kwargs):
rows = request.data
if rows and isinstance(rows[0], dict):
first = list(rows[0].values())[0]
if first.startswith('#Help'):
rows.pop(0)

data = {
'title': (),
'data': request.data,
'data': rows,
}

jms_context = getattr(request, 'jms_context', {})
Expand Down
12 changes: 10 additions & 2 deletions apps/common/drf/parsers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ def parse_value(self, field, value):
value = field.to_file_internal_value(value)
elif isinstance(field, serializers.BooleanField):
value = value.lower() in ['true', '1', 'yes']
elif isinstance(field, serializers.ChoiceField):
value = value
elif isinstance(field, ObjectRelatedField):
if field.many:
value = [self.id_name_to_obj(v) for v in value]
Expand Down Expand Up @@ -164,6 +162,15 @@ def generate_data(self, fields_name, rows):
data.append(row_data)
return data

@staticmethod
def pop_help_text_if_need(rows):
rows = list(rows)
if not rows:
return rows
if rows[0][0] == '#Help':
rows.pop(0)
return rows

def parse(self, stream, media_type=None, parser_context=None):
assert parser_context is not None, '`parser_context` should not be `None`'

Expand Down Expand Up @@ -192,6 +199,7 @@ def parse(self, stream, media_type=None, parser_context=None):
request.jms_context = {}
request.jms_context['column_title_field_pairs'] = column_title_field_pairs

rows = self.pop_help_text_if_need(rows)
data = self.generate_data(field_names, rows)
return data
except Exception as e:
Expand Down
72 changes: 67 additions & 5 deletions apps/common/drf/renders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

import pyzipper
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.renderers import BaseRenderer
from rest_framework.utils import encoders, json

from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.serializers import fields as common_fields
from common.utils import get_logger

logger = get_logger(__file__)
Expand Down Expand Up @@ -38,8 +39,10 @@ def set_response_disposition(self, response):
filename_prefix = serializer.Meta.model.__name__.lower()
else:
filename_prefix = 'download'
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = "{}_{}.{}".format(filename_prefix, now, self.format)
suffix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if self.template == 'import':
suffix = 'template'
filename = "{}_{}.{}".format(filename_prefix, suffix, self.format)
disposition = 'attachment; filename="{}"'.format(filename)
response['Content-Disposition'] = disposition

Expand Down Expand Up @@ -105,10 +108,10 @@ def render_value(self, field, value):
value = field.to_file_representation(value)
elif isinstance(value, bool):
value = 'Yes' if value else 'No'
elif isinstance(field, LabeledChoiceField):
elif isinstance(field, common_fields.LabeledChoiceField):
value = value or {}
value = '{}({})'.format(value.get('label'), value.get('value'))
elif isinstance(field, ObjectRelatedField):
elif isinstance(field, common_fields.ObjectRelatedField):
if field.many:
value = [self.to_id_name(v) for v in value]
else:
Expand All @@ -126,6 +129,53 @@ def render_value(self, field, value):
value = json.dumps(value, cls=encoders.JSONEncoder, ensure_ascii=False)
return str(value)

def get_field_help_text(self, field):
text = ''
if hasattr(field, 'get_render_help_text'):
text = field.get_render_help_text()
elif isinstance(field, serializers.BooleanField):
text = _('Yes/No')
elif isinstance(field, serializers.CharField):
if field.max_length:
text = _('Text, max length {}').format(field.max_length)
else:
text = _("Long text, no length limit")
elif isinstance(field, serializers.IntegerField):
text = _('Number, min {} max {}').format(field.min_value, field.max_value)
text = text.replace('min None', '').replace('max None', '')
elif isinstance(field, serializers.DateTimeField):
text = _('Datetime format {}').format(timezone.now().strftime(settings.REST_FRAMEWORK['DATETIME_FORMAT']))
elif isinstance(field, serializers.IPAddressField):
text = _('IP')
elif isinstance(field, serializers.ChoiceField):
choices = [str(v) for v in field.choices.keys()]
if isinstance(field, common_fields.LabeledChoiceField):
text = _("Choices, format name(value), name is optional for human read,"
" value is requisite, options {}").format(','.join(choices))
else:
text = _("Choices, options {}").format(",".join(choices))
elif isinstance(field, common_fields.PhoneField):
text = _("Phone number, format +8612345678901")
elif isinstance(field, common_fields.LabeledChoiceField):
text = _('Label, format ["key:value"]')
elif isinstance(field, common_fields.ObjectRelatedField):
text = _("Object, format name(id), name is optional for human read, id is requisite")
elif isinstance(field, serializers.PrimaryKeyRelatedField):
text = _('Object, format id')
elif isinstance(field, serializers.ManyRelatedField):
child_relation_class_name = field.child_relation.__class__.__name__
if child_relation_class_name == "ObjectRelatedField":
text = _('Objects, format ["name(id)", ...], name is optional for human read, id is requisite')
elif child_relation_class_name == "LabelRelatedField":
text = _('Labels, format ["key:value", ...], if label not exists, will create it')
else:
text = _('Objects, format ["id", ...]')
elif isinstance(field, serializers.ListSerializer):
child = field.child
if hasattr(child, 'get_render_help_text'):
text = child.get_render_help_text()
return text

def generate_rows(self, data, render_fields):
for item in data:
row = []
Expand All @@ -135,6 +185,17 @@ def generate_rows(self, data, render_fields):
row.append(value)
yield row

def write_help_text_if_need(self):
if self.template == 'export':
return
fields = self.get_rendered_fields()
row = []
for f in fields:
text = self.get_field_help_text(f)
row.append(text)
row[0] = '#Help ' + str(row[0])
self.write_row(row)

@abc.abstractmethod
def initial_writer(self):
raise NotImplementedError
Expand Down Expand Up @@ -184,6 +245,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
rows = self.generate_rows(data, rendered_fields)
self.initial_writer()
self.write_column_titles(column_titles)
self.write_help_text_if_need()
self.write_rows(rows)
self.after_render()
value = self.get_rendered_value()
Expand Down
4 changes: 2 additions & 2 deletions apps/common/drf/renders/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
#

import codecs

import unicodecsv
from six import BytesIO

from .base import BaseFileRenderer
from ..const import CSV_FILE_ESCAPE_CHARS

class CSVFileRenderer(BaseFileRenderer):

class CSVFileRenderer(BaseFileRenderer):
media_type = 'text/csv'
format = 'csv'

writer = None
buffer = None

Expand Down
Loading

0 comments on commit 763fe77

Please sign in to comment.