def __init__(self, name): self.name = name.lower().strip().replace('#', '') self.base_key = 'tag:{}:posts'.format(self.name) self.new = RedisSortedSet(self.base_key) self.images_only = RedisLastBumpedBuffer(self.base_key + ':images', 1000) self.popular = RedisLastBumpedBuffer(self.base_key + ':popular', 1000) self.post_count = RedisKey(self.base_key + ':count')
def __init__(self, user_id, stream_size=1000, activity_types=ACTIVITY_TYPES): self._user_id = user_id self._activity_types = activity_types self._buffer = RedisLastBumpedBuffer( 'user:{}:stream_v6'.format(user_id), stream_size, getter=self._make_activity) self._read = RedisLastBumpedBuffer( 'user:{}:stream_read'.format(user_id), stream_size)
def __init__(self, user_id, stream_size=450, activity_types=ACTIVITY_TYPES, buffer_key_postfix='stream_v6'): self._activity_types = {} for activity_type in activity_types: if isinstance(activity_type, basestring): activity_type = _load_activity_type(activity_type) self._activity_types[activity_type.TYPE] = activity_type self._user_id = user_id self._buffer = RedisLastBumpedBuffer('user:{}:{}'.format(user_id, buffer_key_postfix), stream_size) #TODO if we add support for this in DrawQuest, we need to use one stream size for both # iPhone and iPad if those streams differ. self._read = RedisLastBumpedBuffer('user:{}:stream_read'.format(user_id), stream_size)
def _get_aggregate_rlbb(groups, nav): groups = list(groups) if not groups: return [] else: rlbb = RedisLastBumpedBuffer(gen_temp_key(), size=None) buffers = [_get_buffer(group, nav).key for group in groups] redis.zunionstore(rlbb.key, buffers, aggregate='MAX') return rlbb
def __init__(self, user_id, stream_size=450, activity_types=ACTIVITY_TYPES, buffer_key_postfix='stream_v6'): self._activity_types = {} for activity_type in activity_types: if isinstance(activity_type, basestring): activity_type = _load_activity_type(activity_type) self._activity_types[activity_type.TYPE] = activity_type self._user_id = user_id self._buffer = RedisLastBumpedBuffer( 'user:{}:{}'.format(user_id, buffer_key_postfix), stream_size) #TODO if we add support for this in DrawQuest, we need to use one stream size for both # iPhone and iPad if those streams differ. self._read = RedisLastBumpedBuffer( 'user:{}:stream_read'.format(user_id), stream_size)
class Tag(object): top = property(lambda self: DateKey(lambda key: RedisLastBumpedBuffer(key, 30*30), self.base_key, ':top')) updates_channel = property(lambda self: RealtimeChannel('tu:%s' % self.name, 5, ttl=24*60*60)) def __repr__(self): return self.name def __init__(self, name): self.name = name.lower().strip().replace('#', '') self.base_key = 'tag:{}:posts'.format(self.name) self.new = RedisSortedSet(self.base_key) self.images_only = RedisLastBumpedBuffer(self.base_key + ':images', 1000) self.popular = RedisLastBumpedBuffer(self.base_key + ':popular', 1000) self.post_count = RedisKey(self.base_key + ':count') def to_client(self, **kwargs): return self.name def tag_comment(self, comment, timestamp=None): if timestamp is None: timestamp = Services.time.time() self.new.zadd(int(comment.id), timestamp) all_tags.sadd(self.name) if comment.reply_content is not None: self.images_only.bump(int(comment.id), score=timestamp) count = self.post_count.incr() self.updates_channel.publish({'post': comment.id, 'tag': self.name, 'count': count}) def untag_comment(self, comment): self.new.zrem(comment.id) self.images_only.remove(comment.id) self.popular.remove(comment.id) def get_absolute_url(self): return '/x/' + self.name.replace('#','') def user_is_following(self, user): if not user.is_authenticated(): return False return self.name in user.redis.followed_tags def merge_top_scores(self, day=None): """ Merges daily top scores into monthly and monthly into yearly top scores for this group for the given day and the 365 days before it. If `day` is `None`, defaults to today. """ if not day: day = Services.time.today() # Merge today + last 365 days days = [day - datetime.timedelta(n) for n in range(366)] months = defaultdict(list) for day in days: months[(day.year, day.month)].append(day) years = defaultdict(list) for (year, month) in months.keys(): years[year].append(month) for (year, month), days in months.iteritems(): dest = self.top.month(datetime.date(year, month, 1)) source_keys = [self.top.day(day).key for day in days] redis.zunionstore(dest.key, source_keys, aggregate='max') dest.truncate(2) for year, year_months in years.iteritems(): dest = self.top.year(datetime.date(year, 1, 1)) source_keys = [self.top.month(datetime.date(year, month, 1)).key for month in year_months] redis.zunionstore(dest.key, source_keys, aggregate='max') dest.truncate(5)
import time from canvas.models import Visibility from canvas.redis_models import redis, RedisLastBumpedBuffer from drawquest import knobs from drawquest.apps.quests.details_models import QuestDetails top_quests_buffer = RedisLastBumpedBuffer('top_quests', 500) def get_quest_score(quest): try: details = quest.details() except AttributeError: details = quest if not quest.ugq and not quest.scheduledquest_set.exists(): return -1 visibility = getattr(details, 'visibility', None) if visibility in Visibility.invisible_choices or visibility == Visibility.CURATED: return -1 first_appeared_on = quest.first_appeared_on() if first_appeared_on is None: return -1 tdelta = time.time() - first_appeared_on + 10 * 60 weight = ((tdelta**1.9) if tdelta > 0 else 1) pop_score = details.drawing_count
class ActivityStream(object): ACTIVITY_TYPES = settings.ACTIVITY_TYPE_CLASSES def __init__(self, user_id, stream_size=450, activity_types=ACTIVITY_TYPES, buffer_key_postfix='stream_v6'): self._activity_types = {} for activity_type in activity_types: if isinstance(activity_type, basestring): activity_type = _load_activity_type(activity_type) self._activity_types[activity_type.TYPE] = activity_type self._user_id = user_id self._buffer = RedisLastBumpedBuffer('user:{}:{}'.format(user_id, buffer_key_postfix), stream_size) #TODO if we add support for this in DrawQuest, we need to use one stream size for both # iPhone and iPad if those streams differ. self._read = RedisLastBumpedBuffer('user:{}:stream_read'.format(user_id), stream_size) def _make_activity(self, activity_id): from apps.activity.models import Activity, LegacyActivity try: activity_data = Activity.details_by_id(activity_id)() except Activity.DoesNotExist: try: activity_data = LegacyActivity.details_by_id(activity_id)() except LegacyActivity.DoesNotExist: return None try: return self._activity_types[activity_data['activity_type']](activity_data) except KeyError as e: return None def _make_activities(self, activity_ids, earlier_than=None, later_than=None): from apps.activity.models import Activity, LegacyActivity def filter_by_ts(query): if earlier_than is not None: query = query.filter(timestamp__lt=earlier_than) if later_than is not None: query = query.filter(timestamp__gt=later_than) return query activity_ids = [int(id_) for id_ in activity_ids] activities = Activity.objects.filter(id__in=activity_ids).order_by('-timestamp') activities = filter_by_ts(activities) activities = CachedCall.queryset_details(activities) if len(activities) < len(activity_ids): legacy_ids = set(activity_ids) - set(int(activity['id']) for activity in activities) legacy_activities = LegacyActivity.objects.filter(id__in=legacy_ids).order_by('-timestamp') legacy_activities = filter_by_ts(legacy_activities) legacy_activities = CachedCall.queryset_details(legacy_activities) activities.extend(legacy_activities) ret = [] for activity_data in activities: try: ret.append(self._activity_types[activity_data['activity_type']](activity_data)) except KeyError as e: continue return ret def __iter__(self): for item_id in self._buffer: if item_id is not None: yield self._make_activity(item_id) def __getitem__(self, key): if not isinstance(key, slice): raise TypeError("ActivityStream is not indexable without using a slice.") return self._make_activities(self._buffer[key]) def valid_activity_type(self, recipient, activity): if hasattr(activity, 'APP_VERSION'): last_version = recipient.kv.last_app_version.get() try: if not last_version or util.parse_version(last_version) < activity.APP_VERSION: return False except ValueError: return False return activity.TYPE in self._activity_types def earlier_than(self, timestamp, num=None): """ Returns an iterator over the activities up until `timestamp`. """ start = None if num is None else 0 ids = self._buffer.zrevrangebyscore('({}'.format(timestamp), '-inf', start=start, num=num) return self._make_activities(ids) def later_than(self, timestamp, num=None): start = None if num is None else 0 ids = self._buffer.zrevrangebyscore('inf', '({}'.format(timestamp), start=start, num=num) return self._make_activities(ids) def _invalidate_cache(self): from apps.activity import api api.invalidate_caches(self._user_id) def push(self, activity_item): from apps.activity.models import Activity if activity_item.TYPE not in self._activity_types: return if not hasattr(activity_item, 'id'): dbactivity = Activity.from_redis_activity(activity_item) id_ = dbactivity.id else: id_ = activity_item.id self._buffer.bump(id_, coerce=False) self._invalidate_cache() def mark_read(self, activity_id): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) try: if (activity_id not in self._read and activity_id in set(self._buffer) and float(user_redis.user_kv.hget('activity_stream_last_viewed')) < float(activity.timestamp)): user_redis.user_kv.hincrby('activity_stream_unseen', -1) except TypeError: pass self._read.bump(activity_id) def mark_all_read(self): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) user_redis.user_kv.hset('activity_stream_last_viewed', Services.time.time()) user_redis.user_kv.hset('activity_stream_unseen', 0) user_redis.activity_stream_channel.publish('activity_stream_viewed') def has_read(self, activity_id): activity_id = int(activity_id) from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) if activity_id in self._read: return True try: return float(user_redis.user_kv.hget('activity_stream_last_viewed')) >= float(activity.timestamp) except TypeError: return False
def __init__(self, user_id, stream_size=1000, activity_types=ACTIVITY_TYPES): self._user_id = user_id self._activity_types = activity_types self._buffer = RedisLastBumpedBuffer('user:{}:stream_v6'.format(user_id), stream_size, getter=self._make_activity) self._read = RedisLastBumpedBuffer('user:{}:stream_read'.format(user_id), stream_size)
class ActivityStream(object): ACTIVITY_TYPES = settings.ACTIVITY_TYPE_CLASSES def __init__(self, user_id, stream_size=450, activity_types=ACTIVITY_TYPES, buffer_key_postfix='stream_v6'): self._activity_types = {} for activity_type in activity_types: if isinstance(activity_type, basestring): activity_type = _load_activity_type(activity_type) self._activity_types[activity_type.TYPE] = activity_type self._user_id = user_id self._buffer = RedisLastBumpedBuffer( 'user:{}:{}'.format(user_id, buffer_key_postfix), stream_size) #TODO if we add support for this in DrawQuest, we need to use one stream size for both # iPhone and iPad if those streams differ. self._read = RedisLastBumpedBuffer( 'user:{}:stream_read'.format(user_id), stream_size) def _make_activity(self, activity_id): from apps.activity.models import Activity, LegacyActivity try: activity_data = Activity.details_by_id(activity_id)() except Activity.DoesNotExist: try: activity_data = LegacyActivity.details_by_id(activity_id)() except LegacyActivity.DoesNotExist: return None try: return self._activity_types[activity_data['activity_type']]( activity_data) except KeyError as e: return None def _make_activities(self, activity_ids, earlier_than=None, later_than=None): from apps.activity.models import Activity, LegacyActivity def filter_by_ts(query): if earlier_than is not None: query = query.filter(timestamp__lt=earlier_than) if later_than is not None: query = query.filter(timestamp__gt=later_than) return query activity_ids = [int(id_) for id_ in activity_ids] activities = Activity.objects.filter( id__in=activity_ids).order_by('-timestamp') activities = filter_by_ts(activities) activities = CachedCall.queryset_details(activities) if len(activities) < len(activity_ids): legacy_ids = set(activity_ids) - set( int(activity['id']) for activity in activities) legacy_activities = LegacyActivity.objects.filter( id__in=legacy_ids).order_by('-timestamp') legacy_activities = filter_by_ts(legacy_activities) legacy_activities = CachedCall.queryset_details(legacy_activities) activities.extend(legacy_activities) ret = [] for activity_data in activities: try: ret.append(self._activity_types[activity_data['activity_type']] (activity_data)) except KeyError as e: continue return ret def __iter__(self): for item_id in self._buffer: if item_id is not None: yield self._make_activity(item_id) def __getitem__(self, key): if not isinstance(key, slice): raise TypeError( "ActivityStream is not indexable without using a slice.") return self._make_activities(self._buffer[key]) def valid_activity_type(self, recipient, activity): if hasattr(activity, 'APP_VERSION'): last_version = recipient.kv.last_app_version.get() try: if not last_version or util.parse_version( last_version) < activity.APP_VERSION: return False except ValueError: return False return activity.TYPE in self._activity_types def earlier_than(self, timestamp, num=None): """ Returns an iterator over the activities up until `timestamp`. """ start = None if num is None else 0 ids = self._buffer.zrevrangebyscore('({}'.format(timestamp), '-inf', start=start, num=num) return self._make_activities(ids) def later_than(self, timestamp, num=None): start = None if num is None else 0 ids = self._buffer.zrevrangebyscore('inf', '({}'.format(timestamp), start=start, num=num) return self._make_activities(ids) def _invalidate_cache(self): from apps.activity import api api.invalidate_caches(self._user_id) def push(self, activity_item): from apps.activity.models import Activity if activity_item.TYPE not in self._activity_types: return if not hasattr(activity_item, 'id'): dbactivity = Activity.from_redis_activity(activity_item) id_ = dbactivity.id else: id_ = activity_item.id self._buffer.bump(id_, coerce=False) self._invalidate_cache() def mark_read(self, activity_id): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) try: if (activity_id not in self._read and activity_id in set(self._buffer) and float( user_redis.user_kv.hget('activity_stream_last_viewed')) < float(activity.timestamp)): user_redis.user_kv.hincrby('activity_stream_unseen', -1) except TypeError: pass self._read.bump(activity_id) def mark_all_read(self): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) user_redis.user_kv.hset('activity_stream_last_viewed', Services.time.time()) user_redis.user_kv.hset('activity_stream_unseen', 0) user_redis.activity_stream_channel.publish('activity_stream_viewed') def has_read(self, activity_id): activity_id = int(activity_id) from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) if activity_id in self._read: return True try: return float(user_redis.user_kv.hget( 'activity_stream_last_viewed')) >= float(activity.timestamp) except TypeError: return False
class Tag(object): top = property(lambda self: DateKey(lambda key: RedisLastBumpedBuffer(key, 30*30), self.base_key, ':top')) updates_channel = property(lambda self: RealtimeChannel('tu:%s' % self.name, 5, ttl=24*60*60)) def __repr__(self): return self.name def __init__(self, name): self.name = name.lower().strip().replace('#', '') self.base_key = 'tag:{}:posts'.format(self.name) self.new = RedisSortedSet(self.base_key) self.images_only = RedisLastBumpedBuffer(self.base_key + ':images', 1000) self.popular = RedisLastBumpedBuffer(self.base_key + ':popular', 1000) self.post_count = RedisKey(self.base_key + ':count') def to_client(self): return self.name def tag_comment(self, comment, timestamp=None): if timestamp is None: timestamp = Services.time.time() self.new.zadd(int(comment.id), timestamp) all_tags.sadd(self.name) if comment.reply_content is not None: self.images_only.bump(int(comment.id), score=timestamp) count = self.post_count.incr() self.updates_channel.publish({'post': comment.id, 'tag': self.name, 'count': count}) def untag_comment(self, comment): self.new.zrem(comment.id) self.images_only.remove(comment.id) self.popular.remove(comment.id) def get_absolute_url(self): return '/x/' + self.name.replace('#','') def user_is_following(self, user): if not user.is_authenticated(): return False return self.name in user.redis.followed_tags def merge_top_scores(self, day=None): """ Merges daily top scores into monthly and monthly into yearly top scores for this group for the given day and the 365 days before it. If `day` is `None`, defaults to today. """ if not day: day = Services.time.today() # Merge today + last 365 days days = [day - datetime.timedelta(n) for n in range(366)] months = defaultdict(list) for day in days: months[(day.year, day.month)].append(day) years = defaultdict(list) for (year, month) in months.keys(): years[year].append(month) for (year, month), days in months.iteritems(): dest = self.top.month(datetime.date(year, month, 1)) source_keys = [self.top.day(day).key for day in days] redis.zunionstore(dest.key, source_keys, aggregate='max') dest.truncate(2) for year, year_months in years.iteritems(): dest = self.top.year(datetime.date(year, 1, 1)) source_keys = [self.top.month(datetime.date(year, month, 1)).key for month in year_months] redis.zunionstore(dest.key, source_keys, aggregate='max') dest.truncate(5)
class ActivityStream(object): ACTIVITY_TYPES = {cls.TYPE: cls for cls in _load_activity_types()} def __init__(self, user_id, stream_size=1000, activity_types=ACTIVITY_TYPES): self._user_id = user_id self._activity_types = activity_types self._buffer = RedisLastBumpedBuffer( 'user:{}:stream_v6'.format(user_id), stream_size, getter=self._make_activity) self._read = RedisLastBumpedBuffer( 'user:{}:stream_read'.format(user_id), stream_size) def _make_activity(self, activity_id): from apps.activity.models import Activity activity_data = Activity.details_by_id(activity_id)() try: return self._activity_types[activity_data['activity_type']]( activity_data) except KeyError: return None def __iter__(self): for item in self._buffer: if item is not None: yield item def valid_activity_type(self, activity_type): return activity_type in self.ACTIVITY_TYPES def iter_until(self, timestamp): """ Returns an iterator over the activities up until `timestamp`. """ return itertools.dropwhile( lambda activity: activity.timestamp >= float(timestamp), self._buffer) def _invalidate_cache(self): if settings.PROJECT == 'drawquest': from apps.activity import api api.activity_stream_items.delete_cache(None, None, user=self._user_id) def push(self, activity_item): from apps.activity.models import Activity if not hasattr(activity_item, 'id'): dbactivity = Activity.from_redis_activity(activity_item) id_ = dbactivity.id else: id_ = activity_item.id self._buffer.bump(id_, coerce=False) self._invalidate_cache() def mark_read(self, activity_id): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) try: if (activity_id not in self._read and activity_id in set(item.id for item in self._buffer) and float( user_redis.user_kv.hget('activity_stream_last_viewed')) < float(activity.timestamp)): user_redis.user_kv.hincrby('activity_stream_unseen', -1) except TypeError: pass self._read.bump(activity_id) def mark_all_read(self): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) user_redis.user_kv.hset('activity_stream_last_viewed', Services.time.time()) user_redis.user_kv.hset('activity_stream_unseen', 0) user_redis.activity_stream_channel.publish('activity_stream_viewed') def has_read(self, activity_id): activity_id = int(activity_id) from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) if activity_id in self._read: return True try: return float(user_redis.user_kv.hget( 'activity_stream_last_viewed')) >= float(activity.timestamp) except TypeError: return False
def after_setUp(self): redis.delete('rblf_key') self.lbf = RedisLastBumpedBuffer('rblf_key', 3)
class TestRedisLastBumpedBuffer(CanvasTestCase): def after_setUp(self): redis.delete('rblf_key') self.lbf = RedisLastBumpedBuffer('rblf_key', 3) def set_common_data(self): self.lbf.bump(1, 0.1) self.lbf.bump(2, 0.9) self.lbf.bump(3, 0.4) self.lbf.bump(4, 1.0) def test_bump_and_get_back_bumped_id(self): self.lbf.bump(10, 1.23) self.assertEquals([10], self.lbf[:]) def test_bump_thrice_and_get_ids_in_decreasing_value_order(self): self.lbf.bump(10, 1.0) self.lbf.bump(11, 1.1) self.lbf.bump(9, 1.2) self.assertEquals([9, 11, 10], self.lbf[:]) def test_bumped_four_times_and_get_top_3_ids(self): self.set_common_data() self.assertEquals([4, 2, 3], self.lbf[:]) def test_get_the_top_2_via_slicing(self): self.set_common_data() self.assertEquals([4, 2], self.lbf[:2]) self.assertEquals([4, 2], self.lbf[0:2]) def test_get_the_the_back_2_via_slicing(self): self.set_common_data() self.assertEquals([2, 3], self.lbf[1:3]) def test_ten_bumps_still_three_items(self): for x in range(10): self.lbf.bump(x, x) self.assertEquals([9, 8, 7], self.lbf[:])
class TestRedisLastBumpedBuffer(CanvasTestCase): def after_setUp(self): redis.delete('rblf_key') self.lbf = RedisLastBumpedBuffer('rblf_key', 3) def set_common_data(self): self.lbf.bump(1, 0.1) self.lbf.bump(2, 0.9) self.lbf.bump(3, 0.4) self.lbf.bump(4, 1.0) def test_bump_and_get_back_bumped_id(self): self.lbf.bump(10, 1.23) self.assertEquals([10], self.lbf[:]) def test_bump_thrice_and_get_ids_in_decreasing_value_order(self): self.lbf.bump(10, 1.0) self.lbf.bump(11, 1.1) self.lbf.bump(9, 1.2) self.assertEquals([9, 11, 10], self.lbf[:]) def test_bumped_four_times_and_get_top_3_ids(self): self.set_common_data() self.assertEquals([4, 2, 3], self.lbf[:]) def test_get_the_top_2_via_slicing(self): self.set_common_data() self.assertEquals([4, 2], self.lbf[:2]) self.assertEquals([4, 2], self.lbf[0:2]) def test_get_the_the_back_2_via_slicing(self): self.set_common_data() self.assertEquals([2, 3], self.lbf[1:3]) def test_ten_bumps_still_three_items(self): for x in range(10): self.lbf.bump(x, x) self.assertEquals([9,8,7], self.lbf[:])
class ActivityStream(object): ACTIVITY_TYPES = {cls.TYPE: cls for cls in _load_activity_types()} def __init__(self, user_id, stream_size=1000, activity_types=ACTIVITY_TYPES): self._user_id = user_id self._activity_types = activity_types self._buffer = RedisLastBumpedBuffer('user:{}:stream_v6'.format(user_id), stream_size, getter=self._make_activity) self._read = RedisLastBumpedBuffer('user:{}:stream_read'.format(user_id), stream_size) def _make_activity(self, activity_id): from apps.activity.models import Activity activity_data = Activity.details_by_id(activity_id)() try: return self._activity_types[activity_data['activity_type']](activity_data) except KeyError: return None def __iter__(self): for item in self._buffer: if item is not None: yield item def valid_activity_type(self, activity_type): return activity_type in self.ACTIVITY_TYPES def iter_until(self, timestamp): """ Returns an iterator over the activities up until `timestamp`. """ return itertools.dropwhile(lambda activity: activity.timestamp >= float(timestamp), self._buffer) def _invalidate_cache(self): if settings.PROJECT == 'drawquest': from apps.activity import api api.activity_stream_items.delete_cache(None, None, user=self._user_id) def push(self, activity_item): from apps.activity.models import Activity if not hasattr(activity_item, 'id'): dbactivity = Activity.from_redis_activity(activity_item) id_ = dbactivity.id else: id_ = activity_item.id self._buffer.bump(id_, coerce=False) self._invalidate_cache() def mark_read(self, activity_id): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) try: if (activity_id not in self._read and activity_id in set(item.id for item in self._buffer) and float(user_redis.user_kv.hget('activity_stream_last_viewed')) < float(activity.timestamp)): user_redis.user_kv.hincrby('activity_stream_unseen', -1) except TypeError: pass self._read.bump(activity_id) def mark_all_read(self): from canvas.models import UserRedis user_redis = UserRedis(self._user_id) user_redis.user_kv.hset('activity_stream_last_viewed', Services.time.time()) user_redis.user_kv.hset('activity_stream_unseen', 0) user_redis.activity_stream_channel.publish('activity_stream_viewed') def has_read(self, activity_id): activity_id = int(activity_id) from canvas.models import UserRedis user_redis = UserRedis(self._user_id) activity = self._make_activity(activity_id) if activity_id in self._read: return True try: return float(user_redis.user_kv.hget('activity_stream_last_viewed')) >= float(activity.timestamp) except TypeError: return False