Added TODO and DEVELOPMENT.
[faces-project.git] / faces / resource.py
blobfd212ddd1185da9599b562f6b69e561dd1e93441
1 #@+leo-ver=4
2 #@+node:@file resource.py
3 #@@language python
4 #@<< Copyright >>
5 #@+node:<< Copyright >>
6 ############################################################################
7 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
8 # mreithinger@web.de
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 >>
29 #@nl
30 #@<< Imports >>
31 #@+node:<< Imports >>
32 import pcalendar
33 import datetime
34 import utils
35 import string
36 import bisect
37 import plocale
38 #@-node:<< Imports >>
39 #@nl
41 _is_source = True
42 _to_datetime = pcalendar.to_datetime
43 _ = plocale.get_gettext()
45 #@+others
46 #@+node:_isattrib
47 #@+doc
48 #@nonl
49 # is used to find snapshot attributes
50 #@-doc
51 #@@code
52 def _isattrib(obj, a):
53 return a[0] != "_" \
54 and not callable(getattr(obj, a)) \
55 and not a.endswith("_members") \
56 and a not in ("name")
57 #@-node:_isattrib
58 #@+node:class ResourceCalendar
59 class ResourceCalendar(object):
60 """
61 The resource calendar saves the load time of a resource.
62 Is ia sequence of time intervals of loads. An example of
63 such a sequence is:
64 [ (datetime.min, 0),
65 (2006/1/1, 1.0),
66 (2006/1/10, 0.5),
67 (2006/1/15, 0) ]
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
74 """
76 #@ @+others
77 #@+node:__init__
78 def __init__(self, src=None):
79 if src:
80 self.bookings = list(src.bookings)
81 else:
82 self.bookings = [ (datetime.datetime.min, 0) ]
83 #@-node:__init__
84 #@+node:__str__
85 def __str__(self):
86 return str(self.bookings)
87 #@-node:__str__
88 #@+node:__repr__
89 def __repr__(self):
90 return "<ResourceCalendar %s>" % (str(self))
91 #@-node:__repr__
92 #@+node:add_load
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
100 # rouning problems
101 load = int(load * 10000)
103 start_item = (start, 0)
104 start_pos = bisect.bisect_left(bookings, start_item)
106 left_load = 0
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]
113 else:
114 bookings[start_pos] = (start, prev_load + load)
115 start_pos += 1
116 else:
117 bookings.insert(start_pos, (start, load + left_load))
118 start_pos += 1
120 item = (datetime.datetime.min, 0)
121 for i in range(start_pos, len(bookings)):
122 end_pos = i
123 item = bookings[i]
124 if item[0] >= end: break
125 bookings[i] = (item[0], item[1] + load)
126 else:
127 end_pos = len(bookings)
129 left_load = bookings[end_pos - 1][1]
130 if item[0] == end:
131 if item[1] == left_load:
132 del bookings[end_pos]
133 else:
134 bookings.insert(end_pos, (end, left_load - load))
135 #@-node:add_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
143 load = 0
145 try:
146 book_item = bookings[date_pos]
147 load = bookings[date_pos][1] / 10000.0
148 next_date = bookings[date_pos + 1][0]
149 except:
150 pass
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
160 else:
161 adjust_date = start.calendar.EndDate
163 start = _to_datetime(start)
164 load = int(load * 10000)
165 max_load = int(max_load * 10000)
166 lb = len(bookings)
168 def next_possible(index):
169 while index < lb:
170 sd, lo = bookings[index]
171 if lo + load <= max_load:
172 break
174 index += 1
176 sd = adjust_date(max(start, sd))
177 ed = sd + length
178 end = _to_datetime(ed)
180 index += 1
181 while index < lb:
182 date, lo = bookings[index]
184 if date >= end:
185 #I found a good start date
186 return None, sd
188 if lo + load > max_load:
189 return index + 1, None
191 index += 1
193 return None, sd
195 start_item = (start, 1000000)
196 i = bisect.bisect_left(bookings, start_item) - 1
198 next_start = None
199 while not next_start and i < lb:
200 i, next_start = next_possible(i)
202 assert(next_start is not None)
203 return next_start
204 #@-node:find_free_time
205 #@+node:get_bookings
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:
213 start_pos -= 1
215 end_item = (end, 0)
216 end_pos = bisect.bisect_left(bookings, end_item)
217 return start_pos, end_pos, bookings
218 #@-node:get_bookings
219 #@+node:get_load
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
226 #@-node:get_load
227 #@-others
228 #@-node:class ResourceCalendar
229 #@+node:class _ResourceBase
230 class _ResourceBase(object):
231 pass
233 #@-node:class _ResourceBase
234 #@+node:class _MetaResource
235 class _MetaResource(type):
236 doc_template = """
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)
241 @var max_load:
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.
247 @var title:
248 Specifies an alternative more descriptive name for the task.
250 @var efficiency:
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
256 your resources.
258 @var vacation:
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.
265 #@ @+others
266 #@+node:__init__
267 def __init__(self, name, bases, dict_):
268 super(_MetaResource, self).__init__(name, bases, dict_)
269 self.name = name
270 self.title = dict_.get("title", name)
271 self._calendar = { None: ResourceCalendar() }
272 self._tasks = { }
273 self.__set_vacation()
274 self.__add_resource(bases[0])
275 self.__doc__ = dict_.get("__doc__", self.doc_template) % locals()
276 #@-node:__init__
277 #@+node:__or__
278 def __or__(self, other):
279 return self().__or__(other)
280 #@-node:__or__
281 #@+node:__and__
282 def __and__(self, other):
283 return self().__and__(other)
284 #@-node:__and__
285 #@+node:__cmp__
286 def __cmp__(self, other):
287 return cmp(self.name, getattr(other, "name", None))
288 #@-node:__cmp__
289 #@+node:__repr__
290 def __repr__(self):
291 return "<Resource %s>" % self.name
292 #@-node:__repr__
293 #@+node:__str__
294 def __str__(self):
295 return repr(self)
296 #@-node:__str__
297 #@+node:__set_vacation
298 def __set_vacation(self):
299 vacation = self.vacation
301 if isinstance(vacation, (tuple, list)):
302 for v in vacation:
303 if isinstance(v, (tuple, list)):
304 self.add_vacation(v[0], v[1])
305 else:
306 self.add_vacation(v)
307 else:
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", [])
314 members.append(self)
315 setattr(base, base.__name__ + "_members", members)
316 #@-node:__add_resource
317 #@+node:get_members
318 def get_members(self):
319 return getattr(self, self.__name__ + "_members", [])
320 #@-node:get_members
321 #@+node:add_vacation
322 def add_vacation(self, start, end=None):
323 start_date = _to_datetime(start)
325 if not end:
326 end_date = start_date.replace(hour=23, minute=59)
327 else:
328 end_date = _to_datetime(end)
330 for cal in self._calendar.itervalues():
331 cal.add_load(start_date, end_date, 1)
333 tp = Booking()
334 tp.start = start_date
335 tp.end = end_date
336 tp.book_start = start_date
337 tp.book_end = end_date
338 tp.work_time = end_date - start_date
339 tp.load = 1.0
340 tp.name = tp.title = _("(vacation)")
341 tp._id = ""
342 self._tasks.setdefault("", []).append(tp)
343 #@-node:add_vacation
344 #@+node:calendar
345 def calendar(self, scenario):
346 try:
347 return self._calendar[scenario]
348 except KeyError:
349 cal = self._calendar[scenario] = ResourceCalendar(self._calendar[None])
350 return cal
352 #@-node:calendar
353 #@-others
354 #@-node:class _MetaResource
355 #@+node:make_team
356 def make_team(resource):
357 members = resource.get_members()
358 if not members:
359 return resource
361 result = make_team(members[0])
362 for r in members[1:]:
363 result = result & make_team(r)
365 return result
366 #@-node:make_team
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
376 actual = False
377 _id = ""
379 #@-node:<< declarations >>
380 #@nl
381 #@ @+others
382 #@+node:__init__
383 def __init__(self, task=None):
384 self.__task = task
385 #@-node:__init__
386 #@+node:__cmp__
387 def __cmp__(self, other):
388 return cmp(self._id, other._id)
389 #@-node:__cmp__
390 #@+node:path
391 def path(self):
392 first_dot = self._id.find(".")
393 return "root" + self._id[first_dot:]
395 path = property(path)
396 #@nonl
397 #@-node:path
398 #@+node:_idendity_
399 def _idendity_(self):
400 return self._id
401 #@-node:_idendity_
402 #@+node:__getattr__
403 def __getattr__(self, name):
404 if self.__task:
405 return getattr(self.__task, name)
407 raise AttributeError("'%s' is not a valid attribute" % (name))
408 #@-node:__getattr__
409 #@-others
410 #@-node:class Booking
411 #@+node:class ResourceList
412 class ResourceList(list):
413 #@ @+others
414 #@+node:__init__
415 def __init__(self, *args):
416 if args: self.extend(args)
417 #@-node:__init__
418 #@-others
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
434 vacation = ()
435 efficiency = 1.0
438 #@-node:<< declarations >>
439 #@nl
440 #@ @+others
441 #@+node:__init__
442 def __init__(self, **kwargs):
443 for k, v in kwargs.iteritems():
444 setattr(self, k, v)
445 #@-node:__init__
446 #@+node:_idendity_
447 def _idendity_(cls):
448 return "resource:" + cls.__name__
450 _idendity_ = classmethod(_idendity_)
451 #@-node:_idendity_
452 #@+node:__repr__
453 def __repr__(self):
454 return "<Resource %s>" % self.__class__.__name__
455 #@-node:__repr__
456 #@+node:__str__
457 def __str__(self):
458 return repr(self)
459 #@-node:__str__
460 #@+node:__call__
461 def __call__(self):
462 return self
463 #@-node:__call__
464 #@+node:__hash__
465 def __hash__(self):
466 return hash(self.__class__)
467 #@-node:__hash__
468 #@+node:__cmp__
469 def __cmp__(self, other):
470 return cmp(self.name, other.name)
471 #@-node:__cmp__
472 #@+node:__or__
473 def __or__(self, other):
474 if type(other) is _MetaResource:
475 other = other()
477 result = Resource()
478 result._subresource = _OrResourceGroup(self, other)
479 return result
480 #@-node:__or__
481 #@+node:__and__
482 def __and__(self, other):
483 if type(other) is _MetaResource:
484 other = other()
486 result = Resource()
487 result._subresource = _AndResourceGroup(self, other)
488 return result
489 #@-node:__and__
490 #@+node:_permutation_count
491 def _permutation_count(self):
492 if hasattr(self, "_subresource"):
493 return self._subresource._permutation_count()
495 return 1
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
508 return result
510 result = ResourceList(self)
511 return result
512 #@-node:_get_resources
513 #@+node:all_members
514 def all_members(self):
515 if hasattr(self, "_subresource"):
516 return self._subresource.all_members()
518 return [ self.__class__ ]
519 #@-node:all_members
520 #@+node:unbook_tasks_of_project
521 def unbook_tasks_of_project(cls, project_id, scenario):
522 try:
523 task_list = cls._tasks[scenario]
524 except KeyError:
525 return
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]
535 if not task_list:
536 del cls._tasks[scenario]
538 unbook_tasks_of_project = classmethod(unbook_tasks_of_project)
539 #@-node:unbook_tasks_of_project
540 #@+node:unbook_task
541 def unbook_task(cls, task):
542 identdity = task._idendity_()
543 scenario = task.scenario
545 try:
546 task_list = cls._tasks[scenario]
547 bookings = task_list[identdity]
548 except KeyError:
549 return
551 add_load = cls.calendar(scenario).add_load
552 for b in bookings:
553 add_load(b.book_start, b.book_end, -b.load)
555 del task_list[identdity]
556 if not task_list:
557 del cls._tasks[scenario]
559 unbook_task = classmethod(unbook_task)
560 #@-node:unbook_task
561 #@+node:correct_bookings
562 def correct_bookings(cls, task):
563 #correct the booking data with the actual task data
564 try:
565 tasks = cls._tasks[task.scenario][task._idendity_()]
566 except KeyError:
567 return
569 for t in tasks:
570 t.start = task.start.to_datetime()
571 t.end = task.end.to_datetime()
573 correct_bookings = classmethod(correct_bookings)
574 #@-node:correct_bookings
575 #@+node:book_task
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
587 tb = Booking(task)
588 tb.book_start = start
589 tb.book_end = end
590 tb._id = identdity
591 tb.load = load
592 tb.start = _to_datetime(task.start)
593 tb.end = _to_datetime(task.end)
594 tb.title = task.title
595 tb.name = task.name
596 tb.work_time = int(work_time)
597 tb.actual = actual
598 bookings.append(tb)
599 result = add_load(start, end, load)
600 return result
602 book_task = classmethod(book_task)
603 #@-node:book_task
604 #@+node:length_of
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)
611 #@-node:length_of
612 #@+node:done_of
613 def done_of(self, task):
614 cal = task.root.calendar
615 now = cal.now
616 bookings = self.get_bookings(task)
618 if task.__dict__.has_key("effort"):
619 efficiency = self.efficiency * task.efficiency
620 else:
621 efficiency = 1
623 def book_done(booking):
624 if booking.book_start >= now:
625 return 0
627 factor = 1
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)))
637 #@-node:done_of
638 #@+node:todo_of
639 def todo_of(self, task):
640 cal = task.root.calendar
641 now = cal.now
643 bookings = self.get_bookings(task)
644 if task.__dict__.has_key("effort"):
645 efficiency = self.efficiency * task.efficiency
646 else:
647 efficiency = 1
649 def book_todo(booking):
650 if booking.book_end <= now:
651 return 0
653 factor = 1
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)))
663 #@-node:todo_of
664 #@+node:get_bookings
665 def get_bookings(cls, task):
666 return cls._tasks.get(task.scenario, {}).get(task._idendity_(), ())
668 get_bookings = classmethod(get_bookings)
669 #@-node:get_bookings
670 #@+node:get_bookings_at
671 def get_bookings_at(cls, start, end, scenario):
672 result = []
674 try:
675 items = cls._tasks[scenario].iteritems()
676 except KeyError:
677 return ()
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 ]
689 return result
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
699 #@+node:get_load
700 def get_load(cls, date, scenario):
701 return cls.calendar(scenario).get_load(date)
703 get_load = classmethod(get_load)
704 #@-node: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
711 #@+node:snapshot
712 def snapshot(self):
713 from task import _as_string
714 def isattrib(a):
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))),
721 attribs)
723 return self.name + "(%s)" % ", ".join(attribs)
724 #@-node:snapshot
725 #@-others
726 #@-node:class Resource
727 #@+node:class _ResourceGroup
730 class _ResourceGroup(object):
731 #@ @+others
732 #@+node:__init__
733 def __init__(self, *args):
734 self.resources = []
735 for a in args:
736 self.__append(a)
737 #@-node:__init__
738 #@+node:all_members
739 def all_members(self):
740 group = reduce(lambda a, b: a + b.all_members(),
741 self.resources, [])
742 group = map(lambda r: (r, True), group)
743 group = dict(group)
744 group = group.keys()
745 return group
746 #@-node:all_members
747 #@+node:_permutation_count
748 def _permutation_count(self):
749 abstract
750 #@-node:_permutation_count
751 #@+node:_refactor
752 def _refactor(self, arg):
753 pass
754 #@-node:_refactor
755 #@+node:__append
756 def __append(self, arg):
757 if isinstance(arg, self.__class__):
758 self.resources += arg.resources
759 for r in arg.resources:
760 self._refactor(r)
761 return
762 elif isinstance(arg, Resource):
763 subresources = getattr(arg, "_subresource", None)
764 if subresources:
765 self.__append(subresources)
766 return
767 else:
768 self.resources.append(arg)
769 else:
770 assert(isinstance(arg, _ResourceGroup))
771 self.resources.append(arg)
773 self._refactor(arg)
774 #@-node:__append
775 #@+node:__str__
776 def __str__(self):
777 op = lower(self.__class__.__name__[0:-13])
778 return "(" + \
779 string.join([str(r) for r in self.resources],
780 " " + op + " ") + \
782 #@-node:__str__
783 #@-others
784 #@-node:class _ResourceGroup
785 #@+node:class _OrResourceGroup
788 class _OrResourceGroup(_ResourceGroup):
789 #@ @+others
790 #@+node:_get_resources
791 def _get_resources(self, state):
792 for r in self.resources:
793 c = r._permutation_count()
794 if c <= state:
795 state -= c
796 else:
797 return r._get_resources(state)
799 assert(0)
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
805 #@-others
806 #@-node:class _OrResourceGroup
807 #@+node:class _AndResourceGroup
810 class _AndResourceGroup(_ResourceGroup):
811 #@ @+others
812 #@+node:__init__
813 def __init__(self, *args):
814 self.factors = [ 1 ]
815 _ResourceGroup.__init__(self, *args)
816 #@-node:__init__
817 #@+node:_refactor
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)
822 #@-node:_refactor
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"""
833 result = []
834 for i in range(1, len(self.factors)):
835 f = self.factors[i]
836 substate = state / f
837 state %= f
838 result.append(self.resources[i - 1]._get_resources(substate))
840 result = ResourceList(*list(utils.flatten(result)))
841 dupl_test = { }
842 for r in result:
843 if dupl_test.has_key(r):
844 return None
845 else:
846 dupl_test[r] = 1
848 return result
849 #@-node:_get_resources
850 #@+node:_has_duplicates
851 def _has_duplicates(self, state):
852 resources = self._get_resources(state)
853 tmp = { }
854 for r in resources:
855 if tmp.has_key(r):
856 return True
858 tmp[r] = 1
860 return False
861 #@-node:_has_duplicates
862 #@-others
863 #@-node:class _AndResourceGroup
864 #@-others
865 #@-node:@file resource.py
866 #@-leo