Merge branch 'master' of ssh://git.cy55.de/home/git/cybertools

This commit is contained in:
Helmut Merz 2015-06-12 07:31:58 +02:00
commit 8824044726
23 changed files with 250 additions and 74 deletions

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -37,9 +37,9 @@ class Controller(BaseController):
macros = self.macros
presentationMode = self.request.get('liquid.viewmode') == 'presentation'
params = [('blue/blue8.css', 'all', 20, False),
('print.css', 'print', 25, False),
('blue/ie.css', 'all', 30, True),
('custom.css', 'all', 100, False)]
('custom.css', 'all', 100, False),
('print.css', 'print', 200, False),]
#if presentationMode:
# params.append(('presentation.css', 'all', 30, False))
for id, media, prio, ie in params:

BIN
browser/icons/page_copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

View file

@ -1,5 +1,5 @@
<metal:page define-macro="page"
tal:condition="view/update"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
tal:condition="view/update"><!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
tal:define="body view/body;
layout view/context/template">

View file

@ -83,6 +83,7 @@ class Report(Template):
queryCriteria = None
outputFields = ()
sortCriteria = ()
sortDescending = False
limits = None

View file

@ -81,12 +81,14 @@ class GroupHeaderRow(BaseRow):
class ResultSet(object):
def __init__(self, context, data, rowFactory=Row,
sortCriteria=None, queryCriteria=BaseQueryCriteria(),
sortCriteria=None, sortDescending=False,
queryCriteria=BaseQueryCriteria(),
limits=None):
self.context = context # the report or report instance
self.data = data
self.rowFactory = rowFactory
self.sortCriteria = sortCriteria
self.sortDescending = sortDescending
self.queryCriteria = queryCriteria
self.limits = limits
self.totals = BaseRow(None, self)
@ -95,7 +97,9 @@ class ResultSet(object):
result = [self.rowFactory(item, self) for item in self.data]
result = [row for row in result if self.queryCriteria.check(row)]
if self.sortCriteria:
result.sort(key=lambda x: [f.getSortValue(x) for f in self.sortCriteria])
result.sort(key=lambda x:
[f.getSortValue(x) for f in self.sortCriteria],
reverse=self.sortDescending)
if self.limits:
start, stop = self.limits
result = result[start:stop]

View file

@ -90,7 +90,7 @@
tal:define="width field/width|nothing"
tal:attributes="name name; id name;
style python:
('width: %s;;' % (width and str(width)+'px' or '570px')) +
('width: %s;;' % (width and str(width)+'px' or '555px')) +
'height: 1.5em;;';
value data/?name|string:;
xxrequired field/required_js;" />
@ -144,7 +144,11 @@
<metal:upload define-macro="input_fileupload">
<input type="file" name="field"
tal:attributes="name name;" />
tal:attributes="name name;"
onchange="if (this.form.title.value == '') {
var value = this.value.split('\\');
this.form.title.value = value[value.length-1];
}" />
</metal:upload>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -41,6 +41,14 @@ from cybertools.composer.schema.schema import formErrors
from cybertools.util.format import toStr, toUnicode
class FieldGroup(object):
def __init__(self, name, label, sublabels=[]):
self.name = name
self.label = label
self.sublabels = sublabels
class Field(Component):
implements(IField)

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -42,7 +42,33 @@ class GridFieldInstance(ListFieldInstance):
@Lazy
def columnTypes(self):
return [createField(t) for t in self.context.column_types]
fields = [createField(t) for t in self.context.column_types]
for f in fields:
f.linkedFields = [createField(sf)
for sf in getattr(f.baseField, 'linkedFields', [])]
return fields
#@Lazy
def columnTypesForLayout(self):
result = []
groups = {}
for idx, f in enumerate(self.columnTypes):
group = getattr(f.baseField, 'group', None)
if group is None:
result.append(dict(name=f.name,
label=(f.description or f.title),
fields=[f], indexes=[idx], group=None))
else:
g = groups.get(group.name)
if g is None:
g = dict(name=group.name, label=group.label,
fields=[f], indexes=[idx], group=group)
groups[group.name] = g
result.append(g)
else:
g['fields'].append(f)
g['indexes'].append(idx)
return result
@Lazy
def columnFieldInstances(self):
@ -122,6 +148,8 @@ class GridFieldInstance(ListFieldInstance):
def unmarshallRow(self, row, idx=None):
item = {}
cardinality = getattr(self.context, 'cardinality', None)
ignoreInCheckOnEmpty = list(
getattr(self.context, 'ignoreInCheckOnEmpty', []))
for fi in self.columnFieldInstances:
if idx is not None:
fi.index = idx
@ -133,10 +161,9 @@ class GridFieldInstance(ListFieldInstance):
else:
if fi.default is not None:
if value == fi.default:
continue
ignoreInCheckOnEmpty.append(fi.name)
if value:
item[fi.name] = value
ignoreInCheckOnEmpty = getattr(self.context, 'ignoreInCheckOnEmpty', [])
for k, v in item.items():
if k not in ignoreInCheckOnEmpty: #and v != '__no_change__':
return item
@ -215,7 +242,8 @@ class KeyTableFieldInstance(RecordsFieldInstance):
for k, v in value.items():
row = [k]
for idx, fi in enumerate(self.columnFieldInstances[1:]):
row.append(fi.display(v[idx]))
if idx < len(v):
row.append(fi.display(v[idx]))
rows.append(row)
return dict(headers=headers, rows=rows)
@ -226,7 +254,8 @@ class KeyTableFieldInstance(RecordsFieldInstance):
for k, v in value.items():
item = {self.keyName: k}
for idx, name in enumerate(self.dataNames):
item[name] = v[idx]
if idx < len(v):
item[name] = v[idx]
result.append(item)
return result

View file

@ -57,7 +57,7 @@
tal:attributes="class string:${column/baseField/cssClass|string:}">
<input type="text" style="width: auto"
tal:define="cname column/name"
tal:attributes="value row/?cname;
tal:attributes="value row/?cname|nothing;
name string:$name.$cname:records;
style string:width: ${column/baseField/width|string:auto};" /></td>
</tr>

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2010 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -19,8 +19,6 @@
"""
Basic classes for schemas, i.e. sets of fields that may be used for creating
editing forms or display views for objects.
$Id$
"""
from zope.interface import implements

View file

@ -40,6 +40,10 @@ It's possible to leave some of the questions unanswered.
>>> resp02 = Response(quest, 'john')
>>> resp02.values = {qu01: 2, qu03: 4}
Evaluation
==========
Now let's calculate the result for resp01.
>>> res = resp01.getResult()
@ -55,8 +59,8 @@ Now let's calculate the result for resp01.
fi03 4.0
fi01 2.4
Grouped Feedback Items
======================
Grouped feedback items
----------------------
>>> from cybertools.knowledge.survey.questionnaire import QuestionGroup
>>> qugroup = QuestionGroup(quest)
@ -65,12 +69,25 @@ Grouped Feedback Items
>>> qugroup.feedbackItems = [fi01, fi02, fi03]
>>> res = resp01.getGroupedResult()
>>> for qugroup, fi, score in res:
... print fi.text, round(score, 2)
fi02 0.58
>>> for r in res:
... print r['feedback'].text, round(r['score'], 2), r['rank']
fi02 0.58 1
>>> res = resp02.getGroupedResult()
>>> for qugroup, fi, score in res:
... print fi.text, round(score, 2)
fi03 0.75
>>> for r in res:
... print r['feedback'].text, round(r['score'], 2), r['rank']
fi03 0.75 1
Team evaluation
---------------
>>> resp03 = Response(quest, 'mary')
>>> resp03.values = {qu01: 1, qu02: 2, qu03: 4}
>>> resp01.values[qugroup] = resp01.getGroupedResult()[0]['score']
>>> resp03.values[qugroup] = resp03.getGroupedResult()[0]['score']
>>> teamData = resp01.getTeamResult([qugroup], [resp01, resp03])
>>> teamData
[{'average': 0.6666...}]

View file

@ -80,7 +80,8 @@ class IResponse(Interface):
questionnaire = Attribute('The questionnaire this response belongs to.')
party = Attribute('Some identification of the party that responded '
'to this questionnaire.')
values = Attribute('A mapping associating response values with questions.')
values = Attribute('A mapping associating numeric response values with questions.')
texts = Attribute('A mapping associating text response values with questions.')
def getResult():
""" Calculate the result for this response.

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2015 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -58,12 +58,8 @@ class Question(object):
self.feedbackItems = {}
self.text = text
self.revertAnswerOptions = False
def getAnswerRange(self):
return self._answerRange or self.questionnaire.defaultAnswerRange
def setAnswerRange(self, value):
self._answerRange = value
answerRange = property(getAnswerRange, setAnswerRange)
self.questionType = 'value_selection'
self.answerRange = None
class FeedbackItem(object):
@ -82,13 +78,18 @@ class Response(object):
self.questionnaire = questionnaire
self.party = party
self.values = {}
self.texts = {}
def getResult(self):
result = {}
for question, value in self.values.items():
if question.questionType != 'value_selection':
continue
for fi, rf in question.feedbackItems.items():
if question.revertAnswerOptions:
value = question.answerRange - value - 1
answerRange = (question.answerRange or
self.questionnaire.defaultAnswerRange)
value = answerRange - value - 1
result[fi] = result.get(fi, 0.0) + rf * value
return sorted(result.items(), key=lambda x: -x[1])
@ -97,15 +98,46 @@ class Response(object):
for qugroup in self.questionnaire.questionGroups:
score = scoreMax = 0.0
for qu in qugroup.questions:
value = self.values.get(qu)
if value is None:
if qu.questionType not in (None, 'value_selection'):
continue
value = self.values.get(qu)
if value is None or isinstance(value, basestring):
continue
answerRange = (qu.answerRange or
self.questionnaire.defaultAnswerRange)
if qu.revertAnswerOptions:
value = qu.answerRange - value - 1
value = answerRange - value - 1
score += value
scoreMax += qu.answerRange - 1
scoreMax += answerRange - 1
if scoreMax > 0.0:
relScore = score / scoreMax
wScore = relScore * len(qugroup.feedbackItems) - 0.00001
result.append((qugroup, qugroup.feedbackItems[int(wScore)], relScore))
if qugroup.feedbackItems:
feedback = qugroup.feedbackItems[int(wScore)]
else:
feedback = FeedbackItem()
result.append(dict(
group=qugroup,
feedback=feedback,
score=relScore))
ranks = getRanks([r['score'] for r in result])
for idx, r in enumerate(result):
r['rank'] = ranks[idx]
return result
def getTeamResult(self, groups, teamData):
result = []
for idx, group in enumerate(groups):
values = [data.values.get(group) for data in teamData]
values = [v for v in values if v is not None]
#avg = sum(values) / len(teamData)
avg = sum(values) / len(values)
result.append(dict(group=group, average=avg))
ranks = getRanks([r['average'] for r in result])
for idx, r in enumerate(result):
r['rank'] = ranks[idx]
return result
def getRanks(values):
ordered = list(reversed(sorted(values)))
return [ordered.index(v) + 1 for v in values]

View file

@ -85,6 +85,7 @@ class IPerson(Interface):
description=_(u'The date of birth - should be a '
'datetime.date object.'),
required=False,)
birthDate.hideTime = True
age = schema.Int(
title=_(u'Age'),

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: $Id$\n"
"POT-Creation-Date: 2008-12-01 12:00 CET\n"
"PO-Revision-Date: 2013-05-17 12:00 CET\n"
"PO-Revision-Date: 2014-05-12 12:00 CET\n"
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n"
@ -65,3 +65,33 @@ msgstr "Beginn"
msgid "End date"
msgstr "Ende"
msgid "Street, number"
msgstr "Straße, Hausnummer"
msgid "Street and number"
msgstr "Straße und Hausnummer"
msgid "ZIP code"
msgstr "Postleitzahl"
msgid "ZIP code, postal code"
msgstr "Postleitzahl"
msgid "City"
msgstr "Ort"
msgid "Name of the city"
msgstr "Name der Stadt/des Orts"
msgid "Country code"
msgstr "Ländercode"
msgid "International two-letter country code"
msgstr "Aus zwei Buchstaben bestehender Ländercode"
msgid "Additional lines"
msgstr "Zusätzliche Adresszeilen"
msgid "Additional address lines"
msgstr "Zusätzliche Adresszeilen"

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -42,7 +42,7 @@ def workItemStates():
return StatesDefinition('workItemStates',
State('new', 'new',
('plan', 'accept', 'start', 'work', 'finish', 'delegate',
'cancel', 'reopen'),
'cancel', 'reopen'), # 'move', # ?
color='red'),
State('planned', 'planned',
('plan', 'accept', 'start', 'work', 'finish', 'delegate',
@ -59,7 +59,7 @@ def workItemStates():
'move', 'cancel', 'modify'), color='lightgreen'),
State('finished', 'finished',
('plan', 'accept', 'start', 'work', 'finish',
'move', 'modify', 'close'),
'move', 'modify', 'close', 'cancel'),
color='green'),
State('cancelled', 'cancelled',
('plan', 'accept', 'start', 'work', 'move', 'modify', 'close'),
@ -79,8 +79,12 @@ def workItemStates():
State('replaced', 'replaced', (), color='grey'),
State('planned_x', 'planned', (), color='red'),
State('accepted_x', 'accepted', (), color='yellow'),
State('done_x', 'done', (), color='lightgreen'),
State('finished_x', 'finished', (), color='green'),
State('done_x', 'done',
('modify', 'move', 'cancel'), color='lightgreen'),
State('finished_x', 'finished',
('modify','move', 'cancel'), color='green'),
#State('done_y', 'done', (), color='grey'),
#State('finished_y', 'finished', (), color='grey'),
# transitions:
Transition('plan', 'plan', 'planned'),
Transition('accept', 'accept', 'accepted'),
@ -96,7 +100,8 @@ def workItemStates():
initialState='new')
fieldNames = ['title', 'description', 'deadline', 'start', 'end',
fieldNames = ['title', 'description', 'deadline', 'priority', 'activity',
'start', 'end',
'duration', 'effort',
'comment', 'party'] # for use in editingRules
@ -138,7 +143,8 @@ class WorkItemType(object):
self.title = title
self.description = description
self.actions = actions or list(editingRules)
self.fields = fields or ('deadline', 'start-end', 'duration-effort')
self.fields = fields or ('deadline', 'priority', 'activity',
'start-end', 'duration-effort')
self.indicator = indicator
self.delegatedState = delegatedState
self.prefillDate = prefillDate
@ -156,9 +162,10 @@ workItemTypes = Jeep((
fields =('deadline',),
indicator='work_deadline'),
WorkItemType('checkup', u'Check-up',
actions=('plan', 'accept', 'finish', 'cancel',
actions=('plan', 'accept', 'start', 'finish', 'cancel',
'modify', 'delegate', 'close', 'reopen'),
fields =('deadline', 'start-end',),
#fields =('deadline', 'start-end',),
fields =('deadline', 'daterange',),
indicator='work_checkup',
delegatedState='closed', prefillDate=False),
))
@ -177,7 +184,7 @@ class WorkItem(Stateful, Track):
statesDefinition = 'organize.workItemStates'
initAttributes = set(['workItemType', 'party', 'title', 'description',
'deadline', 'start', 'end',
'deadline', 'priority', 'activity', 'start', 'end',
'duration', 'effort'])
def __init__(self, taskId, runId, userName, data):
@ -225,13 +232,16 @@ class WorkItem(Stateful, Track):
return list(getParent(self).query(runId=self.runId))
def doAction(self, action, userName, **kw):
if self != self.currentWorkItems[-1]:
raise ValueError("Actions are only allowed on the last item of a run.")
#if self != self.currentWorkItems[-1]:
# raise ValueError("Actions are only allowed on the last item of a run.")
if action not in [t.name for t in self.getAvailableTransitions()]:
raise ValueError("Action '%s' not allowed in state '%s'" %
(action, self.state))
if action in self.specialActions:
return self.specialActions[action](self, userName, **kw)
return self.doStandardAction(action, userName, **kw)
def doStandardAction(self, action, userName, **kw):
if self.state == 'new':
self.setData(**kw)
self.doTransition(action)
@ -244,6 +254,9 @@ class WorkItem(Stateful, Track):
elif self.state in ('planned', 'accepted', 'done'):
self.state = self.state + '_x'
self.reindex('state')
elif self.state in ('finished',) and action == 'cancel':
self.state = self.state + '_x'
self.reindex('state')
new.doTransition(action)
new.reindex()
return new
@ -279,25 +292,57 @@ class WorkItem(Stateful, Track):
delegated.data['target'] = new.name
return new
def doStart(self, userName, **kw):
action = 'start'
# stop any running work item of user:
# TODO: check: party query OK?
if (userName == self.userName and
self.workItemType in (None, 'work') and
self.state != 'running'):
running = getParent(self).query(
party=userName, state='running')
for wi in running:
if wi.workItemType in 'work':
wi.doAction('work', userName,
end=(kw.get('start') or getTimeStamp()))
# standard creation of new work item:
if not kw.get('start'):
kw['start'] = getTimeStamp()
kw['end'] = None
kw['duration'] = kw['effort'] = 0
return self.doStandardAction(action, userName, **kw)
def move(self, userName, **kw):
xkw = dict(kw)
for k in ('deadline', 'start', 'end'):
xkw.pop(k, None) # do not change on source item
moved = self.createNew('move', userName, **xkw)
moved.userName = self.userName
moved.state = 'moved'
moved.reindex()
if self.state == 'new': # should this be possible?
moved = self
self.setData(kw)
if self.state in ('done', 'finished', 'running'):
moved = self # is this OK? or better new state ..._y?
else:
moved = self.createNew('move', userName, **xkw)
moved.userName = self.userName
task = kw.pop('task', None)
new = moved.createNew(None, userName, taskId=task, runId=0, **kw)
new.userName = self.userName
new.data['source'] = moved.name
new.state = self.state
if self.state == 'new':
new.state = 'planned'
else:
new.state = self.state
new.reindex()
moved.data['target'] = new.name
if self.state in ('planned', 'accepted', 'delegated', 'moved',
'done', 'finished'):
moved.state = 'moved'
moved.reindex()
if self.state in ('planned', 'accepted', 'delegated', 'moved'):
#'done', 'finished'):
self.state = self.state + '_x'
self.reindex('state')
#elif self.state in ('done', 'finished'):
# self.state = self.state + '_y'
# self.reindex('state')
return new
def close(self, userName, **kw):
@ -315,7 +360,8 @@ class WorkItem(Stateful, Track):
item.reindex('state')
return new
specialActions = dict(modify=modify, delegate=delegate, move=move,
specialActions = dict(modify=modify, delegate=delegate,
start=doStart, move=move,
close=close)
def setData(self, ignoreParty=False, **kw):
@ -328,7 +374,7 @@ class WorkItem(Stateful, Track):
self.reindex('userName')
start = kw.get('start') or kw.get('deadline') # TODO: check OK?
if start is not None:
self.timeStamp = start
self.timeStamp = start # TODO: better use end
self.reindex('timeStamp')
data = self.data
for k, v in kw.items():

View file

@ -100,7 +100,7 @@ but may also be specified explicitly.
>>> wi03 = wi02.doAction('start', 'jim', start=1229958000)
>>> wi03
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'running']:
{'duration': 700, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
{'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
Stopping and finishing work
---------------------------
@ -113,7 +113,7 @@ as "running" will be replaced by a new one.
>>> wi03
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'replaced']:
{'duration': 700, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
{'duration': 0, 'start': 1229958000, 'created': ..., 'creator': 'jim'}>
>>> wi04
<WorkItem ['001', 1, 'jim', '2008-12-22 16:00', 'done']:
{'start': 1229958000, 'created': ..., 'end': 1229958300, 'creator': 'jim'}>

View file

@ -52,7 +52,7 @@ class Stateful(object):
def getStateObject(self):
states = self.getStatesDefinition().states
if self.state not in states:
if self.getState() not in states:
self.state = self.getStatesDefinition().initialState
return states[self.state]

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
ZODB-/BTree-based implementation of user interaction tracking.
$Id$
"""
import time
@ -142,11 +140,12 @@ class TrackingStorage(BTreeContainer):
def setupIndexes(self):
changed = False
for idx in self.indexAttributes:
for idx in self.trackFactory.index_attributes:
if idx not in self.indexes:
self.indexes[idx] = FieldIndex()
changed = True
if changed:
self.indexAttributes = self.trackFactory.index_attributes
self.reindexTracks()
def idFromNum(self, num):
@ -237,6 +236,7 @@ class TrackingStorage(BTreeContainer):
self.unindexTrack(trackNum, track)
def indexTrack(self, trackNum, track, idx=None):
#self.setupIndexes()
if not trackNum:
trackNum = int(track.__name__)
data = track.indexdata

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
# Copyright (c) 2014 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Interface definitions for tracking of user interactions.
$Id$
"""
from zope.interface import Interface, Attribute
@ -50,6 +48,12 @@ class ITrack(Interface):
# """ Return the internal name (ID) of the track.
# """
def update(newData, overwrite=False):
""" Update the track with new data, by default creating a new
track. Overwrite the existing track if the corresponding
flag is set.
"""
class ITrackingStorage(Interface):
""" A utility for storing user tracks.

View file

@ -58,6 +58,7 @@ class ExternalEditorView(object):
r.append('cookie:' + cookie)
r.append('')
r.append(fromUnicode(data))
r = [str(item) for item in r]
result = '\n'.join(r)
self.setHeaders(len(result))
return result