Esempio n. 1
0
def metrics(request):
    from webinars_web.webinars import models as wm
    now = time()
    data = {}
    data['installs']=[0]*7
    data['uninstalls']=[0]*7
    data['new_mrr']=[0]*7
    data['recurring_mrr']=[0]*7
    data['months']=['Dec','Jan','Feb','Mar','Apr','May','Jun']
    for hub in wm.Hub.objects.filter(internal=False):
        if hub.friends_and_family: continue
        install_month = hub.created_at.month
        print install_month
        uninstall_month = hub.uninstalled_at and hub.uninstalled_at.month
        data['installs'][install_month%12] += 1
        if uninstall_month is not None:
            data['uninstalls'][uninstall_month%12] += 1
        if not hub.beta:
            mrr_at = hub.created_at + delta(md=30)
            new = True
            while mrr_at < now and (uninstall_month is None or mrr_at < hub.uninstalled_at):
                if new:
                    data['new_mrr'][mrr_at.month%12] += 50
                    new = False
                else:
                    data['recurring_mrr'][mrr_at.month%12] += 50
                mrr_at = mrr_at + delta(md=30)
    data['net_installs'] = []
    data['total_mrr'] = []
    for i in range(len(data['months'])):
        data['net_installs'].append(data['installs'][i]-data['uninstalls'][i])
        data['total_mrr'].append(data['new_mrr'][i]+data['recurring_mrr'][i])
    for attr in ('installs','uninstalls','net_installs','new_mrr','recurring_mrr','total_mrr'):
        data['total_%s'%attr] = sum(data[attr])
    return render_to_response('hubs/metrics.djml', { 'data': data }, context_instance=RequestContext(request))
Esempio n. 2
0
    def setUp(self):
        self.userdata = {'username':'******',
                         'password':'******'}

        self.user = User.objects.create_user(**self.userdata)
        self.user = User.objects.get_by_natural_key('xenuuu')
        
        self.recipient = Recipient.objects.create(sender=self.user,
                                                  sender_name='bob',
                                                  sender_phone='000-000-0000',
                                                  name='granny',
                                                  email='*****@*****.**',
                                                  timezone='America/Los_Angeles'
                                                )
        self.subscription = TumblrSubscription.objects.create(recipient=self.recipient,
                                                              short_name='bobs_monkeys',
                                                              pretty_name="Bob's Monkey Photos",
                                                              avatar='monkey.jpg') #supplying all fields to avoid call to tumblr

        start_dt = time() - delta(hours=1 * 7 * 24)
        end_dt = time() + delta(hours=1 * 7 * 24)
        self.vacation = Vacation.objects.create(recipient=self.recipient,
                                                start_date=start_dt.datetime,
                                                end_date=end_dt.datetime
                                                )

        #urls of things that require you to be logged in to access        
        self.login_required_urls = [
                               reverse_lazy('subscription_create_tumblr'),
                               reverse_lazy('subscription_list'),
                               reverse_lazy('subscription_detail_tumblr', kwargs={'pk':self.subscription.pk}),
                               reverse_lazy('subscription_delete_tumblr', kwargs={'pk':self.subscription.pk}),
                               reverse_lazy('recipient_create'),
                               reverse_lazy('recipient_detail', kwargs={'pk':self.recipient.pk}),
                               reverse_lazy('vacation_create', kwargs={'recipient_id':self.recipient.pk}),
                               reverse_lazy('vacation_cancel', kwargs={'pk':self.vacation.pk})

                               #todo:  delete the recipient
                       
                       ]
        
        #urls of things that you must own the object in question (or a related one) in order to access.
        #these will just be tested by being logged in as a user other than xenuuu
        self.ownership_required_urls = [
                               reverse_lazy('subscription_detail_tumblr', kwargs={'pk':self.subscription.pk}),
                               reverse_lazy('subscription_delete_tumblr', kwargs={'pk':self.subscription.pk}),
                               reverse_lazy('recipient_detail', kwargs={'pk':self.recipient.pk}),
                               reverse_lazy('vacation_create', kwargs={'recipient_id':self.recipient.pk}),
                               reverse_lazy('vacation_cancel', kwargs={'pk':self.vacation.pk})
                               ]
        self.login_url = reverse('auth_login')
Esempio n. 3
0
def new_or_edit(request, account_id=None, setup=False):
    from webinars_web.webinars import models as wm
    hub = models.Hub.ensure(request.marketplace.hub_id)
    kwargs = {'hub': hub}
    if account_id:
        kwargs['instance']=models.Account.objects.get(pk=account_id)
    if request.method == 'POST': # If the form has been submitted...
        if request.POST.get('cancel'):
            return HttpResponseRedirect('%saccounts'%request.marketplace.base_url) # Redirect for cancel
        if request.POST.get('account_type','1') == '2':
            redirect_uri = urlquote_plus('%s/webinars/hubs/%s/accounts/new?label=%s' % (settings.GTW_OAUTH_REDIRECT_PROTOCOL_HOST, hub.id, urlquote_plus(request.POST.get('extra',''))))
            return HttpResponseRedirect('https://api.citrixonline.com/oauth/authorize?client_id=%s&redirect_uri=%s' % (settings.GTW_API_KEY, redirect_uri))
        form = AccountForm(request.POST, **kwargs) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            if form.cleaned_data.get('account_type') == 1:
                deleted_possibles = wm.Account.objects.filter(hub=hub, username=form.cleaned_data['username'], extra=form.cleaned_data['extra'], deleted_at__isnull=False)
            elif form.cleaned_data.get('account_type') == 2:
                deleted_possibles =wm.Account.objects.filter(hub=hub, username=form.cleaned_data['username'], deleted_at__isnull=False)
            if deleted_possibles:
                account = deleted_possibles[0]
                account.deleted_at = None
                account.password = form.cleaned_data['password']
                account.exclude_old_events_from_hubspot = bool(form.cleaned_data.get('exclude_old_events_from_hubspot'))
                if account.exclude_old_events_from_hubspot:
                    ignore_delta = int(form.cleaned_data.get('exclusion_date_delta'))
                    account.exclusion_date = (time() - delta(md=ignore_delta)).us
                else:
                    account.exclusion_date = None
            else:
                account = form.save(commit=False)
            account.exclude_old_events_from_hubspot = bool(form.cleaned_data.get('exclude_old_events_from_hubspot'))
            if account.exclude_old_events_from_hubspot:
                ignore_delta = int(form.cleaned_data.get('exclusion_date_delta'))
                account.exclusion_date = (time() - delta(md=ignore_delta)).us
                print account.exclusion_date
            else:
                account.exclusion_date = None
            account.hub_id = request.marketplace.hub_id
            account.default = False
            account.prevent_unformed_lead_import = False
            account.save()
            account.hub.sync(visible=True)
            return HttpResponseRedirect('%sevents'%(request.marketplace.base_url)) # Redirect after POST
    else:
        form = AccountForm(**kwargs) # An unbound form

    return render_to_response('accounts/%s.djml'%(setup and 'setup' or account_id and 'edit' or 'new'), {
        'form': form,
        'account_types': models.AccountType.objects.all()
    }, context_instance=RequestContext(request))
Esempio n. 4
0
 def test_register(self):
     w = Webinar(self.organizer,
                 key=2394,
                 timezone='America/New_York',
                 sessions=[])
     s = Session(w, key=6043, started_at=time('2012-06-01'), attendees=[])
     s.attendees.append(
         Registrant(webinar=w,
                    session=s,
                    key=2305,
                    first_name=u'Suzy',
                    last_name=u'Samwell',
                    email=u'*****@*****.**',
                    duration=delta(s=4931)))
     with mocker(CreateRegistrant, text=self.registered_json):
         seed_registrant = Registrant(webinar=w,
                                      session=s,
                                      first_name=u'J\u00f6hn',
                                      last_name=u'Smith',
                                      email=u'*****@*****.**')
         expected_registrant = Registrant(
             webinar=w,
             session=s,
             key=2038,
             first_name=u'J\u00f6hn',
             last_name=u'Smith',
             email=u'*****@*****.**',
             join_url='https://bit.ly/00293423')
         self.assertEquals(expected_registrant, seed_registrant.create())
Esempio n. 5
0
 def test_delete_started_vacation(self):
     """
     Cancelling a vacation that's already started actually sets 
     the end date to now, not actually deleting it.
     """
     self.client.login(**self.userdata)
     start = time(tz='UTC') - delta(hours=2*24) #started yesterday
     end = start + delta(hours=7*24)
     vacation = Vacation.objects.create(recipient=self.recipient,
                                        start_date=start.datetime,
                                        end_date=end.datetime)
     
     url = reverse('vacation_cancel', kwargs={'pk':vacation.pk})
     self.client.post(url)
     vacation = Vacation.objects.get(pk=vacation.pk) #reload
     self.assertTrue(vacation.end_date <= time(tz='UTC').datetime)
Esempio n. 6
0
 def __init__(self, **kwargs):
     super(Registrant, self).__init__()
     self.webinar = kwargs.get('webinar')
     self.session = kwargs.get('session')
     self.key = mget(kwargs, 'key', 'registrant_key', 'registrantKey')
     self.email = nlower(mget(kwargs, 'email', 'attendeeEmail'))
     self.first_name = mget(kwargs, 'first_name', 'firstName', 'first')
     self.last_name = mget(kwargs, 'last_name', 'lastName', 'last')
     if kwargs.get('name'): self.name = nstrip(kwargs.get('name'))
     self.registered_at = ntime(
         mget(kwargs, 'registered_at', 'registrationDate'))
     self.join_url = mget(kwargs, 'join_url', 'joinUrl')
     self.status = kwargs.get('status')
     self.viewings = kwargs.get('viewings', [])
     if not self.viewings and kwargs.get('attendance'):
         self.viewings = sort([(time(d['joinTime']), time(d['leaveTime']))
                               for d in kwargs['attendance']])
     if not self.viewings and (
             kwargs.get('duration') or kwargs.get('attendanceTimeInSeconds')
     ) and self.session and self.session.key and self.session.started_at:
         duration = kwargs.get(
             'duration') or kwargs.get('attendanceTimeInSeconds') and delta(
                 s=kwargs['attendanceTimeInSeconds'])
         self.viewings = [(self.session.started_at,
                           self.session.started_at + duration)]
Esempio n. 7
0
    def test_delete_future_vacation(self):
        """
        Cancelling a future vacation that has not yet started is fine,
        just delete it.
        """
        self.client.login(**self.userdata)
        start = time(tz='UTC') + delta(hours=1*24)
        end = start + delta(hours=1*24*7)

        vacation = Vacation.objects.create(recipient=self.recipient,
                                           start_date=start.datetime,
                                           end_date=end.datetime)

        url = reverse('vacation_cancel', kwargs={'pk':vacation.pk})
        self.client.post(url)
        
        self.assertRaises(Vacation.DoesNotExist, Vacation.objects.get, pk=vacation.pk)
Esempio n. 8
0
File: os.py Progetto: prior/grabbag
 def lock(self, timeout=None, poll_rate=None): # poll_rate & timeout as deltas or micros
     timeout = ndelta(timeout) or self.timeout or delta(0)
     poll_rate = ndelta(poll_rate) or self.poll_rate
     start = time()
     while not self._attempt_lock():
         if poll_rate:
             poll_rate.sleep()
         if time() > start + timeout: 
             break
     return self.locked
Esempio n. 9
0
 def test_bad_vacation_form(self):
     """
     Submitting a vacation form where the start date is after the end date is an error.
     """
     start = time(tz='UTC')
     end = start - delta(hours=1*24)
     data = {'start_date': start.strftime('%Y-%m-%d'),
             'end_date': end.strftime('%Y-%m-%d')}
     form = VacationForm(data)
     self.assertFalse(form.is_valid())
Esempio n. 10
0
    def test_delete_somebody_elses_vacation(self):
        """
        Users shouldn't be able to delete vacations belonging to other users.
        """
        new_user = User.objects.create_user("new_user", password='******')
        new_recipient = Recipient.objects.create(sender=new_user,
                                                  name='Nanna',
                                                  email='*****@*****.**',
                                                  timezone='Europe/Copenhagen')

        #this vacation starts and ends in the future, so it can be deleted and not just trigger
        #the end_date change.
        start_date = time(tz='UTC') + delta(hours=1 * 24)
        end_date = time(tz='UTC') + delta(hours=4 * 24 * 7)
        vacation = Vacation.objects.create(recipient=new_recipient,
                                           start_date=start_date.datetime,
                                           end_date=end_date.datetime)
        url = reverse('vacation_cancel', kwargs={'pk':vacation.pk})
        
        self.client.login(**self.userdata) #login as self.user, NOT new_user
        
        #try to delete vacation belonging to new_user
        res = self.client.post(url)
        vacation = Vacation.objects.get(pk=vacation.pk) #force reload, should still exist
Esempio n. 11
0
 def test_get_vacationing_recipients(self):
     """
     Tests getting the set of recipients currently on vacation.
     """
     #start with a clean slate
     Recipient.objects.all().delete()
     now = time(tz='UTC')
     last_week = now - delta(hours=7*24)
     next_week = now + delta(hours=7*24)
     
     for i in range(10):
         recipient = Recipient.objects.create(sender=self.user,
                                  name='Nonna_%s' % i,
                                  email='*****@*****.**' % i,
                                  postcode='02540',
                                  timezone='America/New_York')
                 
         if i < 4: #create vacations for the first four
             Vacation.objects.create(recipient=recipient,
                                     start_date=last_week.datetime,
                                     end_date=next_week.datetime)
     
     self.assertEqual(Recipient.get_vacationing_recipients().count(), 4)
     self.assertEqual(Recipient.objects.exclude(pk__in=Recipient.get_vacationing_recipients()).count(), 6)
Esempio n. 12
0
 def pre_sync_hook(self, sync):
     from webinars_web.webinars import models as wm
     if self.mothballed: return False
     if self.account.is_webex:
         if self.starts_at < time()-delta(md=89): # past webex registrant/attendee reporting limit (90 days)
             self.unknowable_registrants = True
             self.mothballed = True
             self.save()
             return False
     if not self._lock_for_sync():
         if sync.visible and self.current_sync and not self.current_sync.visible:
             wm.EventSync.objects.filter(id=self.current_sync.id).update(visible=True)
         return False
     self.__class__.objects.filter(id=self.id).update(current_sync=sync)
     self.current_sync = sync # faster than changing attr and save()-ing
     return True
Esempio n. 13
0
    def test_create_somebody_elses_vacation(self):
        new_user = User.objects.create_user("new_user", password='******')
        new_recipient = Recipient.objects.create(sender=new_user,
                                                  name='Nanna',
                                                  email='*****@*****.**',
                                                  timezone='Asia/Tokyo')

        self.client.login(**self.userdata)
        url = reverse('vacation_create', kwargs={'recipient_id': new_recipient.pk})
        
        start = time(tz='UTC')
        end = start - delta(hours=1*24)
        data = {'start_date': start.strftime('%Y-%m-%d'),
                'end_date': end.strftime('%Y-%m-%d')}
        
        res = self.client.post(url, data)
        self.assertEqual(res.status_code, 403) #can't do that
Esempio n. 14
0
def _parcel_timings():
    from webinars_web.webinars import models as wm
    now = time()
    time_chunks = {'hour':10**6*60**2, 'day':10**6*60**2*24, 'week':10**6*60**2*24*7}
    since_times = dict((k,now-v) for k,v in time_chunks.iteritems())
    times = dict((k,{'wait':[], 'work':[]}) for k,v in time_chunks.iteritems())
    min_since_time = min(since_times.values())
    models = (wm.AccountSyncStage, wm.AccountSyncShard, wm.HubSpotEventSyncStage, wm.WebexEventSyncStage, wm.EventSyncShard)
    for m in models:
        for created_at, started_at, completed_at in m.objects.filter(created_at__isnull=False, created_at__gt=min_since_time, parent_sync__debug=False, parent_sync__forced_stop=False).values_list('created_at','started_at','completed_at'):
            for k in time_chunks.keys():
                if created_at >= since_times[k] and started_at:
                    times[k]['wait'].append(time(started_at)-time(created_at))
                    if completed_at:
                        times[k]['work'].append(time(completed_at)-time(started_at))
    for k in times.keys():
        for t in ('wait','work'):
            lst = times[k][t]
            times[k][t] = len(lst)>0 and ((sum(lst,delta(0))/len(lst)).ms, min(lst).ms, max(lst).ms) or (None,None,None)
    return times
Esempio n. 15
0
    def start(self):
        from webinars_web.webinars import models as wm
        now = time()
        self.__class__.objects.filter(id=self.id).update(started_at=now)
        self.started_at = now
        if not self.event.pre_sync_hook(self):
            self.completed_at = time()
            self.save()
            return self
        if self.event.account.is_webex:
            wm.StagedWebexRegistrant.pre_stage(self.event)
            wm.WebexEventSyncStage.trigger_initial_stages(self)
        elif self.event.account.is_gtw:
            wm.StagedGTWRegistrant.pre_stage(self.event)
            wm.GTWEventSyncStage.trigger_initial_stages(self)

        wm.StagedHubSpotRegistrant.pre_stage(self.event)
        for event_form in self.event.event_forms.all():
            if time() > max(self.event.ended_at,self.event.ends_at)+delta(m=30) and not event_form.cms_form.is_sync_target: continue # avoid pulling any more leads after event is over
            wm.HubSpotEventSyncStage.objects.create(parent_sync=self, max_size=settings.HUBSPOT_EVENT_SYNC_STAGE_SIZE, event_form=event_form, start_last_modified_at=event_form.last_last_modified_at).trigger()
        #TODO: figure out what to do when cms form goes away-- maybe need to soft delete forms to keep them from violating foreign key constraints on old syncs?
        self.event.update_cms_form  # establishes its existence-- this is a good place to do it-- don't let the individual shards do it cuz then there are async issues to contend with 

        return self
Esempio n. 16
0
 def actual_duration_in_minutes(self, minutes):
     if self._started_at:
         self._ended_at = self._started_at + delta(m=minutes)
     elif self._ended_at:
         self._started_at = self._ended_at - delta(m=minutes)
Esempio n. 17
0
from utils.property import cached_property
from utils.dict import mget,kwargs_str
from utils.list import sort
from giftwrap import Auth, Exchange, JsonExchange
from .webinar import Webinar
from .session import Session
from sanetime import time,delta


API_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
DEFAULT_HISTORY_DELTA = delta(my=20)
GTW_NOW_DELTA = delta(s=10) # cuz gtw 500s when your time range enters the future, and their servers seem to be off by a few seconds


class Organizer(Auth):
    domain = 'api.citrixonline.com'

    def base_path(self): return 'G2W/rest/organizers/%s' % self.key
    def headers(self): return {'Authorization': 'OAuth oauth_token=%s'%self.oauth}

    def __init__(self, **kwargs):
        super(Organizer, self).__init__()
        self.oauth = mget(kwargs,'oauth','oauth_token','access_token')
        self.key = mget(kwargs,'key','organizerKey','organizer_key')
        self.now = time()
        self.starts_at = kwargs.get('starts_at',None) or (self.now - DEFAULT_HISTORY_DELTA)

    def __repr__(self): 
        return "Organizer(%s)" % kwargs_str(self.__dict__,'oauth','key')
    def __str__(self): return unicode(self).encode('utf-8')
    def __unicode__(self):
Esempio n. 18
0
ACCOUNT_SYNC_STAGE_SIZE = 10
HISTORIC_ACCOUNT_SYNC_STAGE_SIZE = 10
ACCOUNT_SYNC_SHARD_SIZE = 50
WEBEX_REGISTRANT_EVENT_SYNC_STAGE_SIZE = 500
WEBEX_ATTENDANT_EVENT_SYNC_STAGE_SIZE = 50
HUBSPOT_EVENT_SYNC_STAGE_SIZE = 100  # tried 50, but it just made things worse, cuz the chance of a single call failing is high-- so we need to reduce number of calls
EVENT_SHARD_SIZE = 30

TASK_QUEUE_AUTH = tq.Auth(53, HUBSPOT_API_KEY, env=ENV)

# Bump NUM_QUEUES if you want more work processed simultaneously. We are currently near our max
# capacity, but if more boxes are added, we can consider adding more queues.
NUM_QUEUES = 3

# Retry schedule for all taskqueues
schedule = [delta(s=20),delta(m=1),delta(m=5),delta(m=30)]

# These queues are used to process syncs.
# ...
TASK_QUEUES = [tq.Queue(
    TASK_QUEUE_AUTH, 
    name='webinarsxx_%s%s'%(ENV.lower(), x), 
    retry_schedule=schedule, 
    timeout=delta(s=60), 
    frequency=12, 
    idempotent=True) for x in range(NUM_QUEUES)]

# CONVERSION_QUEUE is the taskqueue used to create conversion events in hubspot via the leads API.
# It is a separate queue because we rely on its idempotency to prevent duplicate conversion events.
# It is used by webinars/cynq/registrant.py in HubSpotRegistrantRemoteStore._single_update().
# uid is a hexdigested hash of the form submission json. We could avoid an extra queue by slamming
Esempio n. 19
0
 def scheduled_duration_in_minutes(self, minutes):
     if self._starts_at: self._ends_at = self._starts_at + delta(m=minutes)
     elif self._ends_at: self._starts_at = self._ends_at - delta(m=minutes)
Esempio n. 20
0
 def sync_overdue(self):
     if self.uninstalled_at or self.current_sync: return False
     if not self.last_sync: return True
     return not self.last_sync or (time() - self.last_sync.completed_at) > delta(m=MINUTES_TIL_STALE_SYNC)  # greater than 15 minutes
Esempio n. 21
0
 def actual_duration_in_minutes(self, minutes):
     if self._started_at: self._ended_at = self._started_at + delta(m=minutes)
     elif self._ended_at: self._started_at = self._ended_at - delta(m=minutes)
Esempio n. 22
0
 def post_sync_hook(self, started_at, completed_at):
     if started_at > self.ended_at:
         if not self.registrant_set.filter(lead_guid__isnull=True, deleted_at__isnull=True).count():
             if self._ended_at or self.ends_at and self.ends_at < time()-delta(md=2):
                 self.mothballed = True
                 self.save()
Esempio n. 23
0
 def scheduled_duration_in_minutes(self, minutes):
     if self._starts_at: self._ends_at = self._starts_at + delta(m=minutes)
     elif self._ends_at: self._starts_at = self._ends_at - delta(m=minutes)
Esempio n. 24
0
 def test_register(self):
     w = Webinar(self.organizer, key=2394, timezone='America/New_York', sessions=[])
     s = Session(w, key=6043, started_at=time('2012-06-01'), attendees=[])
     s.attendees.append(Registrant(webinar=w, session=s, key=2305, first_name=u'Suzy', last_name=u'Samwell', email=u'*****@*****.**', duration=delta(s=4931)))
     with mocker(CreateRegistrant, text=self.registered_json):
         seed_registrant = Registrant(webinar=w, session=s, first_name=u'J\u00f6hn', last_name=u'Smith', email=u'*****@*****.**')
         expected_registrant = Registrant(webinar=w, session=s, key=2038, first_name=u'J\u00f6hn', last_name=u'Smith', email=u'*****@*****.**', join_url='https://bit.ly/00293423')
         self.assertEquals(expected_registrant, seed_registrant.create())
Esempio n. 25
0
    def is_bad(self): return not self.completed_at and (time() - self.started_at) > delta(h=1)

    @property
Esempio n. 26
0
 def __init__(self, **kwargs):
     super(Registrant, self).__init__()
     self.webinar = kwargs.get('webinar')
     self.session = kwargs.get('session')
     self.key = mget(kwargs, 'key', 'registrant_key', 'registrantKey')
     self.email = nlower(mget(kwargs, 'email', 'attendeeEmail'))
     self.first_name = mget(kwargs, 'first_name', 'firstName', 'first')
     self.last_name = mget(kwargs, 'last_name', 'lastName', 'last')
     if kwargs.get('name'): self.name = nstrip(kwargs.get('name'))
     self.registered_at = ntime(mget(kwargs, 'registered_at', 'registrationDate'))
     self.join_url = mget(kwargs, 'join_url', 'joinUrl')
     self.status = kwargs.get('status')
     self.viewings = kwargs.get('viewings',[])
     if not self.viewings and kwargs.get('attendance'):
         self.viewings = sort([(time(d['joinTime']),time(d['leaveTime'])) for d in kwargs['attendance']])
     if not self.viewings and (kwargs.get('duration') or kwargs.get('attendanceTimeInSeconds')) and self.session and self.session.key and self.session.started_at:
         duration = kwargs.get('duration') or kwargs.get('attendanceTimeInSeconds') and delta(s=kwargs['attendanceTimeInSeconds'])
         self.viewings = [(self.session.started_at, self.session.started_at+duration)]
Esempio n. 27
0
 def is_worrisome(self): return not self.completed_at and (time() - self.started_at) > delta(m=10)
 @property