def get_vacation_correction (db, user, ctype = -1, date = None) : """ Get latest absolute vacation_correction. Special handling of ctype: None means ctype 'None' while -1 means "don't care, search for *any* ctype". Note that roundups interface for searching specifies -1 when searching for an empty link.... """ if date is None : date = Date ('.') dt = ";%s" % date.pretty (common.ymd) d = dict \ ( user = user , absolute = True , date = dt ) # If no ctype given, try to get dyn. user record on date and use # ctype from there. If not found we simply search for the latest # vacation correction before date. if ctype == -1 : dyn = user_dynamic.get_user_dynamic (db, user, date) if dyn : ctype = dyn.contract_type if ctype != -1 : d ['contract_type'] = ctype if ctype is None : d ['contract_type'] = '-1' # roundup: -1 means search empty vcs = db.vacation_correction.filter (None, d, sort = [('-', 'date')]) if not vcs : return for id in vcs : vc = db.vacation_correction.getnode (id) if ctype == -1 or vc.contract_type == ctype : return vc
def get_user_dynamic (db, user, date) : """ Get a user_dynamic record by user and date. Return None if no record could be found. """ user = str (user) date = Date (date) # last_dynamic: Simple one-element cache as attribute of db try : if ( db.last_dynamic and db.last_dynamic.user == user and db.last_dynamic.valid_from < date and ( not db.last_dynamic.valid_to or db.last_dynamic.valid_to > date ) ) : return db.last_dynamic except AttributeError : db.last_dynamic = None def last_dynamic_clear (db) : db.last_dynamic = None db.registerClearCacheCallback (last_dynamic_clear, db) ids = db.user_dynamic.filter \ ( None, dict (user = user, valid_from = date.pretty (';%Y-%m-%d')) , group = ('-', 'valid_from') ) if ids : db.last_dynamic = db.user_dynamic.getnode (ids [0]) if not db.last_dynamic.valid_to or db.last_dynamic.valid_to > date : return db.last_dynamic return None
def check_start_end_duration \ (date, start, end, duration, new_values, dist = 0) : """ either duration or both start/end must be set but not both of duration/end set duration from start/end if duration empty set end from start/duration if end empty Note: We are using naive times (with timezone 0) here, this means we can safely use date.pretty for converting back to string. """ dstart = dend = None if dist : check_duration (dist) if start and ":" not in start : start = start + ":00" if end and ":" not in end : end = end + ":00" if 'end' in new_values : if not start : attr = _ ('start') raise Reject, _ (''"%(attr)s must be specified") % locals () if 'duration' in new_values : raise Reject, _ (''"Either specify duration or start/end") dstart, dend, sp, ep, dur = check_timestamps (start, end, date) duration = dur new_values ['duration'] = duration new_values ['start'] = sp new_values ['end'] = ep else : check_duration (duration, 24) if 'duration' in new_values : new_values ['duration'] = duration if start : if 'start' in new_values or 'duration' in new_values : minutes = duration * 60 hours = int (duration % 60) minutes = minutes - hours * 60 ds = Date (start, offset = 0) t = (ds + Interval ('%d:%d' % (hours, minutes))).pretty \ (hour_format) if duration > 0 and t == '00:00' : t = '24:00' dstart, dend, sp, ep, dur = check_timestamps (start, t, date) assert dur == duration new_values ['start'] = sp new_values ['end'] = ep if dist and dist < duration : duration -= dist if start : hours = int (dist) minutes = (dist - hours) * 60 if not dstart : dstart = Date (start, offset = 0) dstart = dstart + Interval ('%d:%d' % (hours, minutes)) new_values ['start'] = dstart.pretty (hour_format) new_values ['duration'] = duration return dstart, dend
def check_timestamps (start, end, date) : t = end if end == '24:00' : t = '00:00' dstart = Date (start, offset = 0) dend = Date (t, offset = 0) dstart.year = dend.year = date.year dstart.month = dend.month = date.month dstart.day = dend.day = date.day if end == '24:00' : dend += common.day dend.hours = dend.seconds = dend.minutes = 0 if dstart > dend : raise Reject, _ ("start and end must be on same day and start <= end.") if dstart.timestamp () % 900 or dend.timestamp () % 900 : raise Reject, _ ("Times must be given in quarters of an hour") dur = (dend - dstart).as_seconds () / 3600. ep = dend.pretty (hour_format) if end == '24:00' : assert ep == '00:00' ep = end return dstart, dend, dstart.pretty (hour_format), ep, dur
def remaining_vacation \ (db, user, ctype = -1, date = None, cons = None, to_eoy = True) : """ Compute remaining vacation on the given date """ if date is None : date = Date ('.') pdate = date.pretty (common.ymd) if ctype == -1 : ctype = _get_ctype (db, user, date) if ctype == -1 : return vac = None try : vac = db.rem_vac_cache.get ((user, ctype, pdate, to_eoy)) except AttributeError : def vac_clear_cache (db) : db.rem_vac_cache = {} db.registerClearCacheCallback (vac_clear_cache, db) db.rem_vac_cache = {} if vac is not None : return vac vc = get_vacation_correction (db, user, ctype, date) if not vc : return ed = next_yearly_vacation_date (db, user, ctype, date) if not to_eoy : ed = min (ed, date) if cons is None : cons = consolidated_vacation (db, user, ctype, date, vc, to_eoy) vac = cons vac -= vacation_time_sum (db, user, ctype, vc.date, ed) # All vacation_correction records up to date but starting with one # day later (otherwise we'll find the absolute correction) # Also one day *earlier* than ed for the same reason. dt = common.pretty_range (vc.date + common.day, ed - common.day) d = dict (user = user, date = dt) if ctype is not None : d ['contract_type'] = ctype ds = [('+', 'date')] vcs = db.vacation_correction.filter (None, d, sort = ds) for vcid in vcs : vc = db.vacation_correction.getnode (vcid) if vc.contract_type != ctype : continue assert not vc.absolute vac += vc.days db.rem_vac_cache [(user, ctype, pdate, to_eoy)] = vac return vac
def approvals_pending (db, request, userlist) : try : db = db._db except AttributeError : pass pending = {} submitted = db.daily_record_status.lookup ('submitted') spec = copy (request.filterspec) filter = request.filterspec editdict = {':template' : 'edit', ':filter' : 'user,date'} now = Date ('.') for u in userlist : find_user = dict (user = u, status = submitted) fdate = None last_frozen = db.daily_record_freeze.filter \ ( None , dict (user = u, date = now.pretty (';%Y-%m-%d'), frozen = True) , group = [('-', 'date')] ) if last_frozen : fdate = db.daily_record_freeze.get (last_frozen [0], 'date') \ + common.day find_user ['date'] = fdate.pretty ('%Y-%m-%d;') dr_per_user = db.daily_record.filter (None, find_user) pending [u] = {} if dr_per_user : earliest = latest = None for p in dr_per_user : date = db.daily_record.get (p, 'date') week, year = common.weekno_year_from_day (date) if not earliest or date < earliest : earliest = date if not latest or date > latest : latest = date start, end = common.week_from_date (date) if fdate and start < fdate : start = fdate filter ['date'] = common.pretty_range (start, end) filter ['user'] = u pending [u][(year, week)] = \ [ None , request.indexargs_url ('', editdict) , 'todo' ] interval = latest - earliest for k in pending [u].iterkeys () : if interval < Interval ('31d') : filter ['date'] = common.pretty_range (earliest, latest) pending [u][k][0] = request.indexargs_url ('', editdict) else : pending [u][k][0] = pending [u][k][1] else : dyn = user_dynamic.last_user_dynamic (db, u) if dyn and (not dyn.valid_to or not fdate or dyn.valid_to > fdate) : date = now if dyn.valid_to and dyn.valid_to < date : date = dyn.valid_to week, year = common.weekno_year_from_day (date) start, end = common.week_from_date (date) if fdate and start < fdate : start = fdate if dyn.valid_to and dyn.valid_to < end : end = dyn.valid_to filter ['date'] = common.pretty_range (start, end) filter ['user'] = u url = request.indexargs_url ('', editdict) pending [u][(year, week)] = [url, url, 'done'] request.filterspec = spec return pending
def correct_midnight_date_string (db) : """returns GMT's "today.midnight" in localtime format. suitable for passing in to forms that need this date. """ d = Date ('00:00', -db._db.getUserTimezone ()) return d.pretty ('%Y-%m-%d.%H:%M:%S')
def consolidated_vacation \ (db, user, ctype = -1, date = None, vc = None, to_eoy = True) : """ Compute remaining vacation on the given date """ if date is None : date = Date ('.') if ctype == -1 : ctype = _get_ctype (db, user, date) if ctype == -1 : return vc = vc or get_vacation_correction (db, user, ctype, date) if not vc : return None ed = next_yearly_vacation_date (db, user, ctype, date) if not to_eoy : ed = min (ed, date + common.day) d = vc.date dyn = vac_get_user_dynamic (db, user, ctype, d) while dyn and dyn.valid_to and dyn.valid_to <= d : dyn = vac_next_user_dynamic (db, dyn) if dyn is None : return None vac = float (vc.days) msg = "vac_aliq None for user_dynamic%s" % dyn.id assert dyn.vac_aliq, msg va = db.vac_aliq.getnode (dyn.vac_aliq) assert va.name in ('Daily', 'Monthly') # Need to skip first period without a dyn user record # sd is the current start date for german aliquotation # We subtract 1 day to easily compare the day of the ending-date # with the day of the start date sd = d # This is used for corrections if the start day lies beyond 28 -- in # that case there are months that simply don't have that date. So we # must correct for this in months with less days. sd_day = 0 if dyn.valid_from > d : sd = d = dyn.valid_from while dyn and d < ed : if dyn.valid_from > d : # We want to check if the days that are lost here whenever a # jump in dyn user records occurs are OK for monthly aliqotation sd = d = dyn.valid_from continue assert not dyn.valid_to or dyn.valid_to > d eoy = Date ('%s-12-31' % d.year) msg = "vacation_yearly None for user_dynamic%s" % dyn.id assert dyn.vacation_yearly is not None, msg msg = ( "vac_aliq changes w/o absolute vac_corr for user_dynamic%s" % dyn.id ) assert dyn.vac_aliq == va.id, msg if dyn.valid_to and dyn.valid_to <= ed and dyn.valid_to < eoy : if va.name == 'Daily' : yd = float (common.ydays (dyn.valid_to)) vac += interval_days \ (dyn.valid_to - d) * dyn.vacation_yearly / yd else : md = month_diff (sd, dyn.valid_to) dy = sd_day or sd.day if dyn.valid_to.day < dy : md -= 1 # Example: sd = 2018-04-03 valid_to = 2018-06-01 # Need to set sd=2018-05-03, i.e. the next start # day before valid_to # Even more complex is the case where e.g. # sd = 2018-03-31 valid_to = 2018-05-01 # We set sd=2018-04-30 and sd_day=31 # Get last day of last month lm = dyn.valid_to - Interval ('%sd' % dyn.valid_to.day) em = common.end_of_month (lm) if dy > em.day : sd_day = sd.day sd = em else : sd = Date (lm.pretty ("%%Y-%%m-%s" % sd.day)) sd_day = 0 else : sd = Date (dyn.valid_to.pretty ("%%Y-%%m-%s" % sd.day)) sd_day = 0 d = dyn.valid_to vac += dyn.vacation_yearly * md / 12.0 dyn = vac_next_user_dynamic (db, dyn) elif eoy < ed : if va.name == 'Daily' : yd = float (common.ydays (eoy)) iv = eoy + common.day - d vac += interval_days (iv) * dyn.vacation_yearly / yd else : md = month_diff (sd, eoy) dy = sd_day or sd.day assert eoy.day >= dy if dy == 1 : md += 1 sd = eoy + common.day else : sd = Date (eoy.pretty ("%%Y-%%m-%s" % sd.day)) sd_day = 0 vac += dyn.vacation_yearly * md / 12.0 d = eoy + common.day if dyn.valid_to == d : dyn = vac_next_user_dynamic (db, dyn) else : if va.name == 'Daily' : yd = float (common.ydays (ed - common.day)) vac += interval_days (ed - d) * dyn.vacation_yearly / yd else : md = month_diff (sd, ed) dy = sd_day or sd.day if ed.day < dy : md -= 1 sd = ed vac += dyn.vacation_yearly * md / 12.0 d = ed return vac
class Report (object) : def __init__ \ (self, db, date , send_mail = False , users = [] , mailall = [] , do_nosy = False ) : self.db = db self.date = Date (date) self.output = self.print_results if send_mail : self.output = self.mail_results self.users = list (users) if self.users : self.users.extend (mailall) self.now = Date ('.') stati = dict ((db.status.get (i, 'name'), i) for i in db.status.getnodeids ()) for n in 'deferred', 'done-cbb', 'resolved' : del stati [n] self.user_reports = {} issues = db.issue.filter \ ( None , dict ( status = stati.values () , deadline = ';%s' % self.date.pretty ('%Y-%m-%d.%H:%M:%S') ) , sort = [('+', 'deadline'), ('+', 'priority')] ) for i in issues : n = db.issue.getnode (i) if n.assignedto : u = self.add_user (db.user.get (n.assignedto, 'address')) if u : u.report_lines.append (ReportLine (n)) if do_nosy : for uid in n.nosy : if uid != n.assignedto : u = self.add_user (db.user.get (uid, 'address')) if u : u.nosy_lines.append (ReportLine (n)) for m in mailall : u = self.add_user (m) if u : u.all_lines.append (ReportLine (n)) def add_user (self, email) : if self.users and email not in self.users : return None if email not in self.user_reports : self.user_reports [email] = UserReport (self, email) return self.user_reports [email] def print_results (self) : for email, report in self.user_reports.iteritems () : print "%s:\n" % email, str (report) def mail_results (self) : smtp = SMTP (self.db.config.MAIL_HOST) header = \ [ "Subject: Summary of pending bug reports until %s" % self.date.pretty ('%Y-%m-%d') ] for report in self.user_reports.values () : try : report.mail (smtp, header) except SMTPRecipientsRefused, cause : print >> sys.stderr, cause
def month_name(date): d = Date(date) return d.pretty('%B')
def check_time_record (db, cl, nodeid, new_values) : for i in 'daily_record', : if i in new_values : raise Reject, _ ("%(attr)s may not be changed") % {'attr' : _ (i)} drec = new_values.get ('daily_record', cl.get (nodeid, 'daily_record')) dr = db.daily_record.getnode (drec) date = dr.date user = dr.user wpid = new_values.get ('wp', cl.get (nodeid, 'wp')) wp = None tp = None is_ph = False if (wpid) : wp = db.time_wp.getnode (wpid) tp = db.time_project.getnode (wp.project) is_ph = tp.is_public_holiday if ( frozen (db, user, date) and new_values.keys () != ['tr_duration'] ) : uname = db.user.get (user, 'username') raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals () status = db.daily_record.get (cl.get (nodeid, 'daily_record'), 'status') leave = db.daily_record_status.lookup ('leave') allow = False if dr.status == leave : du = vacation.leave_duration (db, user, date, is_ph) if ( new_values.keys () == ['duration'] and new_values ['duration'] == du and cl.get (nodeid, 'duration') != du ) : allow = True allow = allow or db.getuid () == '1' if ( status != db.daily_record_status.lookup ('open') and new_values.keys () != ['tr_duration'] and not allow ) : raise Reject, _ ('Editing of time records only for status "open"') # allow empty duration to delete record if 'duration' in new_values and new_values ['duration'] is None : keys = dict.fromkeys (new_values.iterkeys ()) del keys ['duration'] if len (keys) > 0 : raise Reject, \ ( _ ('%(date)s: No duration means "delete record" but ' 'you entered new value for %(attr)s' ) % dict ( date = date.pretty (common.ymd) , attr = ", ".join (['"' + _ (i) + '"' for i in keys]) ) ) return check_generated (new_values) start = new_values.get ('start', cl.get (nodeid, 'start')) end = new_values.get ('end', cl.get (nodeid, 'end')) duration = new_values.get ('duration', cl.get (nodeid, 'duration')) dist = new_values.get ('dist', cl.get (nodeid, 'dist')) wp = new_values.get ('wp', cl.get (nodeid, 'wp')) wl = 'work_location' ta = 'time_activity' location = new_values.get (wl, cl.get (nodeid, wl)) activity = new_values.get (ta, cl.get (nodeid, ta)) comment = new_values.get ('comment', cl.get (nodeid, 'comment')) check_start_end_duration \ (date, start, end, duration, new_values, dist = dist) if not location : new_values ['work_location'] = '1' if dist and not wp : raise Reject, _ ("Distribution: WP must be given") if dist : if dist < duration : newrec = dict \ ( daily_record = drec , duration = dist , wp = wp , time_activity = activity , work_location = location ) if comment : newrec ['comment'] = comment start_generated = new_values.get \ ('start_generated', cl.get (nodeid, 'start_generated')) if (start) : newrec ['start'] = start newrec ['end_generated'] = True newrec ['start_generated'] = start_generated new_values ['start_generated'] = True cl.create (** newrec) for attr in 'wp', 'time_activity', 'work_location', 'comment' : if attr in new_values : del new_values [attr] wp = cl.get (nodeid, 'wp') elif dist == duration : # Nothing to do -- just set new wp pass else : dist -= duration wstart, wend = common.week_from_date (date) dsearch = common.pretty_range (date, wend) drs = db.daily_record.filter \ (None, dict (user = user, date = dsearch)) trs = db.time_record.filter \ (None, {'daily_record' : drs}) trs = [db.time_record.getnode (t) for t in trs] trs = [t for t in trs if ( not t.wp and t.id != nodeid and ( t.daily_record != drec or ( start and t.start > start or not start ) ) ) ] trs = [(db.daily_record.get (tr.daily_record, 'date'), tr.start, tr) for tr in trs ] trs.sort () trs = [tr [2] for tr in trs] sum = reduce (add, [t.duration for t in trs], 0) if sum < dist : raise Reject, _ \ ("dist must not exceed sum of unassigned times in week") for tr in trs : if tr.duration <= dist : dist -= tr.duration db.time_record.set \ ( tr.id , wp = wp , time_activity = activity , work_location = location ) else : param = dict (duration = tr.duration - dist) newrec = dict \ ( daily_record = tr.daily_record , duration = dist , wp = wp , time_activity = activity , work_location = location ) if tr.start : param ['start_generated'] = True dstart = Date (tr.start) hours = int (dist) minutes = (dist - hours) * 60 dstart += Interval ('%d:%d' % (hours, minutes)) param ['start'] = dstart.pretty (hour_format) newrec ['start'] = tr.start newrec ['end_generated'] = True cl.create (** newrec) # warning side-effect, calling set will change # values in current tr! db.time_record.set (tr.id, **param) dist = 0 if not dist : break assert (dist == 0) del new_values ['dist'] if wp : correct_work_location (db, wp, new_values) if 'tr_duration' not in new_values : new_values ['tr_duration'] = None
def new_time_record (db, cl, nodeid, new_values) : """ auditor on time_record """ uid = db.getuid () travel = False common.require_attributes (_, cl, nodeid, new_values, 'daily_record') common.reject_attributes (_, new_values, 'dist', 'tr_duration') check_generated (new_values) dr = db.daily_record.getnode (new_values ['daily_record']) uname = db.user.get (dr.user, 'username') if dr.status != db.daily_record_status.lookup ('open') and uid != '1' : raise Reject, _ ('Editing of time records only for status "open"') if frozen (db, dr.user, dr.date) : date = dr.date raise Reject, _ ("Frozen: %(uname)s, %(date)s") % locals () start = new_values.get ('start', None) end = new_values.get ('end', None) duration = new_values.get ('duration', None) wpid = new_values.get ('wp') ttby = db.user.get (dr.user, 'timetracking_by') if ( uid != dr.user and uid != ttby and not common.user_has_role (db, uid, 'controlling', 'admin') and not leave_wp (db, dr, wpid, start, end, duration) and not vacation_wp (db, wpid) ) : raise Reject, _ \ ( ("Only %(uname)s, Timetracking by, and Controlling " "may create time records" ) % locals () ) dynamic = user_dynamic.get_user_dynamic (db, dr.user, dr.date) date = dr.date.pretty (common.ymd) if not dynamic : if uid != '1' : raise Reject, _ \ ("No dynamic user data for %(uname)s, %(date)s") % locals () else : if not dynamic.booking_allowed and uid != '1' : raise Reject, _ \ ("Booking not allowed for %(uname)s, %(date)s") % locals () if not (dr.weekend_allowed or dynamic.weekend_allowed) and uid != '1' : wday = gmtime (dr.date.timestamp ())[6] if wday in (5, 6) : raise Reject, _ ('No weekend booking allowed') dstart, dend = check_start_end_duration \ (dr.date, start, end, duration, new_values) # set default work location for new time record by work location ID # for ID reference values check file initial_data.py or use endpoint # /work_location in the web interface if 'work_location' not in new_values : new_values ['work_location'] = '2' # set default values according to selected work package if 'wp' in new_values and new_values ['wp'] : wp = new_values ['wp'] # overwrite work location default if default specified in time category correct_work_location (db, wp, new_values) travel = travel or db.time_wp.get (wp, 'travel') if 'time_activity' in new_values and new_values ['time_activity'] : act = new_values ['time_activity'] travel = travel or db.time_activity.get (act, 'travel') duration = new_values.get ('duration', None) ls = Date (db.user.get (dr.user, 'lunch_start') or '12:00') ls.year = dr.date.year ls.month = dr.date.month ls.day = dr.date.day ld = db.user.get (dr.user, 'lunch_duration') or 1 hours = int (ld) minutes = (ld - hours) * 60 le = ls + Interval ('%d:%d' % (hours, minutes)) if not travel and duration > 6 and start and dstart < ls and dend > ls : newrec = { 'daily_record' : new_values ['daily_record'] , 'start' : le.pretty (hour_format) } dur1 = (ls - dstart).as_seconds () / 3600. dur2 = duration - dur1 if end : dur2 -= ld newrec ['duration'] = dur2 for attr in 'wp', 'time_activity', 'work_location' : if attr in new_values and new_values [attr] : newrec [attr] = new_values [attr] new_values ['end'] = ls.pretty (hour_format) new_values ['duration'] = dur1 if dur2 > 0 : db.time_record.create (** newrec)
#!/usr/bin/python # -*- coding: iso-8859-1 -*- # Cron-job for updating adr-type for valid or closed abos. # Should be run in the early morning of every 1st of a month. import sys from roundup.date import Date from roundup import instance tracker = instance.open (sys.argv [1]) db = tracker.open ('admin') now = Date ('.') type_cat = db.adr_type_cat.lookup ('ABO') abo_adr_types = db.adr_type.find (typecat = type_cat) adr = db.address.filter (None, {'adr_type' : abo_adr_types}) adr = dict ([(k, 1) for k in adr]) valid_abos = db.abo.filter (None, {'end' : '-1'}) storno_abos = db.abo.filter (None, {'end' : now.pretty (';%Y-%m-%d')}) for abo in valid_abos + storno_abos : adr [db.abo.get (abo, 'subscriber')] = 1 for a in adr.iterkeys () : # May seem like a noop -- leave the correct updating to the auditor. db.address.set (a, adr_type = db.address.get (a, 'adr_type')) db.commit ()
def check_auto_wp (db, auto_wp_id, userid) : """ Get latest non-frozen dynamic user record for this user and then look through auto WPs and check if they conform: There should be a WP for each contiguous time a dynamic user record with do_auto_wp set is available for this user. """ user = db.user.getnode (userid) snam = str (user.username).split ('@') [0] desc = "Automatic wp for user %s" % snam # Very early default should we not find any freeze records start = Date ('1970-01-01') freeze = freeze_date (db, userid) auto_wp = db.auto_wp.getnode (auto_wp_id) duration_end = auto_wp_duration_end (db, auto_wp, userid) # Nothing todo if this ended all before freeze if duration_end and duration_end < start : return tp = db.time_project.getnode (auto_wp.time_project) # Get dynamic user record on or after start dyn = find_user_dynamic (db, userid, start, ct = auto_wp.contract_type) # If auto_wp isn't valid, we set dyn to None this will invalidate # all WPs found: if not auto_wp.is_valid : dyn = None # Find first dyn with do_auto_wp and org_location properly set while dyn and not is_correct_dyn (dyn, auto_wp) : dyn = next_user_dynamic (db, dyn, use_ct = True) # Find all wps auto-created from this auto_wp after start. # Note that all auto wps have a start time. Only the very first wp # may start *before* start (if the time_end of the time_wp is either # empty or after start) d = dict \ ( auto_wp = auto_wp_id , time_start = pretty_range (start) , bookers = userid ) wps = db.time_wp.filter (None, d, sort = ('+', 'time_start')) wps = [db.time_wp.getnode (w) for w in wps] d ['time_start'] = pretty_range (None, start) wp1 = db.time_wp.filter (None, d, sort = ('-', 'time_start'), limit = 1) assert len (wp1) <= 1 if wp1 : wp = db.time_wp.getnode (wp1 [0]) is_same = wps and wps [0].id == wp.id if not is_same and not wp.time_end or wp.time_end > start : wps.insert (0, wp) # Now we have the first relevant dyn user record and a list of WPs # The list of WPs might be empty try : wp = wps.pop (0) except IndexError : wp = None while dyn : # Remember the start time of the first dyn dyn_start = dyn.valid_from # we compute the range of contiguous dyns with same do_auto_wp setting while dyn and dyn.valid_to : n = next_user_dynamic (db, dyn, use_ct = True) if ( n and is_correct_dyn (n, auto_wp) and n.valid_from == dyn.valid_to ) : dyn = n else : break end_time = dyn.valid_to if duration_end and (not end_time or end_time > duration_end) : end_time = duration_end if end_time and end_time <= dyn_start : dyn = None break # Limit the first wp(s) if the start time of the dyn user record is # after the start time of the frozen range and the wp starts # before that. while wp and wp.time_start < start and dyn_start > start : if wp.time_end > start : n = '%s -%s' % (snam, start.pretty (ymd)) d = dict (time_end = start, name = n) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) try : wp = wps.pop (0) except IndexError : wp = None assert not wp or wp.time_start > start # Check if there are wps that start before the validity of the # current dynamic user record while wp and wp.time_start < dyn_start : if wp.time_end and wp.time_end <= dyn_start : # Check that really nothing is booked on this WP d = dict (wp = wp.id) d ['daily_record.user'] = userid d ['daily_record.date'] = pretty_range \ (wp.time_start, wp.time_end) trs = db.time_record.filter (None, d) # If something is booked we set end = start # Otherwise we retire the wp if not trs : db.time_wp.retire (wp.id) else : n = '%s -%s' % (snam, wp.time_start.pretty (ymd)) d = dict (time_end = wp.time_start, name = n) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) try : wp = wps.pop (0) except IndexError : wp = None else : d = dict (time_start = dyn_start) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) break # We now either have a wp with correct start date or # with a start date > dyn.valid_to or none if wp and (not dyn.valid_to or wp.time_start < dyn.valid_to) : # We may have not a single wp but a set of 'fragments' # before the end-date of the dyn user record (or an open # end) while ( wps and ( end_time and wp.time_end < end_time or not end_time and wp.time_end ) ) : if wps [0].time_start > end_time : break assert not wps [0].time_end or wp.time_end <= wps [0].time_end if wp.time_end != wps [0].time_start : n = '%s -%s' % (snam, wps [0].time_start.pretty (ymd)) d = dict (time_end = wps [0].time_start, name = n) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, ** d) wp = wps.pop (0) if end_time : if wp.time_end != end_time : n = '%s -%s' % (snam, end_time.pretty (ymd)) d = dict (time_end = end_time, name = n) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) else : if wp.time_end : d = dict (time_end = None, name = snam) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) try : wp = wps.pop (0) except IndexError : wp = None else : # If we have no wp, create one d = dict \ ( auto_wp = auto_wp_id , project = auto_wp.time_project , durations_allowed = auto_wp.durations_allowed , bookers = [userid] , description = desc , name = snam , time_start = max (start, dyn_start) , is_public = False , planned_effort = 0 , responsible = tp.responsible ) # If the dyn is time-limited we have to name the wp # appropriately and limit the end-time of the wp. if end_time : d ['time_end'] = end_time d ['name'] = '%s -%s' % (snam, end_time.pretty (ymd)) wpid = db.time_wp.create (**d) od = dyn dyn = next_user_dynamic (db, dyn, use_ct = True) while dyn and not is_correct_dyn (dyn, auto_wp) : dyn = next_user_dynamic (db, dyn, use_ct = True) if not dyn : if not end_time : assert not wp and not wps if wp : wps.insert (0, wp) wp = None # Retire WPs that are left over but before that check that # nothing is booked. for wp in wps : d = dict (wp = wp.id) d ['daily_record.user'] = userid trs = db.time_record.filter (None, d) if not trs : db.time_wp.retire (wp.id) else : n = '%s -%s' % (snam, wp.time_start.pretty (ymd)) d = dict (time_end = wp.time_start, name = n) if wp.description != desc : d ['description'] = desc db.time_wp.set (wp.id, **d) wps = [] wp = None