2 #@+node:@file resource.py
5 #@+node:<< Copyright >>
6 ############################################################################
7 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
10 # This file is part of faces.
12 # faces is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # faces is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the
24 # Free Software Foundation, Inc.,
25 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 ############################################################################
28 #@-node:<< Copyright >>
42 _to_datetime
= pcalendar
.to_datetime
43 _
= plocale
.get_gettext()
49 # is used to find snapshot attributes
52 def _isattrib(obj
, a
):
54 and not callable(getattr(obj
, a
)) \
55 and not a
.endswith("_members") \
58 #@+node:class ResourceCalendar
59 class ResourceCalendar(object):
61 The resource calendar saves the load time of a resource.
62 Is ia sequence of time intervals of loads. An example of
69 That means the resource:
70 is free till january the first 2006
71 is fully booked from january the first to january 10th
72 is half booked from january 10th to january 15th
73 is free since january 15th
78 def __init__(self
, src
=None):
80 self
.bookings
= list(src
.bookings
)
82 self
.bookings
= [ (datetime
.datetime
.min, 0) ]
86 return str(self
.bookings
)
90 return "<ResourceCalendar %s>" % (str(self
))
93 def add_load(self
, start
, end
, load
):
94 start
= _to_datetime(start
)
95 end
= _to_datetime(end
)
97 bookings
= self
.bookings
99 # the load will be converted in an integer to avoid
101 load
= int(load
* 10000)
103 start_item
= (start
, 0)
104 start_pos
= bisect
.bisect_left(bookings
, start_item
)
107 left_load
= bookings
[start_pos
- 1][1]
109 if start_pos
< len(bookings
) and bookings
[start_pos
][0] == start
:
110 prev_load
= bookings
[start_pos
][1]
111 if prev_load
+ load
== left_load
:
112 del bookings
[start_pos
]
114 bookings
[start_pos
] = (start
, prev_load
+ load
)
117 bookings
.insert(start_pos
, (start
, load
+ left_load
))
120 item
= (datetime
.datetime
.min, 0)
121 for i
in range(start_pos
, len(bookings
)):
124 if item
[0] >= end
: break
125 bookings
[i
] = (item
[0], item
[1] + load
)
127 end_pos
= len(bookings
)
129 left_load
= bookings
[end_pos
- 1][1]
131 if item
[1] == left_load
:
132 del bookings
[end_pos
]
134 bookings
.insert(end_pos
, (end
, left_load
- load
))
136 #@+node:end_of_booking_interval
137 def end_of_booking_interval(self
, date
):
138 date
= _to_datetime(date
)
139 bookings
= self
.bookings
140 date_item
= (date
, 999999)
141 date_pos
= bisect
.bisect_left(bookings
, date_item
) - 1
142 next_date
= datetime
.datetime
.max
146 book_item
= bookings
[date_pos
]
147 load
= bookings
[date_pos
][1] / 10000.0
148 next_date
= bookings
[date_pos
+ 1][0]
152 return next_date
, load
153 #@-node:end_of_booking_interval
154 #@+node:find_free_time
155 def find_free_time(self
, start
, length
, load
, max_load
):
156 bookings
= self
.bookings
158 if isinstance(start
, datetime
.datetime
):
159 adjust_date
= _to_datetime
161 adjust_date
= start
.calendar
.EndDate
163 start
= _to_datetime(start
)
164 load
= int(load
* 10000)
165 max_load
= int(max_load
* 10000)
168 def next_possible(index
):
170 sd
, lo
= bookings
[index
]
171 if lo
+ load
<= max_load
:
176 sd
= adjust_date(max(start
, sd
))
178 end
= _to_datetime(ed
)
182 date
, lo
= bookings
[index
]
185 #I found a good start date
188 if lo
+ load
> max_load
:
189 return index
+ 1, None
195 start_item
= (start
, 1000000)
196 i
= bisect
.bisect_left(bookings
, start_item
) - 1
199 while not next_start
and i
< lb
:
200 i
, next_start
= next_possible(i
)
202 assert(next_start
is not None)
204 #@-node:find_free_time
206 def get_bookings(self
, start
, end
):
207 start
= _to_datetime(start
)
208 end
= _to_datetime(end
)
209 bookings
= self
.bookings
210 start_item
= (start
, 0)
211 start_pos
= bisect
.bisect_left(bookings
, start_item
)
212 if start_pos
>= len(bookings
) or bookings
[start_pos
][0] > start
:
216 end_pos
= bisect
.bisect_left(bookings
, end_item
)
217 return start_pos
, end_pos
, bookings
220 def get_load(self
, date
):
221 date
= _to_datetime(date
)
222 bookings
= self
.bookings
223 item
= (date
, 100000)
224 pos
= bisect
.bisect_left(bookings
, item
) - 1
225 return bookings
[pos
][1] / 10000.0
228 #@-node:class ResourceCalendar
229 #@+node:class _ResourceBase
230 class _ResourceBase(object):
233 #@-node:class _ResourceBase
234 #@+node:class _MetaResource
235 class _MetaResource(type):
237 A resource class. The resources default attributes can
238 be changed when the class ist instanciated, i.e.
239 %(name)s(max_load=2.0)
242 Specify the maximal allowed load sum of all simultaneously
243 allocated tasks of a resource. A ME{max_load} of 1.0 (default)
244 means the resource may be fully allocated. A ME{max_load} of 1.3
245 means the resource may be allocated with 30%% overtime.
248 Specifies an alternative more descriptive name for the task.
251 The efficiency of a resource can be used for two purposes. First
252 you can use it as a crude way to model a team. A team of 5 people
253 should have an efficiency of 5.0. Keep in mind that you cannot
254 track the member of the team individually if you use this
255 feature. The other use is to model performance variations between
259 Specifies the vacation of the resource. This attribute is
260 specified as a list of date literals or date literal intervals.
261 Be aware that the end of an interval is excluded, i.e. it is
262 the first working date.
267 def __init__(self
, name
, bases
, dict_
):
268 super(_MetaResource
, self
).__init
__(name
, bases
, dict_
)
270 self
.title
= dict_
.get("title", name
)
271 self
._calendar
= { None: ResourceCalendar() }
273 self
.__set
_vacation
()
274 self
.__add
_resource
(bases
[0])
275 self
.__doc
__ = dict_
.get("__doc__", self
.doc_template
) % locals()
278 def __or__(self
, other
):
279 return self().__or
__(other
)
282 def __and__(self
, other
):
283 return self().__and
__(other
)
286 def __cmp__(self
, other
):
287 return cmp(self
.name
, getattr(other
, "name", None))
291 return "<Resource %s>" % self
.name
297 #@+node:__set_vacation
298 def __set_vacation(self
):
299 vacation
= self
.vacation
301 if isinstance(vacation
, (tuple, list)):
303 if isinstance(v
, (tuple, list)):
304 self
.add_vacation(v
[0], v
[1])
308 self
.add_vacation(vacation
)
309 #@-node:__set_vacation
310 #@+node:__add_resource
311 def __add_resource(self
, base
):
312 if issubclass(base
, _ResourceBase
):
313 members
= getattr(base
, base
.__name
__ + "_members", [])
315 setattr(base
, base
.__name
__ + "_members", members
)
316 #@-node:__add_resource
318 def get_members(self
):
319 return getattr(self
, self
.__name
__ + "_members", [])
322 def add_vacation(self
, start
, end
=None):
323 start_date
= _to_datetime(start
)
326 end_date
= start_date
.replace(hour
=23, minute
=59)
328 end_date
= _to_datetime(end
)
330 for cal
in self
._calendar
.itervalues():
331 cal
.add_load(start_date
, end_date
, 1)
334 tp
.start
= start_date
336 tp
.book_start
= start_date
337 tp
.book_end
= end_date
338 tp
.work_time
= end_date
- start_date
340 tp
.name
= tp
.title
= _("(vacation)")
342 self
._tasks
.setdefault("", []).append(tp
)
345 def calendar(self
, scenario
):
347 return self
._calendar
[scenario
]
349 cal
= self
._calendar
[scenario
] = ResourceCalendar(self
._calendar
[None])
354 #@-node:class _MetaResource
356 def make_team(resource
):
357 members
= resource
.get_members()
361 result
= make_team(members
[0])
362 for r
in members
[1:]:
363 result
= result
& make_team(r
)
367 #@+node:class Booking
368 class Booking(object):
370 A booking unit for a task.
372 #@ << declarations >>
373 #@+node:<< declarations >>
374 book_start
= datetime
.datetime
.min
375 book_end
= datetime
.datetime
.max
379 #@-node:<< declarations >>
383 def __init__(self
, task
=None):
387 def __cmp__(self
, other
):
388 return cmp(self
._id
, other
._id
)
392 first_dot
= self
._id
.find(".")
393 return "root" + self
._id
[first_dot
:]
395 path
= property(path
)
399 def _idendity_(self
):
403 def __getattr__(self
, name
):
405 return getattr(self
.__task
, name
)
407 raise AttributeError("'%s' is not a valid attribute" % (name
))
410 #@-node:class Booking
411 #@+node:class ResourceList
412 class ResourceList(list):
415 def __init__(self
, *args
):
416 if args
: self
.extend(args
)
419 #@-node:class ResourceList
420 #@+node:class Resource
421 class Resource(_ResourceBase
):
422 #@ << declarations >>
423 #@+node:<< declarations >>
424 __metaclass__
= _MetaResource
425 __attrib_completions__
= {\
426 "max_load": 'max_load = ',
427 "title": 'title = "|"',
428 "efficiency": 'efficiency = ',
429 "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]' }
431 __type_image__
= "resource16"
433 max_load
= None # the maximum sum load for all task
438 #@-node:<< declarations >>
442 def __init__(self
, **kwargs
):
443 for k
, v
in kwargs
.iteritems():
448 return "resource:" + cls
.__name
__
450 _idendity_
= classmethod(_idendity_
)
454 return "<Resource %s>" % self
.__class
__.__name
__
466 return hash(self
.__class
__)
469 def __cmp__(self
, other
):
470 return cmp(self
.name
, other
.name
)
473 def __or__(self
, other
):
474 if type(other
) is _MetaResource
:
478 result
._subresource
= _OrResourceGroup(self
, other
)
482 def __and__(self
, other
):
483 if type(other
) is _MetaResource
:
487 result
._subresource
= _AndResourceGroup(self
, other
)
490 #@+node:_permutation_count
491 def _permutation_count(self
):
492 if hasattr(self
, "_subresource"):
493 return self
._subresource
._permutation
_count
()
496 #@-node:_permutation_count
497 #@+node:_get_resources
498 def _get_resources(self
, state
):
499 if hasattr(self
, "_subresource"):
500 result
= self
._subresource
._get
_resources
(state
)
502 if self
.name
!= "Resource":
503 result
.name
= self
.name
505 if self
.title
!= "Resource":
506 result
.title
= self
.title
510 result
= ResourceList(self
)
512 #@-node:_get_resources
514 def all_members(self
):
515 if hasattr(self
, "_subresource"):
516 return self
._subresource
.all_members()
518 return [ self
.__class
__ ]
520 #@+node:unbook_tasks_of_project
521 def unbook_tasks_of_project(cls
, project_id
, scenario
):
523 task_list
= cls
._tasks
[scenario
]
527 add_load
= cls
.calendar(scenario
).add_load
528 for task_id
, bookings
in task_list
.items():
529 if task_id
.startswith(project_id
):
530 for item
in bookings
:
531 add_load(item
.book_start
, item
.book_end
, -item
.load
)
533 del task_list
[task_id
]
536 del cls
._tasks
[scenario
]
538 unbook_tasks_of_project
= classmethod(unbook_tasks_of_project
)
539 #@-node:unbook_tasks_of_project
541 def unbook_task(cls
, task
):
542 identdity
= task
._idendity
_()
543 scenario
= task
.scenario
546 task_list
= cls
._tasks
[scenario
]
547 bookings
= task_list
[identdity
]
551 add_load
= cls
.calendar(scenario
).add_load
553 add_load(b
.book_start
, b
.book_end
, -b
.load
)
555 del task_list
[identdity
]
557 del cls
._tasks
[scenario
]
559 unbook_task
= classmethod(unbook_task
)
561 #@+node:correct_bookings
562 def correct_bookings(cls
, task
):
563 #correct the booking data with the actual task data
565 tasks
= cls
._tasks
[task
.scenario
][task
._idendity
_()]
570 t
.start
= task
.start
.to_datetime()
571 t
.end
= task
.end
.to_datetime()
573 correct_bookings
= classmethod(correct_bookings
)
574 #@-node:correct_bookings
576 def book_task(cls
, task
, start
, end
, load
, work_time
, actual
):
577 if not work_time
: return
579 start
= _to_datetime(start
)
580 end
= _to_datetime(end
)
582 identdity
= task
._idendity
_()
583 task_list
= cls
._tasks
.setdefault(task
.scenario
, {})
584 bookings
= task_list
.setdefault(identdity
, [])
585 add_load
= cls
.calendar(task
.scenario
).add_load
588 tb
.book_start
= start
592 tb
.start
= _to_datetime(task
.start
)
593 tb
.end
= _to_datetime(task
.end
)
594 tb
.title
= task
.title
596 tb
.work_time
= int(work_time
)
599 result
= add_load(start
, end
, load
)
602 book_task
= classmethod(book_task
)
605 def length_of(cls
, task
):
606 cal
= task
.root
.calendar
607 bookings
= cls
.get_bookings(task
)
608 return sum(map(lambda b
: task
._to
_delta
(b
.work_time
).round(), bookings
))
610 length_of
= classmethod(length_of
)
613 def done_of(self
, task
):
614 cal
= task
.root
.calendar
616 bookings
= self
.get_bookings(task
)
618 if task
.__dict
__.has_key("effort"):
619 efficiency
= self
.efficiency
* task
.efficiency
623 def book_done(booking
):
624 if booking
.book_start
>= now
:
628 if booking
.book_end
> now
:
629 start
= task
._to
_start
(booking
.book_start
)
630 end
= task
._to
_end
(booking
.book_end
)
631 cnow
= task
._to
_start
(now
)
632 factor
= float(cnow
- start
) / ((end
- start
) or 1)
634 return factor
* booking
.work_time
* efficiency
636 return task
._to
_delta
(sum(map(book_done
, bookings
)))
639 def todo_of(self
, task
):
640 cal
= task
.root
.calendar
643 bookings
= self
.get_bookings(task
)
644 if task
.__dict
__.has_key("effort"):
645 efficiency
= self
.efficiency
* task
.efficiency
649 def book_todo(booking
):
650 if booking
.book_end
<= now
:
654 if booking
.book_start
< now
:
655 start
= task
._to
_start
(booking
.book_start
)
656 end
= task
._to
_end
(booking
.book_end
)
657 cnow
= task
._to
_start
(now
)
658 factor
= float(end
- cnow
) / ((end
- start
) or 1)
660 return factor
* booking
.work_time
* efficiency
662 return task
._to
_delta
(sum(map(book_todo
, bookings
)))
665 def get_bookings(cls
, task
):
666 return cls
._tasks
.get(task
.scenario
, {}).get(task
._idendity
_(), ())
668 get_bookings
= classmethod(get_bookings
)
670 #@+node:get_bookings_at
671 def get_bookings_at(cls
, start
, end
, scenario
):
675 items
= cls
._tasks
[scenario
].iteritems()
679 for task_id
, bookings
in items
:
680 result
+= [ booking
for booking
in bookings
681 if booking
.book_start
< end
682 and booking
.book_end
> start
]
684 vacations
= cls
._tasks
.get("", ())
685 result
+= [ booking
for booking
in vacations
686 if booking
.book_start
< end
687 and booking
.book_end
> start
]
691 get_bookings_at
= classmethod(get_bookings_at
)
692 #@-node:get_bookings_at
693 #@+node:find_free_time
694 def find_free_time(cls
, start
, length
, load
, max_load
, scenario
):
695 return cls
.calendar(scenario
).find_free_time(start
, length
, load
, max_load
)
697 find_free_time
= classmethod(find_free_time
)
698 #@-node:find_free_time
700 def get_load(cls
, date
, scenario
):
701 return cls
.calendar(scenario
).get_load(date
)
703 get_load
= classmethod(get_load
)
705 #@+node:end_of_booking_interval
706 def end_of_booking_interval(cls
, date
, task
):
707 return cls
.calendar(task
.scenario
).end_of_booking_interval(date
)
709 end_of_booking_interval
= classmethod(end_of_booking_interval
)
710 #@-node:end_of_booking_interval
713 from task
import _as_string
715 if a
== "max_load" and self
.max_load
is None: return False
716 if a
in ("name", "title", "vacation"): return False
717 return _isattrib(self
, a
)
719 attribs
= filter(isattrib
, dir(self
))
720 attribs
= map(lambda a
: "%s=%s" % (a
, _as_string(getattr(self
, a
))),
723 return self
.name
+ "(%s)" % ", ".join(attribs
)
726 #@-node:class Resource
727 #@+node:class _ResourceGroup
730 class _ResourceGroup(object):
733 def __init__(self
, *args
):
739 def all_members(self
):
740 group
= reduce(lambda a
, b
: a
+ b
.all_members(),
742 group
= map(lambda r
: (r
, True), group
)
747 #@+node:_permutation_count
748 def _permutation_count(self
):
750 #@-node:_permutation_count
752 def _refactor(self
, arg
):
756 def __append(self
, arg
):
757 if isinstance(arg
, self
.__class
__):
758 self
.resources
+= arg
.resources
759 for r
in arg
.resources
:
762 elif isinstance(arg
, Resource
):
763 subresources
= getattr(arg
, "_subresource", None)
765 self
.__append
(subresources
)
768 self
.resources
.append(arg
)
770 assert(isinstance(arg
, _ResourceGroup
))
771 self
.resources
.append(arg
)
777 op
= lower(self
.__class
__.__name
__[0:-13])
779 string
.join([str(r
) for r
in self
.resources
],
784 #@-node:class _ResourceGroup
785 #@+node:class _OrResourceGroup
788 class _OrResourceGroup(_ResourceGroup
):
790 #@+node:_get_resources
791 def _get_resources(self
, state
):
792 for r
in self
.resources
:
793 c
= r
._permutation
_count
()
797 return r
._get
_resources
(state
)
800 #@-node:_get_resources
801 #@+node:_permutation_count
802 def _permutation_count(self
):
803 return sum([ r
._permutation
_count
() for r
in self
.resources
])
804 #@-node:_permutation_count
806 #@-node:class _OrResourceGroup
807 #@+node:class _AndResourceGroup
810 class _AndResourceGroup(_ResourceGroup
):
813 def __init__(self
, *args
):
815 _ResourceGroup
.__init
__(self
, *args
)
818 def _refactor(self
, arg
):
819 count
= arg
._permutation
_count
()
820 self
.factors
= [ count
* f
for f
in self
.factors
]
821 self
.factors
.append(1)
823 #@+node:_permutation_count
824 #print "AndResourceGroup", count, arg, self.factors
827 def _permutation_count(self
):
828 return self
.factors
[0]
829 #@-node:_permutation_count
830 #@+node:_get_resources
831 def _get_resources(self
, state
):
832 """delivers None when there are duplicate resources"""
834 for i
in range(1, len(self
.factors
)):
838 result
.append(self
.resources
[i
- 1]._get
_resources
(substate
))
840 result
= ResourceList(*list(utils
.flatten(result
)))
843 if dupl_test
.has_key(r
):
849 #@-node:_get_resources
850 #@+node:_has_duplicates
851 def _has_duplicates(self
, state
):
852 resources
= self
._get
_resources
(state
)
861 #@-node:_has_duplicates
863 #@-node:class _AndResourceGroup
865 #@-node:@file resource.py