from datetime import timedelta, datetime import dateutil.parser import pytz from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied from django.db.models import Q from tracker.models import Donation, SpeedRun, Bid _DEFAULT_DONATION_DELTA = timedelta(hours=3) _DEFAULT_DONATION_MAX = 200 _DEFAULT_DONATION_MIN = 25 # There is a slight complication in how this works, in that we cannot use the 'limit' set-up as a general filter mechanism, so these methods return the actual result, rather than a filter object def get_recent_donations( donations=None, min_donations=_DEFAULT_DONATION_MIN, max_donations=_DEFAULT_DONATION_MAX, delta=_DEFAULT_DONATION_DELTA, query_offset=None, ): offset = default_time(query_offset) if donations is None: donations = Donation.objects.all() if delta: high_filter = donations.filter(timereceived__gte=offset - delta) else: high_filter = donations count = high_filter.count() if max_donations is not None and count > max_donations: donations = donations[:max_donations] elif min_donations is not None and count < min_donations: donations = donations[:min_donations] else: donations = high_filter return donations _DEFAULT_RUN_DELTA = timedelta(hours=6) _DEFAULT_RUN_MAX = 7 _DEFAULT_RUN_MIN = 3 def get_upcoming_runs( runs=None, include_current=True, max_runs=_DEFAULT_RUN_MAX, min_runs=_DEFAULT_RUN_MIN, delta=_DEFAULT_RUN_DELTA, query_offset=None, ): offset = default_time(query_offset) if runs is None: runs = SpeedRun.objects.all() if include_current: runs = runs.filter(endtime__gte=offset) else: runs = runs.filter(starttime__gte=offset) if delta: high_filter = runs.filter(endtime__lte=offset + delta) else: high_filter = runs count = high_filter.count() if max_runs is not None and count > max_runs: runs = runs[:max_runs] elif min_runs is not None and count < min_runs: runs = runs[:min_runs] else: runs = high_filter return runs def get_future_runs(**kwargs): return get_upcoming_runs(include_current=False, **kwargs) # TODO: why is this so complicated def upcoming_bid_filter(**kwargs): runs = [ run.id for run in get_upcoming_runs( SpeedRun.objects.filter(Q(bids__state='OPENED')).distinct(), **kwargs ) ] return Q(speedrun__in=runs) def get_upcoming_bids(**kwargs): return Bid.objects.filter(upcoming_bid_filter(**kwargs)) def future_bid_filter(**kwargs): return upcoming_bid_filter(include_current=False, **kwargs) # Gets all of the current prizes that are possible right now (and also _specific_ to right now) def concurrent_prizes_filter(runs): run_count = runs.count() if run_count == 0: return Q(id=None) start_time = runs[0].starttime end_time = runs.reverse()[0].endtime # TODO: with the other changes to the logic I'm not sure this is correct any more, but # it's only a rough guess so maybe it's ok - BC 12/2019 # ---- # yes, the filter query here is correct. We want to get all unwon prizes that _start_ before the last run in the list _ends_, and likewise all prizes that _end_ after the first run in the list _starts_. return Q(prizewinner__isnull=True) & ( Q(startrun__starttime__lte=end_time, endrun__endtime__gte=start_time) | Q(starttime__lte=end_time, endtime__gte=start_time) | Q( startrun__isnull=True, endrun__isnull=True, starttime__isnull=True, endtime__isnull=True, ) ) def current_prizes_filter(query_offset=None): offset = default_time(query_offset) return Q(prizewinner__isnull=True) & ( Q(startrun__starttime__lte=offset, endrun__endtime__gte=offset) | Q(starttime__lte=offset, endtime__gte=offset) | Q( startrun__isnull=True, endrun__isnull=True, starttime__isnull=True, endtime__isnull=True, ) ) def upcoming_prizes_filter(**kwargs): runs = get_upcoming_runs(**kwargs) return concurrent_prizes_filter(runs) def future_prizes_filter(**kwargs): return upcoming_prizes_filter(include_current=False, **kwargs) def todraw_prizes_filter(query_offset=None): offset = default_time(query_offset) return Q(state='ACCEPTED') & ( Q(prizewinner__isnull=True) & ( Q(endrun__endtime__lte=offset) | Q(endtime__lte=offset) | (Q(endtime=None) & Q(endrun=None)) ) ) def apply_feed_filter(query, model, feed_name, params=None, user=None): params = params or {} noslice = canonical_bool(params.pop('noslice', False)) user = user or AnonymousUser() if model == 'donation': query = donation_feed_filter(feed_name, noslice, params, query, user) elif model in ['bid', 'bidtarget', 'allbids']: query = bid_feed_filter(feed_name, noslice, params, query, user) elif model == 'run': query = run_feed_filter(feed_name, noslice, params, query) elif model == 'prize': query = prize_feed_filter(feed_name, noslice, params, query, user) elif model == 'event': query = event_feed_filter(feed_name, params, query) return query def event_feed_filter(feed_name, params, query): if feed_name == 'future': offsettime = default_time(params.get('time', None)) query = query.filter(datetime__gte=offsettime) return query def run_feed_filter(feed_name, noslice, params, query): if feed_name == 'current': query = get_upcoming_runs(**feed_params(noslice, params, {'runs': query})) elif feed_name == 'future': query = get_future_runs(**feed_params(noslice, params, {'runs': query})) return query def feed_params(noslice, params, init=None): call_params = init or {} if 'maxRuns' in params: call_params['max_runs'] = int(params['maxRuns']) if 'minRuns' in params: call_params['min_runs'] = int(params['minRuns']) if noslice: call_params['max_runs'] = None call_params['min_runs'] = None if 'delta' in params: call_params['delta'] = timedelta(minutes=int(params['delta'])) if 'time' in params: call_params['query_offset'] = default_time(params['time']) return call_params def bid_feed_filter(feed_name, noslice, params, query, user): if feed_name == 'all': if not user.has_perm('tracker.view_hidden'): raise PermissionDenied pass # no filtering required elif feed_name == 'open': query = query.filter(state='OPENED') elif feed_name == 'closed': query = query.filter(state='CLOSED') elif feed_name == 'current': query = query.filter(state='OPENED').filter( upcoming_bid_filter(**feed_params(noslice, params)) ) elif feed_name == 'future': query = query.filter(state='OPENED').filter( future_bid_filter(**feed_params(noslice, params)) ) elif feed_name == 'pending': if not user.has_perm('tracker.view_hidden'): raise PermissionDenied query = query.filter(state='PENDING') elif feed_name is None: query = query.filter(state__in=['OPENED', 'CLOSED']) else: raise ValueError(f'Unknown feed name `{feed_name}`') return query def donation_feed_filter(feed_name, noslice, params, query, user): if ( feed_name not in ['recent', 'toprocess', 'toread', 'all'] and feed_name is not None ): raise ValueError(f'Unknown feed name `{feed_name}`') if feed_name == 'recent': query = get_recent_donations( **feed_params(noslice, params, {'donations': query}) ) elif feed_name == 'toprocess': if not user.has_perm('tracker.view_comments'): raise PermissionDenied query = query.filter((Q(commentstate='PENDING') | Q(readstate='PENDING'))) elif feed_name == 'toread': query = query.filter(Q(readstate='READY')) if feed_name != 'all': query = query.filter(transactionstate='COMPLETED', testdonation=False) elif not user.has_perm('tracker.view_pending'): raise PermissionDenied return query def prize_feed_filter(feed_name, noslice, params, query, user): if feed_name == 'current': call_params = {} if 'time' in params: call_params['query_offset'] = default_time(params['time']) query = query.filter(current_prizes_filter(**call_params)) elif feed_name == 'future': query = query.filter(upcoming_prizes_filter(**feed_params(noslice, params))) elif feed_name == 'won': # TODO: are these used? doesn't seem to take multi-prizes into account query = query.filter(Q(prizewinner__isnull=False)) elif feed_name == 'unwon': query = query.filter(Q(prizewinner__isnull=True)) elif feed_name == 'todraw': query = query.filter(todraw_prizes_filter()) if feed_name != 'all': query = query.filter(state='ACCEPTED') elif not user.has_perm('tracker.change_prize'): raise PermissionDenied return query def canonical_bool(b): if isinstance(b, str): if b.lower() in ['t', 'True', 'true', 'y', 'yes']: b = True elif b.lower() in ['f', 'False', 'false', 'n', 'no']: b = False else: b = None return b def default_time(time): if time is None: time = datetime.now(tz=pytz.utc) elif isinstance(time, str): time = dateutil.parser.parse(time) return time.astimezone(pytz.utc)