trac-jsgantt-0.9+r11145/0000755000175000017500000000000011704574102012751 5ustar wmbwmbtrac-jsgantt-0.9+r11145/0.11/0000755000175000017500000000000011704574102013330 5ustar wmbwmbtrac-jsgantt-0.9+r11145/0.11/.gitignore0000644000175000017500000000005311677430656015334 0ustar wmbwmb*~ build dist Trac_jsGantt.egg-info *.pyc trac-jsgantt-0.9+r11145/0.11/setup.cfg0000644000175000017500000000006011607677530015160 0ustar wmbwmb[egg_info] tag_build = tag_svn_revision = true trac-jsgantt-0.9+r11145/0.11/setup.py0000644000175000017500000000106211607677600015052 0ustar wmbwmb#!/usr/bin/env python # -*- coding: iso-8859-1 -*- from setuptools import setup setup( name = 'Trac-jsGantt', author = 'Chris Nelson', author_email = 'Chris.Nelson@SIXNET.com', description = 'Trac plugin displaying jsGantt charts in Trac', version = '0.9', url = 'http://trac-hacks.org/wiki/TracJsGanttPlugin', license='BSD', packages=['tracjsgantt'], package_data = { 'tracjsgantt': ['htdocs/*.js', 'htdocs/*.css'] }, entry_points = { 'trac.plugins': [ 'tracjsgantt = tracjsgantt' ] } ) trac-jsgantt-0.9+r11145/0.11/tracjsgantt/0000755000175000017500000000000011704574102015654 5ustar wmbwmbtrac-jsgantt-0.9+r11145/0.11/tracjsgantt/tracpm.py0000644000175000017500000014115411704441624017524 0ustar wmbwmbimport re import time import math import copy from datetime import timedelta, datetime from trac.util.datefmt import format_date, utc from trac.ticket.query import Query from trac.config import IntOption, Option, ExtensionOption from trac.core import implements, Component, TracError, Interface class IResourceCalendar(Interface): # Return the number of hours available for the resource on the # specified date. # FIXME - should this be pm_hoursAvailable or something so other # plugins can implement it without potential conflict? def hoursAvailable(self, date, resource = None): """Called to see how many hours are available on date""" class ITaskScheduler(Interface): # Schedule each the ticket in tickets with consideration for # dependencies, estimated work, hours per day, etc. # # ticketsByID is a dictionary, indexed by numeric ticket ID, each # ticket contains at least the fields returned by queryFields() # and the whole list was processed by postQuery(). # # On exit, each ticket has t['calc_start'] and t['calc_finish'] # set (FIXME - we should probably be able to configure those field # names.) and can be accessed with TracPM.start() and finish(). def scheduleTasks(self, options, ticketsByID): """Called to schedule tasks""" # TracPM masks implementation details of how various plugins implement # dates and ticket relationships and business rules about what the # default estimate for a ticket is, etc. # # It provides utility functions to augment TicketQuery so that you can # find all the descendants of a ticket (root=id) or all the tickets # required for a specified ticket (goal=id). # # After querying, TracPM can be used to post-process the query results # to augment the tickets in memory with normalized meta-data about # their relationships. After that processing, a ticket may have the # properties listed below. Which properties are present depends on # what is configured in the [TracPM] section of trac.ini. # # parent - accessed with TracPM.parent(t) # children - accessed with TracPM.children(t) # # successors - accessed with TracPM.successors(t) # predecessors - accessed with TracPM.predecessors(t) # # start - accessed with TracPM.start(t) # finish - accessed with TracPM.finish(t) # # FIXME - we should probably have some weird, unique prefix on the # field names ("pm_", etc.). Perhaps configurable. # # FIXME - do we need access methods for estimate and worked? class TracPM(Component): cfgSection = 'TracPM' fields = None Option(cfgSection, 'hours_per_estimate', '1', """Hours represented by each unit of estimated work""") Option(cfgSection, 'default_estimate', '4.0', """Default work for an unestimated task, same units as estimate""") Option(cfgSection, 'estimate_pad', '0.0', "How much work may be remaining when a task goes over estimate,"+ " same units as estimate""") Option(cfgSection, 'fields.percent', None, """Ticket field to use as the data source for the percent complete column.""") Option(cfgSection, 'fields.estimate', None, """Ticket field to use as the data source for estimated work""") Option(cfgSection, 'fields.worked', None, """Ticket field to use as the data source for completed work""") Option(cfgSection, 'fields.start', None, """Ticket field to use as the data source for start date""") Option(cfgSection, 'fields.finish', None, """Ticket field to use as the data source for finish date""" ) Option(cfgSection, 'fields.pred', None, """Ticket field to use as the data source for predecessor list""") Option(cfgSection, 'fields.succ', None, """Ticket field to use as the data source for successor list""") Option(cfgSection, 'fields.parent', None, """Ticket field to use as the data source for the parent""") Option(cfgSection, 'parent_format', '%s', """Format of ticket IDs in parent field""") Option(cfgSection, 'milestone_type', 'milestone', """Ticket type for milestone-like tickets""") scheduler = ExtensionOption(cfgSection, 'scheduler', ITaskScheduler, 'CalendarScheduler') def __init__(self): self.env.log.info('Initializing TracPM') # PM-specific data can be retrieved from custom fields or # external relations. self.sources lists all configured data # sources and provides a key to self.fields or self.relations # to drive the actual db query. self.sources = {} # All the data sources that may come from custom fields. fields = ('percent', 'estimate', 'worked', 'start', 'finish', 'pred', 'succ', 'parent') # All the data sources that may come from external relations. # # Each item lists the fields (above) that are affected by this # relation. From configuration we get another three elements: # table, src, dst. relations = {} relations['pred-succ'] = [ 'pred', 'succ' ] # Process field configuration self.fields = {} for field in fields: value = self.config.get(self.cfgSection, 'fields.%s' % field) # As of 0.11.6, there appears to be a bug in Option() so # that a default of None isn't used and we get an empty # string instead. # # Remember the source for this data if value != '' and value != None: # FUTURE: If we really wanted to, we could validate value # against the list of Trac's custom fields. self.sources[field] = field self.fields[field] = value # Process relation configuration # # Find out how to query ticket relationships # Each item is "table,src,dst" so that we can do # SELECT dst FROM table where src = id # or # SELECT dst FROM table WHERE src IN origins # to find the related tickets. self.relations = {} for r in relations: value = self.config.getlist(self.cfgSection, 'relation.%s' % r) # See note above about defaults. # Remember the source for this data if value != [] and value != None: # Validate field and relation configuation if len(value) != 3: self.env.log.error('Relation %s is misconfigured. ' 'Should have three fields: ' 'table,src,dst; found "%s".' % (r, value)) else: for f in relations[r]: if f in self.fields: self.env.log.error('Cannot configure %s as a field ' 'and from relation %s' % (f, r)) self.sources[f] = r self.relations[r] = relations[r] + value # Tickets of this type will be displayed as milestones. self.milestoneType = self.config.get(self.cfgSection, 'milestone_type') # Hours per estimate unit. # # If estimate is in hours, this is 1. # # If estimate is in days, this is may be 8 or 24, depending on # your needs and the setting of hoursPerDay self.hpe = float(self.config.get(self.cfgSection, 'hours_per_estimate')) # Default work in an unestimated task self.dftEst = float(self.config.get(self.cfgSection, 'default_estimate')) # How much to pad an estimate when a task has run over self.estPad = float(self.config.get(self.cfgSection, 'estimate_pad')) # Parent format option self.parent_format = self.config.get(self.cfgSection,'parent_format') # This is the format of start and finish in the Trac database self.dbDateFormat = str(self.config.get(self.cfgSection, 'date_format')) # Return True if all of the listed PM data items ('pred', # 'parent', etc.) have sources configured, False otherwise def isCfg(self, sources): if type(sources) == type([]): for s in sources: if s not in self.sources: return False else: return sources in self.sources return True def isField(self, field): return self.isCfg(field) and self.sources[field] in self.fields def isRelation(self, field): return self.isCfg(field) and self.sources[field] in self.relations # Return True if ticket has a non-empty value for field, False # otherwise. def isSet(self, ticket, field): if self.isCfg(field) \ and len(ticket[self.fields[field]]) != 0: return True else: return False # FIXME - Many of these should be marked as more private. Perhaps # an leading underscore? # Parse the start field and return a datetime # Return None if the field is not configured or empty. def parseStart(self, ticket): if self.isSet(ticket, 'start'): start = datetime(*time.strptime(ticket[self.fields['start']], self.dbDateFormat)[0:7]) start.replace(hour=0, minute=0, second=0, microsecond=0) else: start = None return start # Parse the finish field and return a datetime # Return None if the field is not configured or empty. def parseFinish(self, ticket): if self.isSet(ticket, 'finish'): finish = datetime(*time.strptime(ticket[self.fields['finish']], self.dbDateFormat)[0:7]) finish.replace(hour=0, minute=0, second=0, microsecond=0) else: finish = None return finish # Is d at the start of the day # # It gets messy to do this explicitly inline, especially because # some date math seems to have rounding issues that result in # non-zero microseconds. def isStartOfDay(self, d): # Subtract d from midnight that day delta = d - d.replace(hour=0, minute=0, second=0, microsecond=0) # If within 5 seconds of midnight, it's midnight. # # Note that seconds and microseconds are always positive # http://docs.python.org/library/datetime.html#timedelta-objects return delta.seconds < 5 # Get the value for a PM field from a ticket def _fieldValue(self, ticket, field): # If the field isn't configured, return None if not self.isCfg(field): return None # If the value comes from a custom field, resolve the field # name through sources and use it to index the ticket. elif self.isField(field): return ticket[self.fields[self.sources[field]]] # If the value comes from a relation, we use the internal # name directly. else: return ticket[field] # Return the integer ID of the parent ticket # 0 if no parent # None if parent is not configured def parent(self, ticket): return self._fieldValue(ticket, 'parent') # Return list of integer IDs of children. # None if parent is not configured. def children(self, ticket): return ticket['children'] # Return a list of integer ticket IDs for immediate precedessors # for ticket or an empty list if there are none. def predecessors(self, ticket): value = self._fieldValue(ticket, 'pred') if value == None: return [] else: return value # Return a list of integer ticket IDs for immediate successors for # ticket or an empty list if there are none. def successors(self, ticket): value = self._fieldValue(ticket, 'succ') if value == None: return [] else: return value # Return computed start for ticket def start(self, ticket): return ticket['calc_start'][0] # Return computed start for ticket def finish(self, ticket): return ticket['calc_finish'][0] # Return a list of custom fields that PM needs to work. The # caller can add this to the list of fields in a query so that # when the tickets are passed back to PM the necessary data is # there. def queryFields(self): fields = [] for field in self.fields: fields.append(self.fields[field]) return fields # Return True if ticket is a milestone, False otherwise. def isMilestone(self, ticket): return ticket['type'] == self.milestoneType # Return total hours of work in ticket as a floating point number def workHours(self, ticket): if self.isCfg('estimate') and ticket.get(self.fields['estimate']): est = float(ticket[self.fields['estimate']]) else: est = None if self.isCfg('worked') and ticket.get(self.fields['worked']): work = float(ticket[self.fields['worked']]) else: work = None # Milestones have no work. if ticket['type'] == self.milestoneType: est = 0.0 # Closed tickets took as long as they took elif ticket['status'] == 'closed' and work: est = work # If the task is over its estimate, assume it will take # pad more time elif work > est: est = work + self.estPad # If unestimated, use the default elif not est or est == 0: est = self.dftEst # Otherwise, use the estimate parsed above. # Scale by hours per estimate hours = est * self.hpe return hours # Return percent complete. # # If estimate and worked are configured and estimate is not 0, # returns 'est/work' as a string. # # If estimate or work are not configured or estimate is 0, returns # percent complete as an integer. def percentComplete(self, ticket): # Closed tickets are 100% complete if ticket['status'] == 'closed': percent = 100 # Compute percent complete if given estimate and worked elif self.isCfg(['estimate', 'worked']): # Try to compute the percent complete, default to 0 estimate = self.workHours(ticket) / self.hpe if (estimate == 0): percent = 0 else: worked = ticket[self.fields['worked']] if worked == '': percent = 0 else: worked = float(worked) percent = '%s/%s' % (worked, estimate) # Use percent if provided elif self.isCfg('percent'): try: percent = int(ticket[self.fields['percent']]) except: percent = 0 # If no estimate and worked (above) and no percent, it's 0 else: percent = 0 return percent # Returns pipe-delimited, possibily empty string of ticket IDs # meeting PM constraints. Suitable for use as id field in ticket # query engine. # FIXME - dumb name def preQuery(self, options, this_ticket = None): # Expand the list of tickets in origins to include those # related through field. # origins is a list of strings def _expand(origins, field, format): if len(origins) == 0: return [] node_list = [format % tid for tid in origins] db = self.env.get_db_cnx() cursor = db.cursor() # Query from external table if self.isRelation(field): relation = self.relations[self.sources[field]] # Forward query if field == relation[0]: (f1, f2, tbl, src, dst) = relation # Reverse query elif field == relation[1]: (f1, f2, tbl, dst, src) = relation else: raise TracError('Relation configuration error for %s' % field) # FIXME - is this portable across DBMSs? cursor.execute("SELECT %s FROM %s WHERE %s IN (%s)" % (dst, tbl, src, "'" + "','".join(node_list) + "'")) # Query from custom field elif self.isField(field): fieldName = self.fields[self.sources[field]] # FIXME - is this portable across DBMSs? cursor.execute("SELECT t.id " "FROM ticket AS t " "LEFT OUTER JOIN ticket_custom AS p ON " " (t.id=p.ticket AND p.name='%s') " "WHERE p.value IN (%s)" % (fieldName, "'" + "','".join(node_list) + "'")) # We really can't get here because the callers test for # isCfg() but it's nice form to have an else. else: raise TracError('Cannot expand %s; ' 'Not configured as a field or relation.' % field) nodes = ['%s' % row[0] for row in cursor] return origins + _expand(nodes, field, format) id = '' if options['root']: if not self.isCfg('parent'): self.env.log.info('Cannot get tickets under root ' + 'without "parent" field configured') else: if options['root'] == 'self': if this_ticket: nodes = [ this_ticket ] else: nodes = [] else: nodes = options['root'].split('|') id += '|'.join(_expand(nodes, 'parent', self.parent_format)) if options['goal']: if not self.isCfg('succ'): self.env.log.info('Cannot get tickets for goal ' + 'without "succ" field configured') else: if options['goal'] == 'self': if this_ticket: nodes = [ this_ticket ] else: nodes = [] else: nodes = options['goal'].split('|') id += '|'.join(_expand(nodes, 'succ', '%s')) return id # Create a pseudoticket for a Trac milestone with all the fields # needed for PM work. def _pseudoTicket(self, id, summary, description, milestone): ticket = {} ticket['id'] = id ticket['summary'] = summary ticket['description'] = description ticket['milestone'] = milestone # Milestones are always shown ticket['level'] = 0 # A milestone has no owner ticket['owner'] = '' ticket['type'] = self.milestoneType ticket['status'] = '' if self.isCfg('estimate'): ticket[self.fields['estimate']] = 0 # There is no percent complete for a milestone if self.isCfg('percent'): ticket[self.fields['percent']] = 0 # A milestone has no children or parent if self.isCfg('parent'): ticket[self.fields['parent']] = 0 ticket['children'] = [] else: ticket['children'] = None # Place holder. ticket['link'] = '' # A milestone has no priority ticket['priority'] = 'n/a' return ticket # Add tasks for milestones related to the tickets def _add_milestones(self, options, tickets): if options.get('milestone'): milestones = options['milestone'].split('|') else: milestones = [] # FIXME - Really? This is a display option if not options['omitMilestones']: for t in tickets: if 'milestone' in t and \ t['milestone'] != '' and \ t['milestone'] not in milestones: milestones.append(t['milestone']) # Need a unique ID for each task. if len(milestones) > 0: id = 0 # Get the milestones and their due dates db = self.env.get_db_cnx() cursor = db.cursor() # FIXME - is this portable across DBMSs? cursor.execute("SELECT name, due FROM milestone " + "WHERE name in ('" + "','".join(milestones) + "')") for row in cursor: id = id-1 milestoneTicket = self._pseudoTicket(id, row[0], 'Milestone %s' % row[0], row[0]) # If there's no due date, default to today at close of business if self.isCfg('finish'): ts = row[1] or \ (datetime.now(utc) + timedelta(hours=options['hoursPerDay'])) milestoneTicket[self.fields['finish']] = \ format_date(ts, self.dbDateFormat) # jsGantt ignores start for a milestone but we use it # for scheduling. if self.isCfg('start'): milestoneTicket[self.fields['start']] = \ milestoneTicket[self.fields['finish']] # Any ticket with this as a milestone and no # successors has the milestone as a successor if self.isCfg(['pred', 'succ']): pred = [] for t in tickets: if not t['children'] and \ t['milestone'] == row[0] and \ self.successors(t) == []: if self.isField('succ'): t[self.fields[self.sources['succ']]] = \ [ str(id) ] else: t['succ'] = [ str(id) ] pred.append(str(t['id'])) if self.isField('pred'): milestoneTicket[self.fields[self.sources['pred']]] = \ pred else: milestoneTicket['pred'] = pred # A Trac milestone has no successors if self.isField('succ'): milestoneTicket[self.fields[self.sources['succ']]] = [] elif self.isRelation('succ'): milestoneTicket['succ'] = [] tickets.append(milestoneTicket) # Process the tickets to normalize formats, etc. to simplify # access functions. # # Also queries PM values that come from external relations. # # A 'children' field is added to each ticket. If a 'parent' field # is configured for PM, then 'children' is the (possibly empty) # list of children. if there is no 'parent' field, then # 'children' is set to None. # # Milestones for the tickets are added as pseudo-tickets. def postQuery(self, options, tickets): # Handle custom fields. # Clean up custom fields which might be null ('--') vs. blank ('') for t in tickets: nullable = [ 'pred', 'succ', 'start', 'finish', 'parent', 'worked', 'estimate', 'percent' ] for field in nullable: if self.isField(field): fieldName = self.fields[self.sources[field]] if fieldName not in t: raise TracError('%s is not a custom ticket field' % fieldName) if t[fieldName] == '--': t[fieldName] = '' # Normalize parent field values. All parent values must be # done before building child lists, below. if self.isCfg('parent'): for t in tickets: # ChildTicketsPlugin puts '#' at the start of the # parent field. Strip it for simplicity. fieldName = self.fields[self.sources['parent']] parent = t[fieldName] if len(parent) > 0 and parent[0] == '#': t[fieldname] = parent[1:] # An empty parent default to 0 (no such ticket) if t[fieldName] == '': t[fieldName] = 0 # Otherwise, convert the string to an integer else: t[fieldName] = int(t[fieldName]) # Build child lists for t in tickets: if not self.isCfg('parent'): t['children'] = None elif self.isField('parent'): t['children'] = [c['id'] for c in tickets \ if c[fieldName] == t['id']] # Clean up successor, predecessor lists for t in tickets: lists = [ 'pred', 'succ' ] for field in lists: if self.isField(field): fieldName = self.fields[self.sources[field]] if t[fieldName] == '': t[fieldName] = [] else: t[fieldName] = \ [int(s.strip()) \ for s in t[fieldName].split(',')] # Fill in relations db = self.env.get_db_cnx() cursor = db.cursor() # Get all the IDs we care about relations from. ids = ['%s' % t['id'] for t in tickets] # For each configured relation ... for r in self.relations: # Get the elements of the relationship ... (f1, f2, tbl, src, dst) = self.relations[r] # FIXME - is this portable across DBMSs? idList = "'" + "','".join(ids) + "'" # ... query all relations with the desired IDs on either end ... cursor.execute("SELECT %s, %s FROM %s " "WHERE %s IN (%s)" " OR %s IN (%s)" % (src, dst, tbl, src, idList, dst, idList)) # ... quickly build a local cache of the forward and # reverse links ... fwd = {} rev = {} for row in cursor: (src, dst) = row if dst in fwd: fwd[dst].append(src) else: fwd[dst] = [ src ] if src in rev: rev[src].append(dst) else: rev[src] = [ dst ] # ... and put the links in the tickets. for t in tickets: if t['id'] in fwd: t[f1] = fwd[t['id']] else: t[f1] = [] if t['id'] in rev: t[f2] = rev[t['id']] else: t[f2] = [] self._add_milestones(options, tickets) # tickets is an unordered list of tickets. Each ticket contains # at least the fields returned by queryFields() and the whole list # was processed by postQuery(). def computeSchedule(self, options, tickets): # Convert list to dictionary, making copies so schedule can # mess with the tickets. ticketsByID = {} for t in tickets: ticketsByID[t['id']] = {} for field in t: ticketsByID[t['id']][field] = copy.copy(t[field]) # Schedule the tickets self.scheduler.scheduleTasks(options, ticketsByID) # Copy back the schedule results for t in tickets: for field in [ 'calc_start', 'calc_finish']: t[field] = ticketsByID[t['id']][field] # ======================================================================== # Really simple calendar # class SimpleCalendar(Component): implements(IResourceCalendar) def __init__(self): self.env.log.debug('Creating a simple calendar') """Nothing""" # FIXME - we'd like this to honor hoursPerDay def hoursAvailable(self, date, resource = None): # No hours on weekends if date.weekday() > 4: hours = 0 # 8 hours on week days else: hours = 8.0 return hours # ------------------------------------------------------------------------ # Handles dates, duration (estimate) and dependencies but not resource # leveling. # # Assumes a 5-day work week (Monday-Friday) and options['hoursPerDay'] # for every resource. # # A Note About Working Hours # # The naive scheduling algorithm in the plugin assumes all resources # work the same number of hours per day. That limit can be configured # (hoursPerDay) but defaults to 8.0. While is is likely that these # hours are something like 8am to 4pm (or 8am to 5pm, minus an hour # lunch), daily scheduling isn't concerned with which hours are # worked, only how many are worked each day. To simplify range # checking throughout the scheduler, calculations are done as if the # work day starts at midnight (hour==0) and continues for the # configured number of hours per day (e.g., 00:00..08:00). # # Differs from SimpleScheduler only in using a pluggable calendar to # determine hours available on a date. class CalendarScheduler(Component): implements(ITaskScheduler) pm = None def __init__(self): # Instantiate the PM component self.pm = TracPM(self.env) self.cal = SimpleCalendar(self.env) # ITaskScheduler method # Uses options hoursPerDay and schedule (alap or asap). def scheduleTasks(self, options, ticketsByID): # fromDate, accounting for working hours and weekends. def _calendarOffset(ticket, hours, fromDate): if hours < 0: sign = -1 else: sign = 1 delta = timedelta(hours=0) while hours != 0: f = fromDate + delta # Get total hours available for resource on that date available = self.cal.hoursAvailable(f, ticket['owner']) # Clip available based on time of day on target date # (hours before a finish or after a start) # # Convert 4:30 into 4.5, 16:15 into 16.25, etc. h = f.hour + f.minute / 60. + f.second / 3600. # See how many hours are available before or after the # threshold on this day if sign == -1: if h < available: available = h else: if options['hoursPerDay']-h < available: available = options['hoursPerDay']-h # If we can finish the task this day if available >= math.fabs(hours): # See how many hours are available for other tasks this day available += -1 * sign * hours # If there are no more hours this day, make sure # that the delta ends up at the end (start or # finish) of the day if available == 0: if sign == -1: delta += timedelta(hours=-h) else: delta += timedelta(hours=options['hoursPerDay']-h) # If there is time left after this, just update # the delta within this day else: # Add the remaining time to the delta (sign is # implicit in hours) delta += timedelta(hours=hours) # No hours left when we're done. hours = 0 # If we can't finish the task this day else: # We do available hours of work this day ... hours -= sign * available # ... And move to another day to do more. if sign == -1: # Account for the time worked this date # (That is, get to start of the day) delta += timedelta(hours = -h) # Back up to end of previous day delta += timedelta(hours = -(24 - options['hoursPerDay'])) else: # Account for the time work this date # (That is move to the end of today) delta += timedelta(hours = options['hoursPerDay'] - h) # Move ahead to the start of the next day delta += timedelta(hours = 24 - options['hoursPerDay']) return delta # Return True if d1 is better than d2 # Each is a tuple in the form [date, explicit] or None def _betterDate(d1, d2): # If both are None, neither is better if d1 == None and d2 == None: better = False # If d1 is None, d2 is better if it is explicit # That is, don't replace "not yet set" with an implicit date elif d1 == None: better = d2[1] # If d2 is None, d1 has to be better elif d2 == None: better = True # If d1 is explicit elif d1[1]: # If d2 is implicit, d1 is better if not d2[1]: better = True # If d2 is also explicit, d1 isn't better else: better = False # d1 is implicit, it can't be better than d2 else: better = False if (better): self.env.log.debug('%s is better than %s' % (d1, d2)) else: self.env.log.debug('%s is NOT better than %s' % (d1, d2)) return better # TODO: If we have start and estimate, we can figure out # finish (opposite case of figuring out start from finish and # estimate as we do now). # Schedule a task As Late As Possible # # Return a tuple like [start, explicit] where # start is the start of the task as a date object # # explicit is True if start was parsed from a user # specified value and False if it was it is inferred as # today def _schedule_task_alap(t): # Find the finish of the closest ancestor with one set (if any) def _ancestor_finish(t): finish = None # If there are parent and finish fields if self.pm.isCfg(['finish', 'parent']): pid = self.pm.parent(t) # If this ticket has a parent, process it if pid != 0: if pid in ticketsByID: parent = ticketsByID[pid] _schedule_task_alap(parent) if _betterDate(ticketsByID[pid]['calc_finish'], finish): finish = ticketsByID[pid]['calc_finish'] else: self.env.log.info(('Ticket %s has parent %s ' + 'but %s is not in the chart. ' + 'Ancestor deadlines ignored.') % (t['id'], pid, pid)) return copy.copy(finish) # Find the earliest start of any successor # t is a ticket (list of ticket fields) # start is a tuple ([date, explicit]) def _earliest_successor(t, start): for id in self.pm.successors(t): if id in ticketsByID: s = _schedule_task_alap(ticketsByID[id]) if _betterDate(s, start) and \ start == None or \ (s and start and s[0] < start[0]): start = s else: self.env.log.info(('Ticket %s has successor %s ' + 'but %s is not in the chart. ' + 'Dependency deadlines ignored.') % (t['id'], id, id)) self.env.log.debug('earliest successor is %s' % start) return copy.copy(start) # If we haven't scheduled this yet, do it now. if t.get('calc_finish') == None: # If there is a finish set, use it if self.pm.isSet(t, 'finish'): # Don't adjust for work week; use the explicit date. finish = self.pm.parseFinish(t) finish += timedelta(hours=options['hoursPerDay']) finish = [finish, True] # Otherwise, compute finish from dependencies. else: finish = _earliest_successor(t, _ancestor_finish(t)) # If dependencies don't give a date, default to # today at close of business if finish == None: # Start at midnight today finish = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # Move ahead to beginning of next day so fixup # below will handle work week. finish += timedelta(days=1) finish = [finish, False] # If we are to finish at the beginning of the work # day, our finish is really the end of the previous # work day if self.pm.isStartOfDay(finish[0]): # Start at start of day f = finish[0] # Move back one hour from start of day to make # sure finish is on a work day. f += _calendarOffset(t, -1, f) # Move forward one hour to the end of the day f += timedelta(hours=1) finish[0] = f # Set the field t['calc_finish'] = finish if t.get('calc_start') == None: if self.pm.isSet(t, 'start'): start = self.pm.parseStart(t) start = [start, True] # Adjust implicit finish for explicit start if _betterDate(start, finish): hours = self.pm.workHours(t) finish[0] = start[0] + _calendarOffset(t, hours, start[0]) t['calc_finish'] = finish else: hours = self.pm.workHours(t) start = t['calc_finish'][0] + \ _calendarOffset(t, -1*hours, t['calc_finish'][0]) start = [start, t['calc_finish'][1]] t['calc_start'] = start return t['calc_start'] # Schedule a task As Soon As Possible # Return the finish of the task as a date object def _schedule_task_asap(t): # Find the start of the closest ancestor with one set (if any) def _ancestor_start(t): start = None # If there are parent and start fields if self.pm.isCfg(['start', 'parent']): pid = self.pm.parent(t) # If this ticket has a parent, process it if pid != 0: if pid in ticketsByID: parent = ticketsByID[pid] _schedule_task_asap(parent) if _betterDate(ticketsByID[pid]['calc_start'], start): start = ticketsByID[pid]['calc_start'] else: self.env.log.info(('Ticket %s has parent %s ' + 'but %s is not in the chart. ' + 'Ancestor deadlines ignored.') % (t['id'], pid, pid)) return copy.copy(start) # Find the latest finish of any predecessor # t is a ticket (list of ticket fields) # start is a tuple ([date, explicit]) def _latest_predecessor(t, finish): for id in self.pm.predecessors(t): if id in ticketsByID: f = _schedule_task_asap(ticketsByID[id]) if _betterDate(f, finish) and \ finish == None or \ (f and finish and f[0] > finish[0]): finish = f else: self.env.log.info(('Ticket %s has predecessor %s ' + 'but %s is not in the chart. ' + 'Dependency deadlines ignored.') % (t['id'], id, id)) return copy.copy(finish) # If we haven't scheduled this yet, do it now. if t.get('calc_start') == None: # If there is a start set, use it if self.pm.isSet(t, 'start'): # Don't adjust for work week; use the explicit date. start = self.pm.parseStart(t) start = [start, True] # Otherwise, compute start from dependencies. else: start = _latest_predecessor(t, _ancestor_start(t)) # If dependencies don't give a date, default to today if start == None: # Start at midnight today start = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) # Move back to end of previous day so fixup # below will handle work week. start += timedelta(days=-1, hours=options['hoursPerDay']) start = [start, False] # If we are to start at the end of the work # day, our start is really the beginning of the next # work day if self.pm.isStartOfDay(start[0] - timedelta(hours = options['hoursPerDay'])): s = start[0] # Move ahead to the start of the next day s += timedelta(hours=24-options['hoursPerDay']) # Adjust for work days as needed s += _calendarOffset(t, 1, s) s += timedelta(hours=-1) start = [s, start[1]] # Set the field t['calc_start'] = start if t.get('calc_finish') == None: if self.pm.isSet(t, 'finish'): # Don't adjust for work week; use the explicit date. finish = self.pm.parseFinish(t) finish += timedelta(hours=options['hoursPerDay']) finish = [finish, True] # Adjust implicit start for explicit finish if _betterDate(finish, start): hours = self.pm.workHours(t) start[0] = finish[0] + _calendarOffset(t, -1*hours, finish[0]) t['calc_start'] = start else: hours = self.pm.workHours(t) finish = t['calc_start'][0] + \ _calendarOffset(t, +1*hours, t['calc_start'][0]) finish = [finish, start[1]] t['calc_finish'] = finish return t['calc_finish'] # Augment tickets in a scheduler-specific way to make # scheduling easier # # If a parent task has a dependency, copy it to its children. def _augmentTickets(ticketsByID): # Indexed by ticket ID, lists ticket's descendants desc = {} # Build descendant look up recursively. def buildDesc(tid): # A ticket is in its own "family" tree. desc[tid] = [ tid ] # For each child, add its subtree. for cid in self.pm.children(ticketsByID[tid]): desc[tid] += buildDesc(cid) return desc[tid] # Find the roots of the trees roots = [] for tid in ticketsByID: if self.pm.parent(ticketsByID[tid]) == 0: roots.append(tid) # Build the descendant tree for each root (and its descendants) for tid in roots: buildDesc(tid) # Propagate dependencies from parent to descendants (first # children, then recurse) def propagateDependencies(pid): parent = ticketsByID[pid] # Process predecessors and successors for fieldFunc in [ self.pm.predecessors, self.pm.successors ]: # Set functions to add dependency and its reverse # between two tickets. fwd = fieldFunc if fwd == self.pm.predecessors: rev = self.pm.successors else: rev = self.pm.predecessors # For each related ticket, if any for tid in fieldFunc(parent): # If the other ticket is in the list we're # working on and not another descendant of the # same parent. if tid in ticketsByID and \ tid not in desc[pid]: # For each child, if any for cid in self.pm.children(parent): # If the child is in the list we're # working on if cid in ticketsByID: # Add parent's dependency to this # child fwd(ticketsByID[cid]).append(tid) rev(ticketsByID[tid]).append(cid) # Recurse to lower-level descendants propagateDependencies(cid) # For each ticket to schedule for tid in ticketsByID: # If it has no parent if self.pm.parent(ticketsByID[tid]) == 0: # Propagate depedencies down to its children # (which recurses to update other descendants) propagateDependencies(tid) # Main schedule processing # If there is a parent/child relationship configured if self.pm.isCfg('parent'): _augmentTickets(ticketsByID) for id in ticketsByID: if options['schedule'] == 'alap': _schedule_task_alap(ticketsByID[id]) else: _schedule_task_asap(ticketsByID[id]) trac-jsgantt-0.9+r11145/0.11/tracjsgantt/__init__.py0000644000175000017500000000002411477252476017777 0ustar wmbwmbimport tracjsgantt trac-jsgantt-0.9+r11145/0.11/tracjsgantt/tracjsgantt.py0000644000175000017500000006711711704441566020575 0ustar wmbwmbimport re import time from datetime import timedelta, datetime from operator import itemgetter, attrgetter from trac.util.html import Markup from trac.util.text import javascript_quote from trac.wiki.macros import WikiMacroBase from trac.web.chrome import Chrome import copy from trac.ticket.query import Query from trac.config import IntOption, Option from trac.core import implements, Component, TracError from trac.web.api import IRequestFilter from trac.web.chrome import ITemplateProvider, add_script, add_stylesheet from pkg_resources import resource_filename from trac.wiki.api import parse_args from tracpm import TracPM # ======================================================================== class TracJSGanttSupport(Component): implements(IRequestFilter, ITemplateProvider) Option('trac-jsgantt', 'option.format', 'day', """Initial format of Gantt chart""") Option('trac-jsgantt', 'option.formats', 'day|week|month|quarter', """Formats to show for Gantt chart""") IntOption('trac-jsgantt', 'option.sample', 0, """Show sample Gantt""") IntOption('trac-jsgantt', 'option.res', 1, """Show resource column""") IntOption('trac-jsgantt', 'option.dur', 1, """Show duration column""") IntOption('trac-jsgantt', 'option.comp', 1, """Show percent complete column""") Option('trac-jsgantt', 'option.caption', 'Resource', """Caption to follow task in Gantt""") IntOption('trac-jsgantt', 'option.startDate', 1, """Show start date column""") IntOption('trac-jsgantt', 'option.endDate', 1, """Show finish date column""") Option('trac-jsgantt', 'option.dateDisplay', 'mm/dd/yyyy', """Format to display dates""") IntOption('trac-jsgantt', 'option.openLevel', 999, """How many levels of task hierarchy to show open""") IntOption('trac-jsgantt', 'option.expandClosedTickets', 1, """Show children of closed tasks in the task hierarchy""") Option('trac-jsgantt', 'option.colorBy', 'priority', """Field to use to color tasks""") IntOption('trac-jsgantt', 'option.lwidth', None, """Width (in pixels) of left table""") IntOption('trac-jsgantt', 'option.root', None, """Ticket(s) to show descendants of""") IntOption('trac-jsgantt', 'option.goal', None, """Ticket(s) to show predecessors of""") IntOption('trac-jsgantt', 'option.showdep', 1, """Show dependencies in Gantt""") IntOption('trac-jsgantt', 'option.userMap', 1, """Map user IDs to user names""") IntOption('trac-jsgantt', 'option.omitMilestones', 0, """Omit milestones""") Option('trac-jsgantt', 'option.schedule', 'alap', """Schedule algorithm: alap or asap""") # This seems to be the first floating point option. Option('trac-jsgantt', 'option.hoursPerDay', '8.0', """Hours worked per day""") # ITemplateProvider methods def get_htdocs_dirs(self): return [('tracjsgantt', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return [] # IRequestFilter methods def pre_process_request(self, req, handler): # I think we should look for a TracJSGantt on the page and set # a flag for the post_process_request handler if found return handler def post_process_request(self, req, template, data, content_type): add_script(req, 'tracjsgantt/jsgantt.js') add_stylesheet(req, 'tracjsgantt/jsgantt.css') add_stylesheet(req, 'tracjsgantt/tracjsgantt.css') return template, data, content_type class TracJSGanttChart(WikiMacroBase): """ Displays a Gantt chart for the specified tickets. The chart display can be controlled with a number of macro arguments: ||'''Argument'''||'''Description'''||'''Default'''|| || `formats`||What to display in the format control. A pipe-separated list of `minute`, `hour`, `day`, `week`, `month`, and `quarter` (though `minute` may not be very useful). ||'day|week|month|quarter'|| || `format`||Initial display format, one of those listed in `formats` || First format || || `sample`||Display sample tasks (1) or not (0) || 0 || || `res`||Show resource column (1) or not (0) || 1 || || `dur`||Show duration colunn (1) or not (0) || 1 || || `comp`||Show percent complete column (1) or not (0) || 1 || || `caption`||Caption to place to right of tasks: None, Caption, Resource, Duration, %Complete || Resource || || `startDate`||Show start date column (1) or not (0) || 1 || || `endDate`||Show end date column (1) or not (0) || 1 || || `dateDisplay`||Date display format: 'mm/dd/yyyy', 'dd/mm/yyyy', or 'yyyy-mm-dd' || 'mm/dd/yyyy' || || `openLevel`||Number of levels of tasks to show. 1 = only top level task. || 999 || || `colorBy`||Field to use to choose task colors. Each unique value of the field will have a different color task. Other likely useful values are owner and milestone but any field can be used. || priority || || `root`||When using something like Subtickets plugin to maintain a tree of tickets and subtickets, you may create a Gantt showing a ticket and all of its descendants with `root=`. The macro uses the configured `parent` field to find all descendant tasks and build an `id=` argument for Trac's native query handler.[[br]][[br]]Multiple roots may be provided like `root=1|12|32`.[[br]][[br]]When used in a ticket description or comment, `root=self` will display the current ticket's descendants.||None|| || `goal`||When using something like MasterTickets plugin to maintain ticket dependencies, you may create a Gantt showing a ticket and all of its predecessors with `goal=`. The macro uses the configured `succ` field to find all predecessor tasks and build an `id=` argument for Trac's native query handler.[[br]][[br]]Multiple goals may be provided like `goal=1|12|32`.[[br]][[br]]When used in a ticket description or comment, `goal=self` will display the current ticket's predecessors.||None|| || `lwidth`||The width, in pixels, of the table of task names, etc. on the left of the Gantt. || || || `showdep`||Show dependencies (1) or not (0)||1|| || `userMap`||Map user !IDs to full names (1) or not (0).||1|| || `omitMilestones`||Show milestones for displayed tickets (0) or only those specified by `milestone=` (1)||0|| || `schedule`||Schedule tasks based on dependenies and estimates. Either as soon as possible (asap) or as late as possible (alap)||alap|| Site-wide defaults for macro arguments may be set in the `trac-jsgantt` section of `trac.ini`. `option.` overrides the built-in default for `` from the table above. All other macro arguments are treated as TracQuery specification (e.g., milestone=ms1|ms2) to control which tickets are displayed. """ pm = None def __init__(self): # Instantiate the PM component self.pm = TracPM(self.env) self.GanttID = 'g' # All the macro's options with default values. # Anything else passed to the macro is a TracQuery field. options = ('format', 'formats', 'sample', 'res', 'dur', 'comp', 'caption', 'startDate', 'endDate', 'dateDisplay', 'openLevel', 'expandClosedTickets', 'colorBy', 'lwidth', 'root', 'goal', 'showdep', 'userMap', 'omitMilestones', 'schedule', 'hoursPerDay') self.options = {} for opt in options: self.options[opt] = self.config.get('trac-jsgantt', 'option.%s' % opt) # These have to be in sync. jsDateFormat is the date format # that the JavaScript expects dates in. It can be one of # 'mm/dd/yyyy', 'dd/mm/yyyy', or 'yyyy-mm-dd'. pyDateFormat # is a strptime() format that matches jsDateFormat. As long # as they are in sync, there's no real reason to change them. self.jsDateFormat = 'yyyy-mm-dd' self.pyDateFormat = '%Y-%m-%d %H:%M' # User map (login -> realname) is loaded on demand, once. # Initialization to None means it is not yet initialized. self.user_map = None def _begin_gantt(self, options): if options['format']: defaultFormat = options['format'] else: defaultFormat = options['formats'].split('|')[0] showdep = options['showdep'] text = '' text += '
\n' text += '\n' return chart def _gantt_options(self, options): opt = '' opt += self.GanttID+'.setShowRes(%s);\n' % options['res'] opt += self.GanttID+'.setShowDur(%s);\n' % options['dur'] opt += self.GanttID+'.setShowComp(%s);\n' % options['comp'] w = options['lwidth'] if w: opt += self.GanttID+'.setLeftWidth(%s);\n' % w opt += self.GanttID+'.setCaptionType("%s");\n' % \ javascript_quote(options['caption']) opt += self.GanttID+'.setShowStartDate(%s);\n' % options['startDate'] opt += self.GanttID+'.setShowEndDate(%s);\n' % options['endDate'] opt += self.GanttID+'.setDateInputFormat("%s");\n' % \ javascript_quote(self.jsDateFormat) opt += self.GanttID+'.setDateDisplayFormat("%s");\n' % \ javascript_quote(options['dateDisplay']) opt += self.GanttID+'.setFormatArr(%s);\n' % ','.join( '"%s"' % javascript_quote(f) for f in options['formats'].split('|')) opt += self.GanttID+'.setPopupFeatures("location=1,scrollbars=1");\n' return opt # TODO - use ticket-classN styles instead of colors? def _add_sample_tasks(self): tasks = '' tasks = self.GanttID+'.setDateInputFormat("mm/dd/yyyy");' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(1, "Define Chart API", "", "", "#ff0000", "http://help.com", 0, "Brian", 0, 1, 0, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(11, "Chart Object", "2/20/2011", "2/20/2011", "#ff00ff", "http://www.yahoo.com", 1, "Shlomy", 100, 0, 1, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(12, "Task Objects", "", "", "#00ff00", "", 0, "Shlomy", 40, 1, 1, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(121, "Constructor Proc", "2/21/2011", "3/9/2011", "#00ffff", "http://www.yahoo.com", 0, "Brian T.", 60, 0, 12, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(122, "Task Variables", "3/6/2011", "3/11/2011", "#ff0000", "http://help.com", 0, "", 60, 0, 12, 1,121, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(123, "Task Functions", "3/9/2011", "3/29/2011", "#ff0000", "http://help.com", 0, "Anyone", 60, 0, 12, 1, 0, "This is another caption", '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(2, "Create HTML Shell", "3/24/2011", "3/25/2011", "#ffff00", "http://help.com", 0, "Brian", 20, 0, 0, 1,122, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(3, "Code Javascript", "", "", "#ff0000", "http://help.com", 0, "Brian", 0, 1, 0, 1 , '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(31, "Define Variables", "2/25/2011", "3/17/2011", "#ff00ff", "http://help.com", 0, "Brian", 30, 0, 3, 1, 0,"Caption 1", '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(32, "Calculate Chart Size", "3/15/2011", "3/24/2011", "#00ff00", "http://help.com", 0, "Shlomy", 40, 0, 3, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(33, "Draw Taks Items", "", "", "#00ff00", "http://help.com", 0, "Someone", 40, 1, 3, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(332, "Task Label Table", "3/6/2011", "3/11/2011", "#0000ff", "http://help.com", 0, "Brian", 60, 0, 33, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(333, "Task Scrolling Grid", "3/9/2011", "3/20/2011", "#0000ff", "http://help.com", 0, "Brian", 60, 0, 33, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(34, "Draw Task Bars", "", "", "#990000", "http://help.com", 0, "Anybody", 60, 1, 3, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(341, "Loop each Task", "3/26/2011", "4/11/2011", "#ff0000", "http://help.com", 0, "Brian", 60, 0, 34, 1, "332,333", '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(342, "Calculate Start/Stop", "4/12/2011", "5/18/2011", "#ff6666", "http://help.com", 0, "Brian", 60, 0, 34, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(343, "Draw Task Div", "5/13/2011", "5/17/2011", "#ff0000", "http://help.com", 0, "Brian", 60, 0, 34, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(344, "Draw Completion Div", "5/17/2011", "6/04/2011", "#ff0000", "http://help.com", 0, "Brian", 60, 0, 34, 1, '+self.GanttID+'));\n' tasks += self.GanttID+'.AddTaskItem(new JSGantt.TaskItem(35, "Make Updates", "10/17/2011","12/04/2011","#f600f6", "http://help.com", 0, "Brian", 30, 0, 3, 1, '+self.GanttID+'));\n' return tasks # Get the required columns for the tickets which match the # criteria in options. def _query_tickets(self, options): query_args = {} for key in options.keys(): if not key in self.options: query_args[key] = options[key] # Expand (or set) list of IDs to include those specified by PM # query meta-options (e.g., root) pm_id = self.pm.preQuery(options, self._this_ticket()) if pm_id != '': if 'id' in query_args: query_args['id'] += '|' + pm_id else: query_args['id'] = pm_id # Start with values that are always needed fields = [ 'description', 'owner', 'type', 'status', 'summary', 'milestone', 'priorty'] # Add configured PM fields fields += self.pm.queryFields() # Make sure the coloring field is included if 'colorBy' in options and options['colorBy'] not in fields: fields.append(options['colorBy']) # Make the query argument query_args['col'] = "|".join(fields) # Construct the querystring. query_string = '&'.join(['%s=%s' % (f, str(v)) for (f, v) in query_args.iteritems()]) # Get the Query Object. query = Query.from_string(self.env, query_string) # Get all tickets rawtickets = query.execute(self.req) # Do permissions check on tickets tickets = [t for t in rawtickets if 'TICKET_VIEW' in self.req.perm('ticket', t['id'])] return tickets def _compare_tickets(self, t1, t2): # If t2 depends on t1, t2 is first if t1['id'] in self.pm.successors(t2): return 1 # If t1 depends on t2, t1 is first elif t2['id'] in self.pm.successors(t1): return -1 # If t1 ends first, it's first elif self.pm.finish(t1) < self.pm.finish(t2): return -1 # If t2 ends first, it's first elif self.pm.finish(t1) > self.pm.finish(t2): return 1 # End dates are same. If t1 starts later, it's later elif self.pm.start(t1) > self.pm.start(t2): return 1 # Otherwise, preserve order (assume t1 is before t2 when called) else: return 0 # Compute WBS for sorting and figure out the tickets' levels for # controlling how many levels are open. # # WBS is a list like [ 2, 4, 1] (the first child of the fourth # child of the second top-level element). def _compute_wbs(self): # Set the ticket's level and wbs then recurse to children. def _setLevel(id, wbs, level): # Update this node self.ticketsByID[id]['level'] = level self.ticketsByID[id]['wbs'] = copy.copy(wbs) # Recurse to children childIDs = self.pm.children(self.ticketsByID[id]) if childIDs: childTickets = [self.ticketsByID[id] for id in childIDs] childTickets.sort(self._compare_tickets) childIDs = [ct['id'] for ct in childTickets] # Add another level wbs.append(1) for c in childIDs: wbs = _setLevel(c, wbs, level+1) # Remove the level we added wbs.pop() # Increment last element of wbs wbs[len(wbs)-1] += 1 return wbs # Set WBS and level on all top level tickets (and recurse) If # a ticket's parent is not in the viewed tickets, consider it # top-level wbs = [ 1 ] for t in self.tickets: if self.pm.parent(t) == None \ or self.pm.parent(t) == 0 \ or self.pm.parent(t) not in self.ticketsByID.keys(): wbs = _setLevel(t['id'], wbs, 1) def _task_display(self, t, options): def _buildMap(field): self.classMap = {} i = 0 for t in self.tickets: if t[field] not in self.classMap: i = i + 1 self.classMap[t[field]] = i def _buildEnumMap(field): self.classMap = {} db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT name," + db.cast('value', 'int') + " FROM enum WHERE type=%s", (field,)) for name, value in cursor: self.classMap[name] = value display = None colorBy = options['colorBy'] # Build the map the first time we need it if self.classMap == None: # Enums (TODO: what others should I list?) if options['colorBy'] in ['priority', 'severity']: _buildEnumMap(colorBy) else: _buildMap(colorBy) # Set display based on class map if t[colorBy] in self.classMap: display = 'class=ticket-class%d' % self.classMap[t[colorBy]] # Add closed status for strike through if t['status'] == 'closed': if display == None: display = 'class=ticket-closed' else: display += ' ticket-closed' if display == None: display = '#ff7f3f' return display # Format a ticket into JavaScript source to display the # task. ticket is expected to have: # children - child ticket IDs or None # description - ticket description. # id - ticket ID, an integer # level - levels from root (0) # link - What to link to # owner - Used as resource name. # percent - integer percent complete, 0..100 (or "act/est") # priority - used to color the task # calc_finish - end date (ignored if children is not None) # self.fields[parent] - parent ticket ID # self.fields[pred] - predecessor ticket IDs # calc_start - start date (ignored if children is not None) # status - string displayed in tool tip ; FIXME - not displayed yet # summary - ticket summary # type - string displayed in tool tip FIXME - not displayed yet def _format_ticket(self, ticket, options): # Translate owner to full name def _owner(ticket): if self.pm.isMilestone(ticket): owner_name = '' else: owner_name = ticket['owner'] if options['userMap']: # Build the map the first time we use it if self.user_map is None: self.user_map = {} for username, name, email in self.env.get_known_users(): self.user_map[username] = name # Map the user name if self.user_map.get(owner_name): owner_name = self.user_map[owner_name] return owner_name task = '' # pID, pName if self.pm.isMilestone(ticket): if ticket['id'] > 0: # Put ID number on inchpebbles name = 'MS:%s (#%s)' % (ticket['summary'], ticket['id']) else: # Don't show bogus ID of milestone pseudo tickets. name = 'MS:%s' % ticket['summary'] else: name = "#%d:%s (%s %s)" % \ (ticket['id'], ticket['summary'], ticket['status'], ticket['type']) task += 't = new JSGantt.TaskItem(%d,"%s",' % \ (ticket['id'], javascript_quote(name)) # pStart, pEnd task += '"%s",' % self.pm.start(ticket).strftime(self.pyDateFormat) task += '"%s",' % self.pm.finish(ticket).strftime(self.pyDateFormat) # pDisplay task += '"%s",' % javascript_quote(self._task_display(ticket, options)) # pLink task += '"%s",' % javascript_quote(ticket['link']) # pMile if self.pm.isMilestone(ticket): task += '1,' else: task += '0,' # pRes (owner) task += '"%s",' % javascript_quote(_owner(ticket)) # pComp (percent complete); integer 0..100 task += '"%s",' % self.pm.percentComplete(ticket) # pGroup (has children) if self.pm.children(ticket): task += '%s,' % 1 else: task += '%s,' % 0 # pParent (parent task ID) # If there's no parent field configured, don't link to parents if self.pm.parent(ticket) == None: task += '%s,' % 0 # If there's a parent field, but the ticket is in root, don't # link to parent elif options['root'] and \ str(ticket['id']) in options['root'].split('|'): task += '%s,' % 0 # If there's a parent field, root == self and this ticket is self, # don't link to parents elif options['root'] and \ options['root'] == 'self' and \ str(ticket['id']) == self._this_ticket(): task += '%s,' % 0 # If there's a parent, and the ticket is not a root, link to parent else: task += '%s,' % self.pm.parent(ticket) # open if ticket['level'] < options['openLevel'] and \ ((options['expandClosedTickets'] != 0) or \ (ticket['status'] != 'closed')): open = 1 else: open = 0 task += '%d,' % open # predecessors pred = [str(s) for s in self.pm.predecessors(ticket)] if len(pred): task += '"%s",' % javascript_quote(','.join(pred)) else: task += '"%s",' % javascript_quote(','.join('')) # caption # FIXME - if caption isn't set to caption, use "" because the # description could be quite long and take a long time to make # safe and display. task += '"%s (%s %s)"' % (javascript_quote(ticket['description']), javascript_quote(ticket['status']), javascript_quote(ticket['type'])) task += ', ' + self.GanttID task += ');\n' task += self.GanttID+'.AddTaskItem(t);\n' return task def _add_tasks(self, options): if options.get('sample'): tasks = self._add_sample_tasks() else: tasks = '' self.tickets = self._query_tickets(options) # Post process the query to add and compute fields so # displaying the tickets is easy self.pm.postQuery(options, self.tickets) # Faster lookups for WBS and scheduling. self.ticketsByID = {} for t in self.tickets: self.ticketsByID[t['id']] = t # Schedule the tasks self.pm.computeSchedule(options, self.tickets) # Sort tickets by date for computing WBS self.tickets.sort(self._compare_tickets) # Compute the WBS and sort them by WBS for display self._compute_wbs() self.tickets.sort(key=itemgetter('wbs')) # Set the link for clicking through the Gantt chart for t in self.tickets: if t['id'] > 0: t['link'] = self.req.href.ticket(t['id']) else: t['link'] = self.req.href.milestone(t['summary']) for ticket in self.tickets: tasks += self._format_ticket(ticket, options) return tasks def _parse_options(self, content): _, options = parse_args(content, strict=False) for opt in self.options.keys(): if opt in options: # FIXME - test for success, log on failure if isinstance(self.options[opt], (int, long)): options[opt] = int(options[opt]) else: options[opt] = self.options[opt] # FIXME - test for success, log on failure options['hoursPerDay'] = float(options['hoursPerDay']) return options def _this_ticket(self): matches = re.match('/ticket/(\d+)', self.req.path_info) if not matches: return None return matches.group(1) def expand_macro(self, formatter, name, content): self.req = formatter.req # Each invocation needs to build its own map. self.classMap = None options = self._parse_options(content) self.GanttID = 'g_'+ str(time.time()).replace('.','') chart = '' tasks = self._add_tasks(options) if len(tasks) == 0: chart += 'No tasks selected.' else: chart += self._begin_gantt(options) chart += self._gantt_options(options) chart += tasks chart += self._end_gantt(options) return chart trac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/0000755000175000017500000000000011704574102017140 5ustar wmbwmbtrac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/jsgantt.css0000644000175000017500000000646511546640172021344 0ustar wmbwmb /* * These are the class/styles used by various objects in GanttChart. * However, Firefox has problems deciphering class style when DIVs are * embedded in other DIVs. GanttChart makes heavy use of embedded * DIVS, thus the style are often embedded directly in the objects html. * If this could be resolved with Firefox, it would make a lot of the * code look simpler/cleaner without all the embedded styles */ .gantt { font-family:tahoma, arial, verdana; font-size:10px;} .gdatehead { BORDER-TOP: #efefef 1px solid; FONT-SIZE: 12px; BORDER-LEFT: #efefef 1px solid; HEIGHT: 18px } .ghead { BORDER-TOP: #efefef 1px solid; FONT-SIZE: 12px; BORDER-LEFT: #efefef 1px solid; WIDTH: 24px; HEIGHT: 20px } .gname { BORDER-TOP: #efefef 1px solid; FONT-SIZE: 12px; WIDTH: 18px; HEIGHT: 18px } .ghead A { FONT-SIZE: 10px; COLOR: #000000; TEXT-DECORATION: none } .gheadwkend A { FONT-SIZE: 10px; COLOR: #000000; TEXT-DECORATION: none } .gheadwkend { BORDER-TOP: #efefef 1px solid; FONT-SIZE: 12px; BORDER-LEFT: #efefef 1px solid; WIDTH: 24px; HEIGHT: 20px; background-color: #cfcfcf } .gfiller { BORDER-TOP: #efefef 1px solid; BORDER-LEFT: #efefef 1px solid; WIDTH: 18px; HEIGHT: 18px } .gfillerwkend { BORDER-LEFT: #efefef 1px solid; WIDTH: 18px; HEIGHT: 18px; BACKGROUND-COLOR: #cfcfcf } .gitem { BORDER-TOP: #cccccc 1px solid; WIDTH: 18px; HEIGHT: 18px } .gitemwkend { BORDER-TOP: #cccccc 1px solid; BORDER-LEFT: #cccccc 1px solid; WIDTH: 18px; HEIGHT: 18px } .gmilestone { BORDER-TOP: #efefef 1px solid; FONT-SIZE: 14px; OVERFLOW: hidden; BORDER-LEFT: #efefef 1px solid; WIDTH: 18px; HEIGHT: 18px} .gmilestonewkend { BORDER-TOP: #efefef 1px solid; BORDER-LEFT: #cccccc 1px solid; WIDTH: 18px; HEIGHT: 18px} .btn { BORDER-RIGHT: #ffffff; BORDER-TOP: #ffffff; FONT-WEIGHT: bold; FONT-SIZE: 10px; BORDER-LEFT: #ffffff; WIDTH: 12px; COLOR: #cccccc; BORDER-BOTTOM: #ffffff; BACKGROUND-COLOR: #ffffff } .hrcomplete { BORDER-RIGHT: #000000 2px solid; PADDING-RIGHT: 0px; BORDER-TOP: #000000 2px solid; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; BORDER-LEFT: #000000 2px solid; WIDTH: 20px; COLOR: #000000; PADDING-TOP: 0px; BORDER-BOTTOM: #000000 2px solid; HEIGHT: 4px } .hrhalfcomplete { BORDER-RIGHT: #000000 2px solid; BORDER-TOP: #000000 2px solid; BORDER-LEFT: #000000 2px solid; WIDTH: 9px; COLOR: #000000; BORDER-BOTTOM: #000000 2px solid; HEIGHT: 4px } .gweekend { font-family:tahoma, arial, verdana; font-size:11px; background-color:#EEEEEE; text-align:center; } .gtask { font-family:tahoma, arial, verdana; font-size:11px; background-color:#00FF00; text-align:center; } .gday { font-family:tahoma, arial, verdana; font-size:11px; text-align:center; } .gcomplete { background-color:black; height:5px; overflow: auto; margin-top:4px; } DIV.scroll { BORDER-RIGHT: #efefef 1px solid; PADDING-RIGHT: 0px; BORDER-TOP: #efefef 1px solid; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; OVERFLOW: hidden; BORDER-LEFT: #efefef 1px solid; PADDING-TOP: 0px; BORDER-BOTTOM: #efefef 1px solid; BACKGROUND-COLOR: #ffffff; float: left; } DIV.scroll2 { position:relative; PADDING-RIGHT: 0px; overflow-x:scroll; overflow-y:hidden; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; PADDING-TOP: 0px; BACKGROUND-COLOR: #ffffff } trac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/jsgantt_Minutes.html0000644000175000017500000005324011477252436023222 0ustar wmbwmb FREE javascript gantt - JSGantt HTML and CSS only
  jsGantt - 1.2
  Bugs/Issues    Download    License    Usage    Examples    Documenation    Subscribe    Credits




  100% Free Javascript / CSS/ HTML Gantt chart control. Completely buzzword compliant including AJAX !


Basic Features
  • Tasks & Collapsible Task Groups
  • Multiple Dependencies
  • Task Completion
  • Task Color
  • Milestones
  • Resources
  • No images needed
Advanced Features
  • Dynamic Loading of Tasks
  • Dynamic change of format
    • Day
    • Week
    • Month
    • Quarter
    • Hour
    • Minute
  • Load Gantt from XML file


Current Issues:
  1. Currently only one gantt chart is allowed per page.

New in 1.2:
  • Support for half-days
  • Hour/Minute format

Click here to download the jsgantt
You can download the latest bleeding edge version, request features and report issues at http://code.google.com/p/jsgantt/

JSGantt is released under BSD license. If you require another license please contact shlomygantz@hotmail.com
If you plan to use it in a commercial product please consider donating the first sale to charity.



1. Include JSGantt CSS and Javascript

<link rel="stylesheet" type="text/css" href="jsgantt.css" />
<script language="javascript" src="jsgantt.js"></script>

2. Create a div element to hold the gantt chart

<div style="position:relative" class="gantt" id="GanttChartDIV"></div>

3. Start a <script> block

<script language="javascript">

4. Instantiate JSGantt using GanttChart()

var g = new JSGantt.GanttChart('g',document.getElementById('GanttChartDIV'), 'day');

    

GanttChart(pGanttVar, pDiv, pFormat)
pGanttVar: (required) name of the variable assigned
pDiv: (required) this is a DIV object created in HTML
pFormat: (required) - used to indicate whether chart should be drawn in "day", "week", "month", or "quarter" format

Customize the look and feel using the following setters

g.setShowRes(1); // Show/Hide Responsible (0/1)
g.setShowDur(1); // Show/Hide Duration (0/1)
g.setShowComp(1); // Show/Hide % Complete(0/1)
g.setCaptionType('Resource');  // Set to Show Caption (None,Caption,Resource,Duration,Complete)
g.setShowStartDate(1); // Show/Hide Start Date(0/1)
g.setShowEndDate(1); // Show/Hide End Date(0/1)
g.setDateInputFormat('mm/dd/yyyy')  // Set format of input dates ('mm/dd/yyyy', 'dd/mm/yyyy', 'yyyy-mm-dd')
g.setDateDisplayFormat('mm/dd/yyyy') // Set format to display dates ('mm/dd/yyyy', 'dd/mm/yyyy', 'yyyy-mm-dd')
g.setFormatArr("day","week","month","quarter") // Set format options (up to 4 : "minute","hour","day","week","month","quarter")

5. Add Tasks using AddTaskItem()

 
g.AddTaskItem(new JSGantt.TaskItem(1,   'Define Chart API',     '',          '',          'ff0000', 'http://help.com', 0, 'Brian',     0, 1, 0, 1));
g.AddTaskItem(new JSGantt.TaskItem(11,  'Chart Object',         '2/10/2008', '2/10/2008', 'ff00ff', 'http://www.yahoo.com', 1, 'Shlomy',  100, 0, 1, 1, "121,122", "My Caption"));
TaskItem(pID, pName, pStart, pEnd, pColor, pLink, pMile, pRes, pComp, pGroup, pParent, pOpen, pDepend)
pID: (required) is a unique ID used to identify each row for parent functions and for setting dom id for hiding/showing
pName: (required) is the task Label
pStart: (required) the task start date, can enter empty date ('') for groups. You can also enter specific time (2/10/2008 12:00) for additional percision or half days.
pEnd: (required) the task end date, can enter empty date ('') for groups
pColor: (required) the html color for this task; e.g. '00ff00'
pLink: (optional) any http link navigated to when task bar is clicked.
pMile:(optional) represent a milestone
pRes: (optional) resource name
pComp: (required) completion percent
pGroup: (optional) indicates whether this is a group(parent) - 0=NOT Parent; 1=IS Parent
pParent: (required) identifies a parent pID, this causes this task to be a child of identified task
pOpen: can be initially set to close folder when chart is first drawn
pDepend: optional list of id's this task is dependent on ... line drawn from dependent to this item
pCaption: optional caption that will be added after task bar if CaptionType set to "Caption"
*You should be able to add items to the chart in realtime via javascript and issuing "g.Draw()" command.

5a. Another way to add tasks is to use an external XML file with parseXML()

 
JSGantt.parseXML("project.xml",g);
The structure of the XML file:

6. Call Draw() and DrawDependencies()

 

g.Draw();	
g.DrawDependencies();


7. Close the <script> block

</script>


Final code should look like

Enter your email address to receive JSGantt announcements


Developed by Shlomy Gantz and Brian Twidt
Contributed: Paul Labuschagne, Kevin Badgett, Ilan Admon

trac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/tracjsgantt.css0000644000175000017500000000213011620555552022177 0ustar wmbwmb/* * These styles are adapted from the standard report.css in Trac. * The border-color is a medium-dark gray so that tasks are more visible. */ .ticket-closed { text-decoration: line-through; } .ticket-class1 { background-color: #fdc; } div.ticket-class1 { border: 1px; border-color: #949494; border-style: solid; color: #a22; } .ticket-class2 { background-color: #ffb; } div.ticket-class2 { border: 1px; border-color: #949494; border-style: solid; color: #880; } .ticket-class3 { background-color: #fbfbfb; } div.ticket-class3 { border: 1px; border-color: #949494; border-style: solid; color: #444; } .ticket-class4 { background-color: #e7ffff; } div.ticket-class4 { border: 1px; border-color: #949494; border-style: solid; color: #099; } .ticket-class5 { background-color: #e7eeff; } div.ticket-class5 { border: 1px; border-color: #949494; border-style: solid; color: #469; } .ticket-class6 { background-color: #f0f0f0; } div.ticket-class6 { border: 1px; border-color: #949494; border-style: solid; color: #888; } trac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/jsgantt.js0000644000175000017500000026474711702566142021177 0ustar wmbwmb/* Copyright (c) 2009, Shlomy Gantz BlueBrick Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Shlomy Gantz or BlueBrick Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY SHLOMY GANTZ/BLUEBRICK INC. ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL SHLOMY GANTZ/BLUEBRICK INC. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * JSGantt component is a UI control that displays gantt charts based by using CSS and HTML * @module jsgantt * @title JSGantt */ var JSGantt; if (!JSGantt) JSGantt = {}; var vTimeout = 0; var vBenchTime = new Date().getTime(); /* Adapted from * http://www.toao.net/32-my-htmlspecialchars-function-for-javascript */ function htmlspecialchars(str) { if (typeof(str) == "string") { str = str.replace(/&/g, "&"); /* must do & first */ str = str.replace(/"/g, """); str = str.replace(/'/g, "'"); str = str.replace(//g, ">"); } return str; } /** * Creates a task (one row) in gantt object * @class TaskItem * @namespace JSGantt * @constructor * @for JSGantt * @param pID {Number} Task unique numeric ID * @param pName {String} Task Name * @param pStart {Date} Task start date/time (not required for pGroup=1 ) * @param pEnd {Date} Task end date/time, you can set the end time to 12:00 to indicate half-day (not required for pGroup=1 ) * @param pDisplay {String} How to display the task. "class=classname" or "style=style string" or a color ("#rgb" or a color name) * @param pLink {String} Task URL, clicking on the task will redirect to this url. Leave empty if you do not with the Task also serve as a link * @param pMile {Boolean} Determines whether task is a milestone (1=Yes,0=No) * @param pRes {String} Resource to perform the task * @param pComp {Number} Percent complete (Number between 0 and 100) * @param pGroup {Boolean} * @param pParent {Number} ID of the parent task * @param pOpen {Boolean} * @param pDepend {String} Comma seperated list of IDs this task depends on * @param pCaption {String} Caption to be used instead of default caption (Resource). * note : you should use setCaption("Caption") in order to display the caption * @return void */ JSGantt.TaskItem = function(pID, pName, pStart, pEnd, pDisplay, pLink, pMile, pRes, pComp, pGroup, pParent, pOpen, pDepend, pCaption, g) { /** * The name of the attribute. * @property vID * @type String * @default pID * @private */ var vID = pID; /** * @property vName * @type String * @default pName * @private */ var vName = pName; /** * @property vStart * @type Datetime * @default new Date() * @private */ var vStart = new Date(); /** * @property vEnd * @type Datetime * @default new Date() * @private */ var vEnd = new Date(); /** * @property vColor * @type String * @default pColor * @private */ var vColor; var vClass; var vStyle; /* Parse display value */ if (pDisplay.indexOf('=') == -1) { vColor = pDisplay; vClass = ''; vStyle = ''; } else { vColor = ''; pDisplay = pDisplay.split('=') switch (pDisplay[0]) { case 'class': vStyle = ''; vClass = pDisplay[1]; break; case 'style': vStyle = pDisplay[1]; vClass = ''; break; } } /** * @property vLink * @type String * @default pLink * @private */ var vLink = pLink; /** * @property vMile * @type Boolean * @default pMile * @private */ var vMile = pMile; /** * @property vRes * @type String * @default pRes * @private */ var vRes = pRes; /** * @property vComp * @type Number * @default pComp * @private */ var vComp; var vEst; var vAct; if (pComp.indexOf('/') == -1) { vComp = parseFloat(pComp); vEst = null; vAct = null; } else { // Parse on /, do math, and assign pComp = pComp.split('/'); vAct = parseFloat(pComp[0]); vEst = parseFloat(pComp[1]) if (vAct == NaN || vEst == NaN) { vComp = 0; } else if (vEst == 0) { vComp = 0 } else { vComp = Math.round(100 * (vAct / vEst)); } } /** * @property vGroup * @type Boolean * @default pGroup * @private */ var vGroup = pGroup; /** * @property vParent * @type Number * @default pParent * @private */ var vParent = pParent; /** * @property vOpen * @type Boolean * @default pOpen * @private */ var vOpen = pOpen; /** * @property vDepend * @type String * @default pDepend * @private */ var vDepend = pDepend; /** * @property vCaption * @type String * @default pCaption * @private */ var vCaption = pCaption; /** * @property vDuration * @type Number * @default '' * @private */ var vDuration = ''; /** * @property vLevel * @type Number * @default 0 * @private */ var vLevel = 0; /** * @property vNumKid * @type Number * @default 0 * @private */ var vNumKid = 0; /** * @property vVisible * @type Boolean * @default 0 * @private */ var vVisible = 1; var x1, y1, x2, y2; if (vGroup != 1) { vStart = JSGantt.parseDateStr(pStart,g.getDateInputFormat()); vEnd = JSGantt.parseDateStr(pEnd,g.getDateInputFormat()); } /** * Returns task ID * @method getID * @return {Number} */ this.getID = function(){ return vID }; /** * Returns task name * @method getName * @return {String} */ this.getName = function(){ return htmlspecialchars(vName) }; /** * Returns task start date * @method getStart * @return {Datetime} */ this.getStart = function(){ return vStart}; /** * Returns task end date * @method getEnd * @return {Datetime} */ this.getEnd = function(){ return vEnd }; /** * Returns task bar color (i.e. 00FF00) * @method getColor * @return {String} */ this.getColor = function(){ return vColor}; /** * Returns task bar style * @method getStyle * @return {String} */ this.getStyle = function(){ return vStyle}; /** * Returns task bar class * @method getClass * @return {String} */ this.getClass = function(){ return vClass}; /** * Returns task URL (i.e. http://www.jsgantt.com) * @method getLink * @return {String} */ this.getLink = function(){ return vLink }; /** * Returns whether task is a milestone (1=Yes,0=No) * @method getMile * @return {Boolean} */ this.getMile = function(){ return vMile }; /** * Returns task dependencies as list of values (i.e. 123,122) * @method getDepend * @return {String} */ this.getDepend = function(){ if(vDepend) return vDepend; else return null }; /** * Returns task caption (if it exists) * @method getCaption * @return {String} */ this.getCaption = function(){ if(vCaption) return vCaption; else return ''; }; /** * Returns task resource name as string * @method getResource * @return {String} */ this.getResource = function(){ if(vRes) return vRes; else return ' '; }; /** * Returns task estimate as a numeric value * @method getEstVal * @return {Number} */ this.getEstVal = function(){ if(vEst != null) return vEst; else return 100; }; /** * Returns task work as numeric value * @method getWorkVal * @return {Number} */ this.getWorkVal = function(){ if(vAct != null) return vAct; else return vComp; }; /** * Returns task completion percent as numeric value * @method getCompVal * @return {Number} */ this.getCompVal = function(){ if(vComp) return vComp; else return 0; }; /** * Returns task completion percent as formatted string (##%) * @method getCompStr * @return {String} */ this.getCompStr = function(){ if(vComp) return vComp+'%'; else return ''; }; /** * Returns task duration as a fortmatted string based on the current selected format * @method getDuration * @param vFormat {String} selected format (minute,hour,day,week,month) * @return {String} */ this.getDuration = function(vFormat){ if (vMile) vDuration = '-'; else if (vFormat=='hour') { tmpPer = Math.ceil((this.getEnd() - this.getStart()) / ( 60 * 60 * 1000) ); if(tmpPer == 1) vDuration = '1 Hour'; else vDuration = tmpPer + ' Hours'; } else if (vFormat=='minute') { tmpPer = Math.ceil((this.getEnd() - this.getStart()) / ( 60 * 1000) ); if(tmpPer == 1) vDuration = '1 Minute'; else vDuration = tmpPer + ' Minutes'; } else { //if(vFormat == 'day') { tmpPer = Math.ceil((this.getEnd() - this.getStart()) / (24 * 60 * 60 * 1000) + 1); if(tmpPer == 1) vDuration = '1 Day'; else vDuration = tmpPer + ' Days'; } //else if(vFormat == 'week') { // tmpPer = ((this.getEnd() - this.getStart()) / (24 * 60 * 60 * 1000) + 1)/7; // if(tmpPer == 1) vDuration = '1 Week'; // else vDuration = tmpPer + ' Weeks'; //} //else if(vFormat == 'month') { // tmpPer = ((this.getEnd() - this.getStart()) / (24 * 60 * 60 * 1000) + 1)/30; // if(tmpPer == 1) vDuration = '1 Month'; // else vDuration = tmpPer + ' Months'; //} //else if(vFormat == 'quater') { // tmpPer = ((this.getEnd() - this.getStart()) / (24 * 60 * 60 * 1000) + 1)/120; // if(tmpPer == 1) vDuration = '1 Qtr'; // else vDuration = tmpPer + ' Qtrs'; //} return( vDuration ) }; /** * Returns task parent ID * @method getParent * @return {Number} */ this.getParent = function(){ return vParent }; /** * Returns whether task is a group (1=Yes,0=No) * @method getGroup * @return {Number} */ this.getGroup = function(){ return vGroup }; /** * Returns whether task is open (1=Yes,0=No) * @method getOpen * @return {Boolean} */ this.getOpen = function(){ return vOpen }; /** * Returns task tree level (0,1,2,3...) * @method getLevel * @return {Boolean} */ this.getLevel = function(){ return vLevel }; /** * Returns the number of child tasks * @method getNumKids * @return {Number} */ this.getNumKids = function(){ return vNumKid }; /** * Returns the X position of the left side of the task bar on the graph (right side) * @method getStartX * @return {Number} */ this.getStartX = function(){ return x1 }; /** * Returns the Y position of the top of the task bar on the graph (right side) * @method getStartY * @return {Number} */ this.getStartY = function(){ return y1 }; /** * Returns the X position of the right of the task bar on the graph (right side) * @method getEndX * @return {Int} */ this.getEndX = function(){ return x2 }; /** * Returns the Y position of the bottom of the task bar on the graph (right side) * @method getEndY * @return {Number} */ this.getEndY = function(){ return y2 }; /** * Returns whether task is visible (1=Yes,0=No) * @method getVisible * @return {Boolean} */ this.getVisible = function(){ return vVisible }; /** * Set task dependencies * @method setDepend * @param pDepend {String} A comma delimited list of task IDs the current task depends on. * @return {void} */ this.setDepend = function(pDepend){ vDepend = pDepend;}; /** * Set task start date/time * @method setStart * @param pStart {Datetime} * @return {void} */ this.setStart = function(pStart){ vStart = pStart;}; /** * Set task end date/time * @method setEnd * @param pEnd {Datetime} * @return {void} */ this.setEnd = function(pEnd) { vEnd = pEnd; }; /** * Set task tree level (0,1,2,3...) * @method setLevel * @param pLevel {Number} * @return {void} */ this.setLevel = function(pLevel){ vLevel = pLevel;}; /** * Set Number of children for the task * @method setNumKid * @param pNumKid {Number} * @return {void} */ this.setNumKid = function(pNumKid){ vNumKid = pNumKid;}; /** * Set task work * @method setWorkVal * @param pWorkVal {Number} * @return {void} */ this.setWorkVal = function(pWorkVal){ vAct = pWorkVal; }; /** * Set task estimate * @method setEstVal * @param pEstVal {Number} * @return {void} */ this.setEstVal = function(pEstVal){ vEst = pEstVal; }; /** * Set task completion percentage * @method setCompVal * @param pCompVal {Number} * @return {void} */ this.setCompVal = function(pCompVal){ vComp = pCompVal;}; /** * Set a task bar starting position (left) * @method setStartX * @param pX {Number} * @return {void} */ this.setStartX = function(pX) {x1 = pX; }; /** * Set a task bar starting position (top) * @method setStartY * @param pY {Number} * @return {String} */ this.setStartY = function(pY) {y1 = pY; }; /** * Set a task bar starting position (right) * @method setEndX * @param pX {Number} * @return {String} */ this.setEndX = function(pX) {x2 = pX; }; /** * Set a task bar starting position (bottom) * @method setEndY * @param pY {Number} * @return {String} */ this.setEndY = function(pY) {y2 = pY; }; /** * Set task open/closed * @method setOpen * @param pOpen {Boolean} * @return {void} */ this.setOpen = function(pOpen) {vOpen = pOpen; }; /** * Set task visibility * @method setVisible * @param pVisible {Boolean} * @return {void} */ this.setVisible = function(pVisible) {vVisible = pVisible; }; }; /** * Creates the Gantt chart. for example:

var g = new JSGantt.GanttChart('g',document.getElementById('GanttChartDIV'), 'day');

var g = new JSGantt.GanttChart( - assign the gantt chart to a javascript variable called 'g' 'g' - the name of the variable that was just assigned (will be used later so that gantt object can reference itself) document.getElementById('GanttChartDIV') - reference to the DIV that will hold the gantt chart 'day' - default format will be by day * * @class GanttChart * @param pGanttVar {String} the name of the gantt chart variable * @param pDiv {String} reference to the DIV that will hold the gantt chart * @param pFormat {String} default format (minute,hour,day,week,month,quarter) * @return void */ /* FIXME - should be able to set Today color (or in CSS?) */ JSGantt.GanttChart = function(pGanttVar, pDiv, pFormat, pShowDep) { /** * The name of the gantt chart variable * @property vGanttVar * @type String * @default pGanttVar * @private */ var vGanttVar = pGanttVar; /** * The name of the gantt chart DIV * @property vDiv * @type String * @default pDiv * @private */ var vDiv = pDiv; /** * Selected format (minute,hour,day,week,month) * @property vFormat * @type String * @default pFormat * @private */ var vFormat = pFormat; /** * Whether or not to draw dependency lines * @property vShowDep * @type Character * @default pShowDep * @private */ var vShowDep = pShowDep; /** * Show resource column * @property vShowRes * @type Number * @default 1 * @private */ var vShowRes = 1; /** * Show duration column * @property vShowDur * @type Number * @default 1 * @private */ var vShowDur = 1; /** * Show percent complete column * @property vShowComp * @type Number * @default 1 * @private */ var vShowComp = 1; /** * Show start date column * @property vShowStartDate * @type Number * @default 1 * @private */ var vShowStartDate = 1; /** * Show end date column * @property vShowEndDate * @type Number * @default 1 * @private */ var vShowEndDate = 1; /** * Date input format * @property vDateInputFormat * @type String * @default "mm/dd/yyyy" * @private */var vDateInputFormat = "mm/dd/yyyy"; /** * Date display format * @property vDateDisplayFormat * @type String * @default "mm/dd/yy" * @private */var vDateDisplayFormat = "mm/dd/yy"; /** * Popup-window features (width, height, scroll bars, etc.) * @property vPopupFeatures * @type String * @default "height=200,width=300" * @private */var vPopupFeatures = "height=200,width=300"; /** * Width of the left table (task, duration, etc.) * @property vLeftWidth * @type integer * @default Room for +/-, name, resource, duration, %, start, and end dates * @private */var vLeftWidth = 15 + 220 + 70 + 70 + 70 + 70 + 70; var vNumUnits = 0; var vCaptionType; var vDepId = 1; var vTaskList = new Array(); var vFormatArr = new Array("day","week","month","quarter"); var vQuarterArr = new Array(1,1,1,2,2,2,3,3,3,4,4,4); var vMonthDaysArr = new Array(31,28,31,30,31,30,31,31,30,31,30,31); var vMonthArr = new Array("January","February","March","April","May","June","July","August","September","October","November","December"); /** * Set current display format (minute/hour/day/week/month/quarter) * Only the first 4 arguments are used, for example: * * g.setFormatArr("day","week","month"); * * will show 3 formatting options (day/week/month) at the bottom right of the gantt chart * @method setFormatArr * @return {void} */ this.setFormatArr = function() { vFormatArr = new Array(); for(var i = 0; i < arguments.length; i++) {vFormatArr[i] = arguments[i];} if(vFormatArr.length>4){vFormatArr.length=4;} }; /** * Show/Hide resource column * @param pShow {Number} 1=Show,0=Hide * @method setShowRes * @return {void} */ this.setShowRes = function(pShow) { vShowRes = pShow; }; /** * Show/Hide duration column * @param pShow {Number} 1=Show,0=Hide * @method setShowDur * @return {void} */ this.setShowDur = function(pShow) { vShowDur = pShow; }; /** * Whether or not to show depedency lines * @param pShowDep{Character} 1=show,0=Hide * @method setShowDep * #return {void} */this.setShowDep = function(pShowDep){vShowDep = pShowDep}; /** * Show/Hide completed column * @param pShow {Number} 1=Show,0=Hide * @method setShowComp * @return {void} */ this.setShowComp = function(pShow) { vShowComp = pShow; }; /** * Show/Hide start date column * @param pShow {Number} 1=Show,0=Hide * @method setShowStartDate * @return {void} */ this.setShowStartDate = function(pShow) { vShowStartDate = pShow; }; /** * Show/Hide end date column * @param pShow {Number} 1=Show,0=Hide * @method setShowEndDate * @return {void} */ this.setShowEndDate = function(pShow) { vShowEndDate = pShow; }; /** * Overall date input format * @param pShow {String} (mm/dd/yyyy,dd/mm/yyyy,yyyy-mm-dd) * @method setDateInputFormat * @return {void} */ this.setDateInputFormat = function(pShow) { vDateInputFormat = pShow; }; /** * Overall date display format * @param pShow {String} (mm/dd/yyyy,dd/mm/yyyy,yyyy-mm-dd) * @method setDateDisplayFormat * @return {void} */ this.setDateDisplayFormat = function(pShow) { vDateDisplayFormat = pShow; }; /** * Popup window features * @param pPopupFeatures {String} * @method setPopupFeatures * @return {void} */ this.setPopupFeatures = function(pPopupFeatures) { vPopupFeatures = pPopupFeatures; }; /** * Left table width * @param pLeftWidth {integer} * @method setLeftWidth * @return {void} */ this.setLeftWidth = function(pLeftWidth) { vLeftWidth = pLeftWidth; }; /** * Set gantt caption * @param pType {String}

Caption-Displays a custom caption set in TaskItem
Resource-Displays task resource
Duration-Displays task duration
Complete-Displays task percent complete

* @method setCaptionType * @return {void} */ this.setCaptionType = function(pType) { vCaptionType = pType }; /** * Set current display format and redraw gantt chart (minute/hour/day/week/month/quarter) * @param pFormat {String} (mm/dd/yyyy,dd/mm/yyyy,yyyy-mm-dd) * @method setFormat * @return {void} */ this.setFormat = function(pFormat){ vFormat = pFormat; this.Draw(); }; /** * Returns whether resource column is shown * @method getShowRes * @return {Boolean} */ this.getShowRes = function(){ return vShowRes }; /** * Returns whether duration column is shown * @method getShowDur * @return {Boolean} */ this.getShowDur = function(){ return vShowDur }; /** * Returns whether percent complete column is shown * @method getShowComp * @return {Boolean} */ this.getShowComp = function(){ return vShowComp }; /** * Returns whether start date column is shown * @method getShowStartDate * @return {Boolean} */ this.getShowStartDate = function(){ return vShowStartDate }; /** * Returns whether end date column is shown * @method getShowEndDate * @return {Boolean} */ this.getShowEndDate = function(){ return vShowEndDate }; /** * Returns date input format * @method getDateInputFormat * @return {String} */ this.getDateInputFormat = function() { return vDateInputFormat }; /** * Returns current display format * @method getDateDisplayFormat * @return {String} */ this.getDateDisplayFormat = function() { return vDateDisplayFormat }; /** * Returns popup feature string * @method getPopupFeatures * @return {String} */ this.getPopupFeatures = function() { return vPopupFeatures }; /** * Returns current gantt caption type * @method getCaptionType * @return {String} */ this.getCaptionType = function() { return vCaptionType }; /** * Calculates X/Y coordinates of a task and sets the Start and End properties of the TaskItem * @method CalcTaskXY * @return {Void} */ this.CalcTaskXY = function () { var vList = this.getList(); var vTaskDiv; var vParDiv; var vLeft, vTop, vHeight, vWidth; for(i = 0; i < vList.length; i++) { vID = vList[i].getID(); vTaskDiv = document.getElementById("taskbar_"+vID); vBarDiv = document.getElementById(pGanttVar+"_bardiv_"+vID); vParDiv = document.getElementById(pGanttVar+"_childgrid_"+vID); if(vBarDiv) { vList[i].setStartX( vBarDiv.offsetLeft ); vList[i].setStartY( vParDiv.offsetTop+vBarDiv.offsetTop+6 ); vList[i].setEndX( vBarDiv.offsetLeft + vBarDiv.offsetWidth ); vList[i].setEndY( vParDiv.offsetTop+vBarDiv.offsetTop+6 ); }; }; }; /** * Adds a TaskItem to the Gantt object task list array * @method AddTaskItem * @return {Void} */ this.AddTaskItem = function(value) { vTaskList.push(value); }; /** * Returns task list Array * @method getList * @return {Array} */ this.getList = function() { return vTaskList }; /** * Returns Gantt ID * @method getID * @return string */ this.getID = function() { return vGanttVar }; /** * Clears dependency lines between tasks * @method clearDependencies * @return {Void} */ this.clearDependencies = function() { var parent = document.getElementById('rightside_'+pGanttVar); var depLine; var vMaxId = vDepId; for ( i=1; i task 2 start) * @method drawDependency * @return {Void} */ this.drawDependency =function(x1,y1,x2,y2) { if(x1 + 10 < x2) { this.sLine(x1,y1,x1+4,y1); this.sLine(x1+4,y1,x1+4,y2); this.sLine(x1+4,y2,x2,y2); this.dLine(x2,y2,x2-3,y2-3); this.dLine(x2,y2,x2-3,y2+3); this.dLine(x2-1,y2,x2-3,y2-2); this.dLine(x2-1,y2,x2-3,y2+2); } else { this.sLine(x1,y1,x1+4,y1); this.sLine(x1+4,y1,x1+4,y2-10); this.sLine(x1+4,y2-10,x2-8,y2-10); this.sLine(x2-8,y2-10,x2-8,y2); this.sLine(x2-8,y2,x2,y2); this.dLine(x2,y2,x2-3,y2-3); this.dLine(x2,y2,x2-3,y2+3); this.dLine(x2-1,y2,x2-3,y2-2); this.dLine(x2-1,y2,x2-3,y2+2); } }; /** * Draw all task dependencies * @method DrawDependencies * @return {Void} */ this.DrawDependencies = function () { if (vShowDep=="1"){ //First recalculate the x,y this.CalcTaskXY(); this.clearDependencies(); var vList = this.getList(); for(var i = 0; i < vList.length; i++) { vDepend = vList[i].getDepend(); if(vDepend) { var vDependStr = vDepend + ''; var vDepList = vDependStr.split(','); var n = vDepList.length; for(var k=0;k 0) { // Process all tasks preset parent date and completion % JSGantt.processRows(vTaskList, 0, -1, 1, 1); // get overall min/max dates plus padding vMinDate = JSGantt.getMinDate(vTaskList, vFormat); vMaxDate = JSGantt.getMaxDate(vTaskList, vFormat); // Calculate chart width variables. vColWidth can be altered manually to change each column width // May be smart to make this a parameter of GanttChart or set it based on existing pWidth parameter if(vFormat == 'day') { vColWidth = 18; vColUnit = 1; } else if(vFormat == 'week') { vColWidth = 37; vColUnit = 7; } else if(vFormat == 'month') { vColWidth = 37; vColUnit = 30; } else if(vFormat == 'quarter') { vColWidth = 60; vColUnit = 90; } else if(vFormat=='hour') { vColWidth = 18; vColUnit = 1; } else if(vFormat=='minute') { vColWidth = 18; vColUnit = 1; } vNumDays = (Date.parse(vMaxDate) - Date.parse(vMinDate)) / ( 24 * 60 * 60 * 1000); vNumUnits = vNumDays / vColUnit; vChartWidth = vNumUnits * vColWidth + 1; vDayWidth = (vColWidth / vColUnit) + (1/vColUnit); vMainTable = '' if (vShowRes == 1) { vNameWidth -= vStatusWidth; vRadioSpan += 1; } if (vShowDur == 1) { vNameWidth -= vStatusWidth; vRadioSpan += 1; } if (vShowComp == 1) { vNameWidth -= vStatusWidth; vRadioSpan += 1; } if (vShowStartDate == 1) { vNameWidth -= vStatusWidth; vRadioSpan += 1; } if (vShowEndDate == 1) { vNameWidth -= vStatusWidth; vRadioSpan += 1; } // DRAW the Left-side of the chart (names, resources, comp%) vLeftTable = '
' + '' + ' ' + ' '; vLeftTable += '' + ' ' + ' ' ; if(vShowRes ==1) vLeftTable += ' ' ; if(vShowDur ==1) vLeftTable += ' ' ; if(vShowComp==1) vLeftTable += ' ' ; if(vShowStartDate==1) vLeftTable += ' ' ; if(vShowEndDate==1) vLeftTable += ' ' ; vLeftTable += ''; for(i = 0; i < vTaskList.length; i++) { if( vTaskList[i].getGroup()) { vBGColor = "f3f3f3"; vRowType = "group"; } else { vBGColor = "ffffff"; vRowType = "row"; } vID = vTaskList[i].getID(); if(vTaskList[i].getVisible() == 0) vLeftTable += '' ; else vLeftTable += '' ; vLeftTable += ' ' + ' ' ; if(vShowRes ==1) vLeftTable += ' ' ; if(vShowDur ==1) vLeftTable += ' ' ; if(vShowComp==1) vLeftTable += ' ' ; if(vShowStartDate==1) vLeftTable += ' ' ; if(vShowEndDate==1) vLeftTable += ' ' ; vLeftTable += ''; } // DRAW the date format selector at bottom left. Another potential GanttChart parameter to hide/show this selector vLeftTable += '' + '
'; vLeftTable += 'Format:'; if (vFormatArr.join().indexOf("minute")!=-1) { if (vFormat=='minute') vLeftTable += 'Minute'; else vLeftTable += 'Minute'; } if (vFormatArr.join().indexOf("hour")!=-1) { if (vFormat=='hour') vLeftTable += 'Hour'; else vLeftTable += 'Hour'; } if (vFormatArr.join().indexOf("day")!=-1) { if (vFormat=='day') vLeftTable += 'Day'; else vLeftTable += 'Day'; } if (vFormatArr.join().indexOf("week")!=-1) { if (vFormat=='week') vLeftTable += 'Week'; else vLeftTable += 'Week'; } if (vFormatArr.join().indexOf("month")!=-1) { if (vFormat=='month') vLeftTable += 'Month'; else vLeftTable += 'Month'; } if (vFormatArr.join().indexOf("quarter")!=-1) { if (vFormat=='quarter') vLeftTable += 'Quarter'; else vLeftTable += 'Quarter'; } // vLeftTable += ' .'; vLeftTable += '
ResourceDuration% Comp.Start DateEnd Date
 '; for(j=1; j ' ; else vLeftTable += '+ ' ; } else { vLeftTable += '   '; } vLeftTable += ' ' + vTaskList[i].getName() + '' + '' + vTaskList[i].getResource() + '' + vTaskList[i].getDuration(vFormat) + '' + vTaskList[i].getCompStr() + '' + JSGantt.formatDateStr( vTaskList[i].getStart(), vDateDisplayFormat) + '' + JSGantt.formatDateStr( vTaskList[i].getEnd(), vDateDisplayFormat) + '
  Powered by jsGantt      '; vLeftTable += '
'; vMainTable += vLeftTable; // Draw the Chart Rows vRightTable = '
' + '' + ''; vTmpDate.setFullYear(vMinDate.getFullYear(), vMinDate.getMonth(), vMinDate.getDate()); vTmpDate.setHours(0); vTmpDate.setMinutes(0); // Major Date Header while(Date.parse(vTmpDate) <= Date.parse(vMaxDate)) { vStr = vTmpDate.getFullYear() + ''; vStr = vStr.substring(2,4); if(vFormat == 'minute') { vRightTable += ''; vTmpDate.setHours(vTmpDate.getHours()+1); } if(vFormat == 'hour') { vRightTable += ''; vTmpDate.setDate(vTmpDate.getDate()+1); } if(vFormat == 'day') { vRightTable += ''; vTmpDate.setDate(vTmpDate.getDate()+7); } else if(vFormat == 'month') { vRightTable += ''; vTmpDate.setDate(vTmpDate.getDate() + 1); while(vTmpDate.getDate() > 1) { vTmpDate.setDate(vTmpDate.getDate() + 1); } } else if(vFormat == 'quarter') { vRightTable += ''; vTmpDate.setDate(vTmpDate.getDate() + 81); while(vTmpDate.getDate() > 1) { vTmpDate.setDate(vTmpDate.getDate() + 1); } } } vRightTable += ''; // Minor Date header and Cell Rows vTmpDate.setFullYear(vMinDate.getFullYear(), vMinDate.getMonth(), vMinDate.getDate()); vNxtDate.setFullYear(vMinDate.getFullYear(), vMinDate.getMonth(), vMinDate.getDate()); vNumCols = 0; while(Date.parse(vTmpDate) <= Date.parse(vMaxDate)) { if (vFormat == 'minute') { if( vTmpDate.getMinutes() ==0 ) vWeekdayColor = "ccccff"; else vWeekdayColor = "ffffff"; vDateRowStr += ''; vItemRowStr += ''; vTmpDate.setMinutes(vTmpDate.getMinutes() + 1); } else if (vFormat == 'hour') { if( vTmpDate.getHours() ==0 ) vWeekdayColor = "ccccff"; else vWeekdayColor = "ffffff"; vDateRowStr += ''; vItemRowStr += ''; vTmpDate.setHours(vTmpDate.getHours() + 1); } else if(vFormat == 'day' ) { if( JSGantt.formatDateStr(vCurrDate,'mm/dd/yyyy') == JSGantt.formatDateStr(vTmpDate,'mm/dd/yyyy')) { vWeekdayColor = "ccccff"; vWeekendColor = "9999ff"; vWeekdayGColor = "bbbbff"; vWeekendGColor = "8888ff"; } else { vWeekdayColor = "ffffff"; vWeekendColor = "cfcfcf"; vWeekdayGColor = "f3f3f3"; vWeekendGColor = "c3c3c3"; } if(vTmpDate.getDay() % 6 == 0) { vDateRowStr += ''; vItemRowStr += ''; } else { vDateRowStr += ''; if( JSGantt.formatDateStr(vCurrDate,'mm/dd/yyyy') == JSGantt.formatDateStr(vTmpDate,'mm/dd/yyyy')) vItemRowStr += ''; else vItemRowStr += ''; } vTmpDate.setDate(vTmpDate.getDate() + 1); } else if(vFormat == 'week') { vNxtDate.setDate(vNxtDate.getDate() + 7); if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vWeekdayColor = "ccccff"; else vWeekdayColor = "ffffff"; if(vNxtDate <= vMaxDate) { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } else { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } vTmpDate.setDate(vTmpDate.getDate() + 7); } else if(vFormat == 'month') { vNxtDate.setFullYear(vTmpDate.getFullYear(), vTmpDate.getMonth(), vMonthDaysArr[vTmpDate.getMonth()]); if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vWeekdayColor = "ccccff"; else vWeekdayColor = "ffffff"; if(vNxtDate <= vMaxDate) { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } else { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } vTmpDate.setDate(vTmpDate.getDate() + 1); while(vTmpDate.getDate() > 1) { vTmpDate.setDate(vTmpDate.getDate() + 1); } } else if(vFormat == 'quarter') { vNxtDate.setDate(vNxtDate.getDate() + 122); if( vTmpDate.getMonth()==0 || vTmpDate.getMonth()==1 || vTmpDate.getMonth()==2 ) vNxtDate.setFullYear(vTmpDate.getFullYear(), 2, 31); else if( vTmpDate.getMonth()==3 || vTmpDate.getMonth()==4 || vTmpDate.getMonth()==5 ) vNxtDate.setFullYear(vTmpDate.getFullYear(), 5, 30); else if( vTmpDate.getMonth()==6 || vTmpDate.getMonth()==7 || vTmpDate.getMonth()==8 ) vNxtDate.setFullYear(vTmpDate.getFullYear(), 8, 30); else if( vTmpDate.getMonth()==9 || vTmpDate.getMonth()==10 || vTmpDate.getMonth()==11 ) vNxtDate.setFullYear(vTmpDate.getFullYear(), 11, 31); if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vWeekdayColor = "ccccff"; else vWeekdayColor = "ffffff"; if(vNxtDate <= vMaxDate) { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } else { vDateRowStr += ''; if( vCurrDate >= vTmpDate && vCurrDate < vNxtDate ) vItemRowStr += ''; else vItemRowStr += ''; } vTmpDate.setDate(vTmpDate.getDate() + 81); while(vTmpDate.getDate() > 1) { vTmpDate.setDate(vTmpDate.getDate() + 1); } } } vRightTable += vDateRowStr + ''; vRightTable += '
' ; vRightTable += JSGantt.formatDateStr(vTmpDate, vDateDisplayFormat) + ' ' + vTmpDate.getHours() + ':00 -' + vTmpDate.getHours() + ':59 ' ; vRightTable += JSGantt.formatDateStr(vTmpDate, vDateDisplayFormat) + ''; var vTmpMonth = vTmpDate.getMonth(); vTmpDate.setDate(vTmpDate.getDate()+6); if (vTmpMonth == vTmpDate.getMonth()) vRightTable += vMonthArr[vTmpDate.getMonth()].substr(0,3) + " "+ vTmpDate.getFullYear(); else vRightTable += vMonthArr[vTmpMonth].substr(0,3) +"-"+vMonthArr[vTmpDate.getMonth()].substr(0,3) + " "+ vTmpDate.getFullYear(); vTmpDate.setDate(vTmpDate.getDate()+1); } else if(vFormat == 'week') { vRightTable += '`'+ vStr + '`'+ vStr + '`'+ vStr + '
' + vTmpDate.getMinutes() + '
  
' + vTmpDate.getHours() + '
  
' + vTmpDate.getDate() + '
 
' + vTmpDate.getDate() + '
  
  
' + (vTmpDate.getMonth()+1) + '/' + vTmpDate.getDate() + '
  
  
' + (vTmpDate.getMonth()+1) + '/' + vTmpDate.getDate() + '
  
  
' + vMonthArr[vTmpDate.getMonth()].substr(0,3) + '
  
  
' + vMonthArr[vTmpDate.getMonth()].substr(0,3) + '
  
  
Qtr. ' + vQuarterArr[vTmpDate.getMonth()] + '
  
  
Qtr. ' + vQuarterArr[vTmpDate.getMonth()] + '
  
  
'; // Draw each row for(i = 0; i < vTaskList.length; i++) { vTmpDate.setFullYear(vMinDate.getFullYear(), vMinDate.getMonth(), vMinDate.getDate()); vTaskStart = vTaskList[i].getStart(); vTaskEnd = vTaskList[i].getEnd(); vNumCols = 0; vID = vTaskList[i].getID(); // vNumUnits = Math.ceil((vTaskList[i].getEnd() - vTaskList[i].getStart()) / (24 * 60 * 60 * 1000)) + 1; vNumUnits = (vTaskList[i].getEnd() - vTaskList[i].getStart()) / (24 * 60 * 60 * 1000) + 1; if (vFormat=='hour') { vNumUnits = (vTaskList[i].getEnd() - vTaskList[i].getStart()) / ( 60 * 1000) + 1; } else if (vFormat=='minute') { vNumUnits = (vTaskList[i].getEnd() - vTaskList[i].getStart()) / ( 60 * 1000) + 1; } if(vTaskList[i].getVisible() == 0) vRightTable += ''; } vMainTable += vRightTable + '
'; vDiv.innerHTML = vMainTable; // Sane defaults for most screens these days. In the worst // case, it will scroll var width = "1000px"; if (typeof window.innerWidth != 'undefined') { // 95% of overall window width width = window.innerWidth * 0.95 + 'px'; } else { width = document.documentElement.clientWidth * 0.95 + 'px'; } vDiv.style.width = width; } }; //this.draw /** * Mouseover behaviour for gantt row * @method mouseOver * @return {Void} */ this.mouseOver = function( pObj, pID, pPos, pType ) { if( pPos == 'right' ) vID = 'child_' + pID; else vID = 'childrow_' + pID; pObj.bgColor = "#ffffaa"; vRowObj = JSGantt.findObj(vID); if (vRowObj) vRowObj.bgColor = "#ffffaa"; }; /** * Mouseout behaviour for gantt row * @method mouseOut * @return {Void} */ this.mouseOut = function( pObj, pID, pPos, pType ) { if( pPos == 'right' ) vID = 'child_' + pID; else vID = 'childrow_' + pID; pObj.bgColor = "#ffffff"; vRowObj = JSGantt.findObj(vID); if (vRowObj) { if( pType == "group") { pObj.bgColor = "#f3f3f3"; vRowObj.bgColor = "#f3f3f3"; } else { pObj.bgColor = "#ffffff"; vRowObj.bgColor = "#ffffff"; } } }; }; //GanttChart /** * @class */ /** * Checks whether browser is IE * * @method isIE */ JSGantt.isIE = function () { if(typeof document.all != 'undefined') {return true;} else {return false;} }; /** * Recursively process task tree ... set min, max dates of parent tasks * and identfy task level. * * @method processRows * @param pList {Array} - Array of all TaskItem Objects in the chart * @param pID {Number} - task ID of the parent to process * @param pRow {Number} - Row in chart * @param pLevel {Number} - Current tree level * @param pOpen {Boolean} * @return void */ JSGantt.processRows = function(pList, pID, pRow, pLevel, pOpen) { var vMinDate = new Date(); var vMaxDate = new Date(); var vMinSet = 0; var vMaxSet = 0; var vList = pList; var vLevel = pLevel; var i = 0; var vNumKid = 0; var vEstSum = 0; var vWorkSum = 0; var vVisible = pOpen; for (i = 0; i < pList.length; i++) { if (pList[i].getParent() == pID) { vVisible = pOpen; pList[i].setVisible(vVisible); if (vVisible == 1 && pList[i].getOpen() == 0) { vVisible = 0; } pList[i].setLevel(vLevel); vNumKid++; if (pList[i].getGroup() == 1) { JSGantt.processRows(vList, pList[i].getID(), i, vLevel+1, vVisible); } if (vMinSet == 0 || pList[i].getStart() < vMinDate) { vMinDate = pList[i].getStart(); vMinSet = 1; } if (vMaxSet == 0 || pList[i].getEnd() > vMaxDate) { vMaxDate = pList[i].getEnd(); vMaxSet = 1; } vWorkSum += pList[i].getWorkVal(); vEstSum += pList[i].getEstVal(); } } if (pRow >= 0) { pList[pRow].setStart(vMinDate); pList[pRow].setEnd(vMaxDate); pList[pRow].setNumKid(vNumKid); if (pList[pRow].getGroup() == 1) { pList[pRow].setWorkVal(vWorkSum); pList[pRow].setEstVal(vEstSum); } if (vEstSum == 0) { pList[pRow].setCompVal(0); } else { pList[pRow].setCompVal(Math.ceil(100 * vWorkSum / vEstSum)); } } }; /** * Determine the minimum date of all tasks and set lower bound based on format * * @method getMinDate * @param pList {Array} - Array of TaskItem Objects * @param pFormat {String} - current format (minute,hour,day...) * @return {Datetime} */ JSGantt.getMinDate = function getMinDate(pList, pFormat) { var vDate = new Date(); vDate.setFullYear(pList[0].getStart().getFullYear(), pList[0].getStart().getMonth(), pList[0].getStart().getDate()); // Parse all Task End dates to find min for(i = 0; i < pList.length; i++) { if(Date.parse(pList[i].getStart()) < Date.parse(vDate)) vDate.setFullYear(pList[i].getStart().getFullYear(), pList[i].getStart().getMonth(), pList[i].getStart().getDate()); } if ( pFormat== 'minute') { vDate.setHours(0); vDate.setMinutes(0); } else if (pFormat == 'hour' ) { vDate.setHours(0); vDate.setMinutes(0); } // Adjust min date to specific format boundaries (first of week or first of month) else if (pFormat=='day') { vDate.setDate(vDate.getDate() - 1); while(vDate.getDay() % 7 > 0) { vDate.setDate(vDate.getDate() - 1); } } else if (pFormat=='week') { while(vDate.getDay() % 7 > 0) { vDate.setDate(vDate.getDate() - 1); } } else if (pFormat=='month') { while(vDate.getDate() > 1) { vDate.setDate(vDate.getDate() - 1); } } else if (pFormat=='quarter') { if( vDate.getMonth()==0 || vDate.getMonth()==1 || vDate.getMonth()==2 ) {vDate.setFullYear(vDate.getFullYear(), 0, 1);} else if( vDate.getMonth()==3 || vDate.getMonth()==4 || vDate.getMonth()==5 ) {vDate.setFullYear(vDate.getFullYear(), 3, 1);} else if( vDate.getMonth()==6 || vDate.getMonth()==7 || vDate.getMonth()==8 ) {vDate.setFullYear(vDate.getFullYear(), 6, 1);} else if( vDate.getMonth()==9 || vDate.getMonth()==10 || vDate.getMonth()==11 ) {vDate.setFullYear(vDate.getFullYear(), 9, 1);} }; return(vDate); }; /** * Used to determine the minimum date of all tasks and set lower bound based on format * * @method getMaxDate * @param pList {Array} - Array of TaskItem Objects * @param pFormat {String} - current format (minute,hour,day...) * @return {Datetime} */ JSGantt.getMaxDate = function (pList, pFormat) { var vDate = new Date(); vDate.setFullYear(pList[0].getEnd().getFullYear(), pList[0].getEnd().getMonth(), pList[0].getEnd().getDate()); // Parse all Task End dates to find max for(i = 0; i < pList.length; i++) { if(Date.parse(pList[i].getEnd()) > Date.parse(vDate)) { //vDate.setFullYear(pList[0].getEnd().getFullYear(), pList[0].getEnd().getMonth(), pList[0].getEnd().getDate()); vDate.setTime(Date.parse(pList[i].getEnd())); } } if (pFormat == 'minute') { vDate.setHours(vDate.getHours() + 1); vDate.setMinutes(59); } if (pFormat == 'hour') { vDate.setHours(vDate.getHours() + 2); } // Adjust max date to specific format boundaries (end of week or end of month) if (pFormat=='day') { vDate.setDate(vDate.getDate() + 1); while(vDate.getDay() % 6 > 0) { vDate.setDate(vDate.getDate() + 1); } } if (pFormat=='week') { //For weeks, what is the last logical boundary? vDate.setDate(vDate.getDate() + 11); while(vDate.getDay() % 6 > 0) { vDate.setDate(vDate.getDate() + 1); } } // Set to last day of current Month if (pFormat=='month') { while(vDate.getDay() > 1) { vDate.setDate(vDate.getDate() + 1); } vDate.setDate(vDate.getDate() - 1); } // Set to last day of current Quarter if (pFormat=='quarter') { if( vDate.getMonth()==0 || vDate.getMonth()==1 || vDate.getMonth()==2 ) vDate.setFullYear(vDate.getFullYear(), 2, 31); else if( vDate.getMonth()==3 || vDate.getMonth()==4 || vDate.getMonth()==5 ) vDate.setFullYear(vDate.getFullYear(), 5, 30); else if( vDate.getMonth()==6 || vDate.getMonth()==7 || vDate.getMonth()==8 ) vDate.setFullYear(vDate.getFullYear(), 8, 30); else if( vDate.getMonth()==9 || vDate.getMonth()==10 || vDate.getMonth()==11 ) vDate.setFullYear(vDate.getFullYear(), 11, 31); } return(vDate); }; /** * Returns an object from the current DOM * * @method findObj * @param theObj {String} - Object name * @param theDoc {Document} - current document (DOM) * @return {Object} */ JSGantt.findObj = function (theObj, theDoc) { var p, i, foundObj; if(!theDoc) {theDoc = document;} if( (p = theObj.indexOf("?")) > 0 && parent.frames.length){ theDoc = parent.frames[theObj.substring(p+1)].document; theObj = theObj.substring(0,p); } if(!(foundObj = theDoc[theObj]) && theDoc.all) {foundObj = theDoc.all[theObj];} for (i=0; !foundObj && i < theDoc.forms.length; i++) {foundObj = theDoc.forms[i][theObj];} for(i=0; !foundObj && theDoc.layers && i < theDoc.layers.length; i++) {foundObj = JSGantt.findObj(theObj,theDoc.layers[i].document);} if(!foundObj && document.getElementById) {foundObj = document.getElementById(theObj);} return foundObj; }; /** * Change display format of current gantt chart * * @method changeFormat * @param pFormat {String} - Current format (minute,hour,day...) * @param ganttObj {GanttChart} - The gantt object * @return {void} */ JSGantt.changeFormat = function(pFormat,ganttObj) { if(ganttObj) { ganttObj.setFormat(pFormat); ganttObj.DrawDependencies(); } else {alert('Chart undefined');}; }; /** * Open/Close and hide/show children of specified task * * @method folder * @param pID {Number} - Task ID * @param ganttObj {GanttChart} - The gantt object * @return {void} */ JSGantt.folder= function (pID,ganttObj) { var vList = ganttObj.getList(); for(i = 0; i < vList.length; i++) { if(vList[i].getID() == pID) { if( vList[i].getOpen() == 1 ) { vList[i].setOpen(0); JSGantt.hide(pID,ganttObj); if (JSGantt.isIE()) {JSGantt.findObj('group_'+pID).innerText = '+';} else {JSGantt.findObj('group_'+pID).textContent = '+';} } else { vList[i].setOpen(1); JSGantt.show(pID, 1, ganttObj); if (JSGantt.isIE()) {JSGantt.findObj('group_'+pID).innerText = '-';} else {JSGantt.findObj('group_'+pID).textContent = '-';} } } } }; /** * Hide children of a task * * @method hide * @param pID {Number} - Task ID * @param ganttObj {GanttChart} - The gantt object * @return {void} */ JSGantt.hide= function (pID,ganttObj) { var vList = ganttObj.getList(); var vID = 0; for(var i = 0; i < vList.length; i++) { if(vList[i].getParent() == pID) { vID = vList[i].getID(); JSGantt.findObj('child_' + vID).style.display = "none"; JSGantt.findObj(ganttObj.getID()+'_childgrid_' + vID).style.display = "none"; vList[i].setVisible(0); if(vList[i].getGroup() == 1) {JSGantt.hide(vID,ganttObj);} } } }; /** * Show children of a task * * @method show * @param pID {Number} - Task ID * @param ganttObj {GanttChart} - The gantt object * @return {void} */ JSGantt.show = function (pID, pTop, ganttObj) { var vList = ganttObj.getList(); var vID = 0; for(var i = 0; i < vList.length; i++) { if(vList[i].getParent() == pID) { vID = vList[i].getID(); if(pTop == 1) { if (JSGantt.isIE()) { // IE; if( JSGantt.findObj('group_'+pID).innerText == '+') { JSGantt.findObj('child_'+vID).style.display = ""; JSGantt.findObj(ganttObj.getID()+'_childgrid_'+vID).style.display = ""; vList[i].setVisible(1); } } else { if( JSGantt.findObj('group_'+pID).textContent == '+') { JSGantt.findObj('child_'+vID).style.display = ""; JSGantt.findObj(ganttObj.getID()+'_childgrid_'+vID).style.display = ""; vList[i].setVisible(1); } } } else { if (JSGantt.isIE()) { // IE; if( JSGantt.findObj('group_'+pID).innerText == '-') { JSGantt.findObj('child_'+vID).style.display = ""; JSGantt.findObj(ganttObj.getID()+'_childgrid_'+vID).style.display = ""; vList[i].setVisible(1); } } else { if( JSGantt.findObj('group_'+pID).textContent == '-') { JSGantt.findObj('child_'+vID).style.display = ""; JSGantt.findObj(ganttObj.getID()+'_childgrid_'+vID).style.display = ""; vList[i].setVisible(1); } } } if(vList[i].getGroup() == 1) {JSGantt.show(vID, 0,ganttObj);} } } }; /** * Handles click events on task name, currently opens a new window * * @method taskLink * @param pRef {String} - URL for window * @param pFeatures (String) - Feature string (e.g., "status=1,toolbar=1") * @return {void} */ JSGantt.taskLink = function(pRef,pFeatures) { if (pFeatures && pFeatures.length != 0) { vFeatures = pFeatures } else { vFeatures = "height=200,width=300" } var OpenWindow=window.open(pRef, "newwin", vFeatures); }; /** * Parse dates based on gantt date format setting as defined in JSGantt.GanttChart.setDateInputFormat() * * @method parseDateStr * @param pDateStr {String} - A string that contains the date (i.e. "01/01/09") * @param pFormatStr {String} - The date format (mm/dd/yyyy,dd/mm/yyyy,yyyy-mm-dd) * @return {Datetime} */ JSGantt.parseDateStr = function(pDateStr,pFormatStr) { var vDate =new Date(); vDate.setTime( Date.parse(pDateStr)); switch(pFormatStr) { case 'mm/dd/yyyy': var vDateParts = pDateStr.split('/'); vDate.setFullYear(parseInt(vDateParts[2], 10), parseInt(vDateParts[0], 10) - 1, parseInt(vDateParts[1], 10)); break; case 'dd/mm/yyyy': var vDateParts = pDateStr.split('/'); vDate.setFullYear(parseInt(vDateParts[2], 10), parseInt(vDateParts[1], 10) - 1, parseInt(vDateParts[0], 10)); break; case 'yyyy-mm-dd': var vDateParts = pDateStr.split('-'); vDate.setFullYear(parseInt(vDateParts[0], 10), parseInt(vDateParts[1], 10) - 1, parseInt(vDateParts[2], 10)); break; } return(vDate); }; /** * Display a formatted date based on gantt date format setting as defined in JSGantt.GanttChart.setDateDisplayFormat() * * @method formatDateStr * @param pDate {Date} - A javascript date object * @param pFormatStr {String} - The date format (mm/dd/yyyy,dd/mm/yyyy,yyyy-mm-dd...) * @return {String} */ JSGantt.formatDateStr = function(pDate,pFormatStr) { vYear4Str = pDate.getFullYear() + ''; vYear2Str = vYear4Str.substring(2,4); vMonthStr = (pDate.getMonth()+1) + ''; vDayStr = pDate.getDate() + ''; var vDateStr = ""; switch(pFormatStr) { case 'mm/dd/yyyy': return( vMonthStr + '/' + vDayStr + '/' + vYear4Str ); case 'dd/mm/yyyy': return( vDayStr + '/' + vMonthStr + '/' + vYear4Str ); case 'yyyy-mm-dd': return( vYear4Str + '-' + vMonthStr + '-' + vDayStr ); case 'mm/dd/yy': return( vMonthStr + '/' + vDayStr + '/' + vYear2Str ); case 'dd/mm/yy': return( vDayStr + '/' + vMonthStr + '/' + vYear2Str ); case 'yy-mm-dd': return( vYear2Str + '-' + vMonthStr + '-' + vDayStr ); case 'mm/dd': return( vMonthStr + '/' + vDayStr ); case 'dd/mm': return( vDayStr + '/' + vMonthStr ); } }; /** * Parse an external XML file containing task items. * * @method parseXML * @param ThisFile {String} - URL to XML file * @param pGanttVar {Gantt} - Gantt object * @return {void} */ JSGantt.parseXML = function(ThisFile,pGanttVar){ var is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1; // Is this Chrome try { //Internet Explorer xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); } catch(e) { try { //Firefox, Mozilla, Opera, Chrome etc. if (is_chrome==false) { xmlDoc=document.implementation.createDocument("","",null); } } catch(e) { alert(e.message); return; } } if (is_chrome==false) { // can't use xmlDoc.load in chrome at the moment xmlDoc.async=false; xmlDoc.load(ThisFile); // we can use loadxml JSGantt.AddXMLTask(pGanttVar); xmlDoc=null; // a little tidying Task = null; } else { JSGantt.ChromeLoadXML(ThisFile,pGanttVar); ta=null; // a little tidying } }; /** * Add a task based on parsed XML doc * * @method AddXMLTask * @param pGanttVar {Gantt} - Gantt object * @return {void} */ JSGantt.AddXMLTask = function(pGanttVar){ Task=xmlDoc.getElementsByTagName("task"); var n = xmlDoc.documentElement.childNodes.length; // the number of tasks. IE gets this right, but mozilla add extra ones (Whitespace) for(var i=0;i/gi); var n = ta.length; // the number of tasks. for(var i=1;i/i); if(te.length> 2){var pID=te[1];} else {var pID = 0;} pID *= 1; var te = Task.split(//i); if(te.length> 2){var pName=te[1];} else {var pName = "No Task Name";} var te = Task.split(//i); if(te.length> 2){var pStart=te[1];} else {var pStart = "";} var te = Task.split(//i); if(te.length> 2){var pEnd=te[1];} else {var pEnd = "";} var te = Task.split(//i); if(te.length> 2){var pColor=te[1];} else {var pColor = '0000ff';} var te = Task.split(//i); if(te.length> 2){var pLink=te[1];} else {var pLink = "";} var te = Task.split(//i); if(te.length> 2){var pMile=te[1];} else {var pMile = 0;} pMile *= 1; var te = Task.split(//i); if(te.length> 2){var pRes=te[1];} else {var pRes = "";} var te = Task.split(//i); if(te.length> 2){var pComp=te[1];} else {var pComp = 0;} pComp *= 1; var te = Task.split(//i); if(te.length> 2){var pGroup=te[1];} else {var pGroup = 0;} pGroup *= 1; var te = Task.split(//i); if(te.length> 2){var pParent=te[1];} else {var pParent = 0;} pParent *= 1; var te = Task.split(//i); if(te.length> 2){var pOpen=te[1];} else {var pOpen = 1;} pOpen *= 1; var te = Task.split(//i); if(te.length> 2){var pDepend=te[1];} else {var pDepend = "";} //pDepend *= 1; if (pDepend.length==0){pDepend=''} // need this to draw the dependency lines var te = Task.split(//i); if(te.length> 2){var pCaption=te[1];} else {var pCaption = "";} // Finally add the task pGanttVar.AddTaskItem(new JSGantt.TaskItem(pID , pName, pStart, pEnd, pColor, pLink, pMile, pRes, pComp, pGroup, pParent, pOpen, pDepend,pCaption )); }; }; }; /** * Used for benchmarking performace * * @method benchMark * @param pItem {TaskItem} - TaskItem object * @return {void} */ JSGantt.benchMark = function(pItem){ var vEndTime=new Date().getTime(); alert(pItem + ': Elapsed time: '+((vEndTime-vBenchTime)/1000)+' seconds.'); vBenchTime=new Date().getTime(); }; trac-jsgantt-0.9+r11145/0.11/tracjsgantt/htdocs/jsgantt.compressed.js0000644000175000017500000013270511477252436023335 0ustar wmbwmbvar JSGantt;if(!JSGantt)JSGantt={};var vTimeout=0;var vBenchTime=new Date().getTime();JSGantt.TaskItem=function(pID,pName,pStart,pEnd,pColor,pLink,pMile,pRes,pComp,pGroup,pParent,pOpen,pDepend,pCaption){var vID=pID;var vName=pName;var vStart=new Date();var vEnd=new Date();var vColor=pColor;var vLink=pLink;var vMile=pMile;var vRes=pRes;var vComp=pComp;var vGroup=pGroup;var vParent=pParent;var vOpen=pOpen;var vDepend=pDepend;var vCaption=pCaption;var vDuration='';var vLevel=0;var vNumKid=0;var vVisible=1;var x1,y1,x2,y2;if(vGroup!=1){vStart=JSGantt.parseDateStr(pStart,g.getDateInputFormat());vEnd=JSGantt.parseDateStr(pEnd,g.getDateInputFormat())}this.getID=function(){return vID};this.getName=function(){return vName};this.getStart=function(){return vStart};this.getEnd=function(){return vEnd};this.getColor=function(){return vColor};this.getLink=function(){return vLink};this.getMile=function(){return vMile};this.getDepend=function(){if(vDepend)return vDepend;else return null};this.getCaption=function(){if(vCaption)return vCaption;else return''};this.getResource=function(){if(vRes)return vRes;else return' '};this.getCompVal=function(){if(vComp)return vComp;else return 0};this.getCompStr=function(){if(vComp)return vComp+'%';else return''};this.getDuration=function(vFormat){if(vMile)vDuration='-';else if(vFormat=='hour'){tmpPer=Math.ceil((this.getEnd()-this.getStart())/(60*60*1000));if(tmpPer==1)vDuration='1 Hour';else vDuration=tmpPer+' Hours'}else if(vFormat=='minute'){tmpPer=Math.ceil((this.getEnd()-this.getStart())/(60*1000));if(tmpPer==1)vDuration='1 Minute';else vDuration=tmpPer+' Minutes'}else{tmpPer=Math.ceil((this.getEnd()-this.getStart())/(24*60*60*1000)+1);if(tmpPer==1)vDuration='1 Day';else vDuration=tmpPer+' Days'}return(vDuration)};this.getParent=function(){return vParent};this.getGroup=function(){return vGroup};this.getOpen=function(){return vOpen};this.getLevel=function(){return vLevel};this.getNumKids=function(){return vNumKid};this.getStartX=function(){return x1};this.getStartY=function(){return y1};this.getEndX=function(){return x2};this.getEndY=function(){return y2};this.getVisible=function(){return vVisible};this.setDepend=function(pDepend){vDepend=pDepend};this.setStart=function(pStart){vStart=pStart};this.setEnd=function(pEnd){vEnd=pEnd};this.setLevel=function(pLevel){vLevel=pLevel};this.setNumKid=function(pNumKid){vNumKid=pNumKid};this.setCompVal=function(pCompVal){vComp=pCompVal};this.setStartX=function(pX){x1=pX};this.setStartY=function(pY){y1=pY};this.setEndX=function(pX){x2=pX};this.setEndY=function(pY){y2=pY};this.setOpen=function(pOpen){vOpen=pOpen};this.setVisible=function(pVisible){vVisible=pVisible}};JSGantt.GanttChart=function(pGanttVar,pDiv,pFormat){var vGanttVar=pGanttVar;var vDiv=pDiv;var vFormat=pFormat;var vShowRes=1;var vShowDur=1;var vShowComp=1;var vShowStartDate=1;var vShowEndDate=1;var vDateInputFormat="mm/dd/yyyy";var vDateDisplayFormat="mm/dd/yy";var vNumUnits=0;var vCaptionType;var vDepId=1;var vTaskList=new Array();var vFormatArr=new Array("day","week","month","quarter");var vQuarterArr=new Array(1,1,1,2,2,2,3,3,3,4,4,4);var vMonthDaysArr=new Array(31,28,31,30,31,30,31,31,30,31,30,31);var vMonthArr=new Array("January","February","March","April","May","June","July","August","September","October","November","December");this.setFormatArr=function(){vFormatArr=new Array();for(var i=0;i4){vFormatArr.length=4}};this.setShowRes=function(pShow){vShowRes=pShow};this.setShowDur=function(pShow){vShowDur=pShow};this.setShowComp=function(pShow){vShowComp=pShow};this.setShowStartDate=function(pShow){vShowStartDate=pShow};this.setShowEndDate=function(pShow){vShowEndDate=pShow};this.setDateInputFormat=function(pShow){vDateInputFormat=pShow};this.setDateDisplayFormat=function(pShow){vDateDisplayFormat=pShow};this.setCaptionType=function(pType){vCaptionType=pType};this.setFormat=function(pFormat){vFormat=pFormat;this.Draw()};this.getShowRes=function(){return vShowRes};this.getShowDur=function(){return vShowDur};this.getShowComp=function(){return vShowComp};this.getShowStartDate=function(){return vShowStartDate};this.getShowEndDate=function(){return vShowEndDate};this.getDateInputFormat=function(){return vDateInputFormat};this.getDateDisplayFormat=function(){return vDateDisplayFormat};this.getCaptionType=function(){return vCaptionType};this.CalcTaskXY=function(){var vList=this.getList();var vTaskDiv;var vParDiv;var vLeft,vTop,vHeight,vWidth;for(i=0;i0){JSGantt.processRows(vTaskList,0,-1,1,1);vMinDate=JSGantt.getMinDate(vTaskList,vFormat);vMaxDate=JSGantt.getMaxDate(vTaskList,vFormat);if(vFormat=='day'){vColWidth=18;vColUnit=1}else if(vFormat=='week'){vColWidth=37;vColUnit=7}else if(vFormat=='month'){vColWidth=37;vColUnit=30}else if(vFormat=='quarter'){vColWidth=60;vColUnit=90}else if(vFormat=='hour'){vColWidth=18;vColUnit=1}else if(vFormat=='minute'){vColWidth=18;vColUnit=1}vNumDays=(Date.parse(vMaxDate)-Date.parse(vMinDate))/(24*60*60*1000);vNumUnits=vNumDays/vColUnit;vChartWidth=vNumUnits*vColWidth+1;vDayWidth=(vColWidth/vColUnit)+(1/vColUnit);vMainTable=''+'';vMainTable+=vLeftTable;vRightTable='
';if(vShowRes!=1)vNameWidth+=vStatusWidth;if(vShowDur!=1)vNameWidth+=vStatusWidth;if(vShowComp!=1)vNameWidth+=vStatusWidth;if(vShowStartDate!=1)vNameWidth+=vStatusWidth;if(vShowEndDate!=1)vNameWidth+=vStatusWidth;vLeftTable='
'+''+' '+' ';if(vShowRes==1)vLeftTable+=' ';if(vShowDur==1)vLeftTable+=' ';if(vShowComp==1)vLeftTable+=' ';if(vShowStartDate==1)vLeftTable+=' ';if(vShowEndDate==1)vLeftTable+=' ';vLeftTable+=''+' '+' ';if(vShowRes==1)vLeftTable+=' ';if(vShowDur==1)vLeftTable+=' ';if(vShowComp==1)vLeftTable+=' ';if(vShowStartDate==1)vLeftTable+=' ';if(vShowEndDate==1)vLeftTable+=' ';vLeftTable+='';for(i=0;i';vLeftTable+=' '+' ';if(vShowRes==1)vLeftTable+=' ';if(vShowDur==1)vLeftTable+=' ';if(vShowComp==1)vLeftTable+=' ';if(vShowStartDate==1)vLeftTable+=' ';if(vShowEndDate==1)vLeftTable+=' ';vLeftTable+=''}vLeftTable+=''+'
ResourceDuration% Comp.Start DateEnd Date
 ';for(j=1;j';if(vTaskList[i].getGroup()){if(vTaskList[i].getOpen()==1)vLeftTable+=' ';else vLeftTable+='+ '}else{vLeftTable+='   '}vLeftTable+=' '+vTaskList[i].getName()+''+vTaskList[i].getResource()+''+vTaskList[i].getDuration(vFormat)+''+vTaskList[i].getCompStr()+''+JSGantt.formatDateStr(vTaskList[i].getStart(),vDateDisplayFormat)+''+JSGantt.formatDateStr(vTaskList[i].getEnd(),vDateDisplayFormat)+'
  Powered by jsGantt      Format:';if(vFormatArr.join().indexOf("minute")!=-1){if(vFormat=='minute')vLeftTable+='Minute';else vLeftTable+='Minute'}if(vFormatArr.join().indexOf("hour")!=-1){if(vFormat=='hour')vLeftTable+='Hour';else vLeftTable+='Hour'}if(vFormatArr.join().indexOf("day")!=-1){if(vFormat=='day')vLeftTable+='Day';else vLeftTable+='Day'}if(vFormatArr.join().indexOf("week")!=-1){if(vFormat=='week')vLeftTable+='Week';else vLeftTable+='Week'}if(vFormatArr.join().indexOf("month")!=-1){if(vFormat=='month')vLeftTable+='Month';else vLeftTable+='Month'}if(vFormatArr.join().indexOf("quarter")!=-1){if(vFormat=='quarter')vLeftTable+='Quarter';else vLeftTable+='Quarter'}vLeftTable+='
'+'
'+''+'';vTmpDate.setFullYear(vMinDate.getFullYear(),vMinDate.getMonth(),vMinDate.getDate());vTmpDate.setHours(0);vTmpDate.setMinutes(0);while(Date.parse(vTmpDate)<=Date.parse(vMaxDate)){vStr=vTmpDate.getFullYear()+'';vStr=vStr.substring(2,4);if(vFormat=='minute'){vRightTable+='';vTmpDate.setHours(vTmpDate.getHours()+1)}if(vFormat=='hour'){vRightTable+='';vTmpDate.setDate(vTmpDate.getDate()+1)}if(vFormat=='day'){vRightTable+='';vTmpDate.setDate(vTmpDate.getDate()+1)}else if(vFormat=='week'){vRightTable+='';vTmpDate.setDate(vTmpDate.getDate()+7)}else if(vFormat=='month'){vRightTable+='';vTmpDate.setDate(vTmpDate.getDate()+1);while(vTmpDate.getDate()>1){vTmpDate.setDate(vTmpDate.getDate()+1)}}else if(vFormat=='quarter'){vRightTable+='';vTmpDate.setDate(vTmpDate.getDate()+81);while(vTmpDate.getDate()>1){vTmpDate.setDate(vTmpDate.getDate()+1)}}}vRightTable+='';vTmpDate.setFullYear(vMinDate.getFullYear(),vMinDate.getMonth(),vMinDate.getDate());vNxtDate.setFullYear(vMinDate.getFullYear(),vMinDate.getMonth(),vMinDate.getDate());vNumCols=0;while(Date.parse(vTmpDate)<=Date.parse(vMaxDate)){if(vFormat=='minute'){if(vTmpDate.getMinutes()==0)vWeekdayColor="ccccff";else vWeekdayColor="ffffff";vDateRowStr+='';vItemRowStr+='';vTmpDate.setMinutes(vTmpDate.getMinutes()+1)}else if(vFormat=='hour'){if(vTmpDate.getHours()==0)vWeekdayColor="ccccff";else vWeekdayColor="ffffff";vDateRowStr+='';vItemRowStr+='';vTmpDate.setHours(vTmpDate.getHours()+1)}else if(vFormat=='day'){if(JSGantt.formatDateStr(vCurrDate,'mm/dd/yyyy')==JSGantt.formatDateStr(vTmpDate,'mm/dd/yyyy')){vWeekdayColor="ccccff";vWeekendColor="9999ff";vWeekdayGColor="bbbbff";vWeekendGColor="8888ff"}else{vWeekdayColor="ffffff";vWeekendColor="cfcfcf";vWeekdayGColor="f3f3f3";vWeekendGColor="c3c3c3"}if(vTmpDate.getDay()%6==0){vDateRowStr+='';vItemRowStr+=''}else{vDateRowStr+='';if(JSGantt.formatDateStr(vCurrDate,'mm/dd/yyyy')==JSGantt.formatDateStr(vTmpDate,'mm/dd/yyyy'))vItemRowStr+='';else vItemRowStr+=''}vTmpDate.setDate(vTmpDate.getDate()+1)}else if(vFormat=='week'){vNxtDate.setDate(vNxtDate.getDate()+7);if(vCurrDate>=vTmpDate&&vCurrDate
'+(vTmpDate.getMonth()+1)+'/'+vTmpDate.getDate()+'
';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}else{vDateRowStr+='';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}vTmpDate.setDate(vTmpDate.getDate()+7)}else if(vFormat=='month'){vNxtDate.setFullYear(vTmpDate.getFullYear(),vTmpDate.getMonth(),vMonthDaysArr[vTmpDate.getMonth()]);if(vCurrDate>=vTmpDate&&vCurrDate
'+vMonthArr[vTmpDate.getMonth()].substr(0,3)+'
';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}else{vDateRowStr+='';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}vTmpDate.setDate(vTmpDate.getDate()+1);while(vTmpDate.getDate()>1){vTmpDate.setDate(vTmpDate.getDate()+1)}}else if(vFormat=='quarter'){vNxtDate.setDate(vNxtDate.getDate()+122);if(vTmpDate.getMonth()==0||vTmpDate.getMonth()==1||vTmpDate.getMonth()==2)vNxtDate.setFullYear(vTmpDate.getFullYear(),2,31);else if(vTmpDate.getMonth()==3||vTmpDate.getMonth()==4||vTmpDate.getMonth()==5)vNxtDate.setFullYear(vTmpDate.getFullYear(),5,30);else if(vTmpDate.getMonth()==6||vTmpDate.getMonth()==7||vTmpDate.getMonth()==8)vNxtDate.setFullYear(vTmpDate.getFullYear(),8,30);else if(vTmpDate.getMonth()==9||vTmpDate.getMonth()==10||vTmpDate.getMonth()==11)vNxtDate.setFullYear(vTmpDate.getFullYear(),11,31);if(vCurrDate>=vTmpDate&&vCurrDate
Qtr. '+vQuarterArr[vTmpDate.getMonth()]+'
';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}else{vDateRowStr+='';if(vCurrDate>=vTmpDate&&vCurrDate
  
';else vItemRowStr+='
'}vTmpDate.setDate(vTmpDate.getDate()+81);while(vTmpDate.getDate()>1){vTmpDate.setDate(vTmpDate.getDate()+1)}}}vRightTable+=vDateRowStr+'';vRightTable+='
';vRightTable+=JSGantt.formatDateStr(vTmpDate,vDateDisplayFormat)+' '+vTmpDate.getHours()+':00 -'+vTmpDate.getHours()+':59 ';vRightTable+=JSGantt.formatDateStr(vTmpDate,vDateDisplayFormat)+''+JSGantt.formatDateStr(vTmpDate,vDateDisplayFormat.substring(0,5))+' - ';vTmpDate.setDate(vTmpDate.getDate()+6);vRightTable+=JSGantt.formatDateStr(vTmpDate,vDateDisplayFormat)+'`'+vStr+'`'+vStr+'`'+vStr+'
'+vTmpDate.getMinutes()+'
  
'+vTmpDate.getHours()+'
  
'+vTmpDate.getDate()+'
 
'+vTmpDate.getDate()+'
  
  
  
'+(vTmpDate.getMonth()+1)+'/'+vTmpDate.getDate()+'
  
  
'+vMonthArr[vTmpDate.getMonth()].substr(0,3)+'
  
  
Qtr. '+vQuarterArr[vTmpDate.getMonth()]+'
  
';for(i=0;i
'}vMainTable+=vRightTable+'
';vDiv.innerHTML=vMainTable}};this.mouseOver=function(pObj,pID,pPos,pType){if(pPos=='right')vID='child_'+pID;else vID='childrow_'+pID;pObj.bgColor="#ffffaa";vRowObj=JSGantt.findObj(vID);if(vRowObj)vRowObj.bgColor="#ffffaa"};this.mouseOut=function(pObj,pID,pPos,pType){if(pPos=='right')vID='child_'+pID;else vID='childrow_'+pID;pObj.bgColor="#ffffff";vRowObj=JSGantt.findObj(vID);if(vRowObj){if(pType=="group"){pObj.bgColor="#f3f3f3";vRowObj.bgColor="#f3f3f3"}else{pObj.bgColor="#ffffff";vRowObj.bgColor="#ffffff"}}}};JSGantt.isIE=function(){if(typeof document.all!='undefined'){return true}else{return false}};JSGantt.processRows=function(pList,pID,pRow,pLevel,pOpen){var vMinDate=new Date();var vMaxDate=new Date();var vMinSet=0;var vMaxSet=0;var vList=pList;var vLevel=pLevel;var i=0;var vNumKid=0;var vCompSum=0;var vVisible=pOpen;for(i=0;ivMaxDate){vMaxDate=pList[i].getEnd();vMaxSet=1};vCompSum+=pList[i].getCompVal()}}if(pRow>=0){pList[pRow].setStart(vMinDate);pList[pRow].setEnd(vMaxDate);pList[pRow].setNumKid(vNumKid);pList[pRow].setCompVal(Math.ceil(vCompSum/vNumKid))}};JSGantt.getMinDate=function getMinDate(pList,pFormat){var vDate=new Date();vDate.setFullYear(pList[0].getStart().getFullYear(),pList[0].getStart().getMonth(),pList[0].getStart().getDate());for(i=0;i0){vDate.setDate(vDate.getDate()-1)}}else if(pFormat=='week'){vDate.setDate(vDate.getDate()-7);while(vDate.getDay()%7>0){vDate.setDate(vDate.getDate()-1)}}else if(pFormat=='month'){while(vDate.getDate()>1){vDate.setDate(vDate.getDate()-1)}}else if(pFormat=='quarter'){if(vDate.getMonth()==0||vDate.getMonth()==1||vDate.getMonth()==2){vDate.setFullYear(vDate.getFullYear(),0,1)}else if(vDate.getMonth()==3||vDate.getMonth()==4||vDate.getMonth()==5){vDate.setFullYear(vDate.getFullYear(),3,1)}else if(vDate.getMonth()==6||vDate.getMonth()==7||vDate.getMonth()==8){vDate.setFullYear(vDate.getFullYear(),6,1)}else if(vDate.getMonth()==9||vDate.getMonth()==10||vDate.getMonth()==11){vDate.setFullYear(vDate.getFullYear(),9,1)}};return(vDate)};JSGantt.getMaxDate=function(pList,pFormat){var vDate=new Date();vDate.setFullYear(pList[0].getEnd().getFullYear(),pList[0].getEnd().getMonth(),pList[0].getEnd().getDate());for(i=0;iDate.parse(vDate)){vDate.setTime(Date.parse(pList[i].getEnd()))}}if(pFormat=='minute'){vDate.setHours(vDate.getHours()+1);vDate.setMinutes(59)}if(pFormat=='hour'){vDate.setHours(vDate.getHours()+2)}if(pFormat=='day'){vDate.setDate(vDate.getDate()+1);while(vDate.getDay()%6>0){vDate.setDate(vDate.getDate()+1)}}if(pFormat=='week'){vDate.setDate(vDate.getDate()+11);while(vDate.getDay()%6>0){vDate.setDate(vDate.getDate()+1)}}if(pFormat=='month'){while(vDate.getDay()>1){vDate.setDate(vDate.getDate()+1)}vDate.setDate(vDate.getDate()-1)}if(pFormat=='quarter'){if(vDate.getMonth()==0||vDate.getMonth()==1||vDate.getMonth()==2)vDate.setFullYear(vDate.getFullYear(),2,31);else if(vDate.getMonth()==3||vDate.getMonth()==4||vDate.getMonth()==5)vDate.setFullYear(vDate.getFullYear(),5,30);else if(vDate.getMonth()==6||vDate.getMonth()==7||vDate.getMonth()==8)vDate.setFullYear(vDate.getFullYear(),8,30);else if(vDate.getMonth()==9||vDate.getMonth()==10||vDate.getMonth()==11)vDate.setFullYear(vDate.getFullYear(),11,31)}return(vDate)};JSGantt.findObj=function(theObj,theDoc){var p,i,foundObj;if(!theDoc){theDoc=document}if((p=theObj.indexOf("?"))>0&&parent.frames.length){theDoc=parent.frames[theObj.substring(p+1)].document;theObj=theObj.substring(0,p)}if(!(foundObj=theDoc[theObj])&&theDoc.all){foundObj=theDoc.all[theObj]}for(i=0;!foundObj&&i-1;try{xmlDoc=new ActiveXObject("Microsoft.XMLDOM")}catch(e){try{if(is_chrome==false){xmlDoc=document.implementation.createDocument("","",null)}}catch(e){alert(e.message);return}}if(is_chrome==false){xmlDoc.async=false;xmlDoc.load(ThisFile);JSGantt.AddXMLTask(pGanttVar);xmlDoc=null;Task=null}else{JSGantt.ChromeLoadXML(ThisFile,pGanttVar);ta=null}};JSGantt.AddXMLTask=function(pGanttVar){Task=xmlDoc.getElementsByTagName("task");var n=xmlDoc.documentElement.childNodes.length;for(var i=0;i/gi);var n=ta.length;for(var i=1;i/i);if(te.length>2){var pID=te[1]}else{var pID=0}pID*=1;var te=Task.split(//i);if(te.length>2){var pName=te[1]}else{var pName="No Task Name"}var te=Task.split(//i);if(te.length>2){var pStart=te[1]}else{var pStart=""}var te=Task.split(//i);if(te.length>2){var pEnd=te[1]}else{var pEnd=""}var te=Task.split(//i);if(te.length>2){var pColor=te[1]}else{var pColor='0000ff'}var te=Task.split(//i);if(te.length>2){var pLink=te[1]}else{var pLink=""}var te=Task.split(//i);if(te.length>2){var pMile=te[1]}else{var pMile=0}pMile*=1;var te=Task.split(//i);if(te.length>2){var pRes=te[1]}else{var pRes=""}var te=Task.split(//i);if(te.length>2){var pComp=te[1]}else{var pComp=0}pComp*=1;var te=Task.split(//i);if(te.length>2){var pGroup=te[1]}else{var pGroup=0}pGroup*=1;var te=Task.split(//i);if(te.length>2){var pParent=te[1]}else{var pParent=0}pParent*=1;var te=Task.split(//i);if(te.length>2){var pOpen=te[1]}else{var pOpen=1}pOpen*=1;var te=Task.split(//i);if(te.length>2){var pDepend=te[1]}else{var pDepend=""}if(pDepend.length==0){pDepend=''}var te=Task.split(//i);if(te.length>2){var pCaption=te[1]}else{var pCaption=""}pGanttVar.AddTaskItem(new JSGantt.TaskItem(pID,pName,pStart,pEnd,pColor,pLink,pMile,pRes,pComp,pGroup,pParent,pOpen,pDepend,pCaption))}}};JSGantt.benchMark=function(pItem){var vEndTime=new Date().getTime();alert(pItem+': Elapsed time: '+((vEndTime-vBenchTime)/1000)+' seconds.');vBenchTime=new Date().getTime()};