def test_inactive_challenge(app): setup_challenges(app) with app.app_context(): db = get_db() redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) with db.scoped_session() as session: mgr = ChallengeManager("some_inactive_challenge", DefaultUpdater()) TEST_EVENT = "TEST_EVENT" bus.register_listener(TEST_EVENT, mgr) with bus.use_scoped_dispatch_queue(): bus.dispatch(TEST_EVENT, 100, 1, {}) bus.process_events(session) state = mgr.get_user_challenge_state(session, ["1"]) # We should not have any UserChallenges created for the # inactive challenge!! assert len(state) == 0
def test_rejects_invalid_events(app): setup_challenges(app) with app.app_context(): db = get_db() redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) with db.scoped_session() as session: mgr = ChallengeManager("test_challenge_1", DefaultUpdater()) TEST_EVENT = "TEST_EVENT" bus.register_listener(TEST_EVENT, mgr) with bus.use_scoped_dispatch_queue(): bus.dispatch(TEST_EVENT, None, 1) bus.dispatch(TEST_EVENT, 1, None) bus.dispatch(TEST_EVENT, 1, 1, 1) (count, did_error) = bus.process_events(session) assert count == 0 assert did_error == False
def test_in_memory_queue(app): setup_challenges(app) with app.app_context(): db = get_db() redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) with db.scoped_session() as session, bus.use_scoped_dispatch_queue(): agg_challenge = ChallengeManager("test_challenge_3", AggregateUpdater()) agg_challenge.process(session, "test_event", []) TEST_EVENT = "TEST_EVENT" bus.register_listener( TEST_EVENT, ChallengeManager("test_challenge_1", DefaultUpdater())) bus.register_listener( TEST_EVENT, ChallengeManager("test_challenge_2", DefaultUpdater())) # - Multiple events with the same user_id but diff specifiers get created bus.register_listener(TEST_EVENT, agg_challenge) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 2}) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 3}) bus.process_events(session) # no events should be processed because we haven't dispatched yet state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) assert len(state) == 0 bus.process_events(session) state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) assert len(state) == 2 # Also make sure the thing is incomplete res = get_challenges(1, False, session, bus) agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] assert agg_chal["is_complete"] == False redis_conn = redis.Redis.from_url(url=REDIS_URL)
).all()) follows_counter = Counter(map(lambda x: x.user_id, favorites)) for completion in partial_completions: completion.favorites = (follows_counter[completion.user_id] >= FAVORITES_THRESHOLD) def _get_steps_complete(self, partial_challenge): return (partial_challenge.profile_description + partial_challenge.profile_name + partial_challenge.profile_picture + partial_challenge.profile_cover_photo + partial_challenge.follows + partial_challenge.favorites + partial_challenge.reposts) profile_challenge_manager = ChallengeManager("profile-completion", ProfileChallengeUpdater()) # Accessors def get_profile_completion_challenges(session, user_ids): return (session.query(ProfileCompletionChallenge).filter( ProfileCompletionChallenge.user_id.in_(user_ids)).all()) def get_user_dicts(session, user_ids): res = (session.query( User.bio, User.name, User.profile_picture, User.profile_picture_sizes, User.cover_photo,
from typing import List, Optional from sqlalchemy.orm.session import Session from src.challenges.challenge import ( ChallengeManager, ChallengeUpdater, FullEventMetadata, UserChallenge, ) class MobileInstallChallengeUpdater(ChallengeUpdater): def update_user_challenges( self, session: Session, event: str, user_challenges: List[UserChallenge], step_count: Optional[int], event_metadatas: List[FullEventMetadata], starting_block: Optional[int], ): # We only fire the event if the user logged in on mobile for user_challenge in user_challenges: user_challenge.is_complete = True mobile_install_challenge_manager = ChallengeManager( "mobile-install", MobileInstallChallengeUpdater())
def should_create_new_challenge(self, session, event: str, user_id: int, extra: Dict) -> bool: return does_user_exist_with_verification_status(session, user_id, True) def should_show_challenge_for_user(self, session: Session, user_id: int) -> bool: return does_user_exist_with_verification_status(session, user_id, True) class ReferredChallengeUpdater(ChallengeUpdater): def update_user_challenges( self, session: Session, event: str, user_challenges: List[UserChallenge], step_count: Optional[int], event_metadatas: List[FullEventMetadata], starting_block: Optional[int], ): for user_challenge in user_challenges: user_challenge.is_complete = True referral_challenge_manager = ChallengeManager("referrals", ReferralChallengeUpdater()) verified_referral_challenge_manager = ChallengeManager( "ref-v", VerifiedReferralChallengeUpdater()) referred_challenge_manager = ChallengeManager("referred", ReferredChallengeUpdater())
from typing import List, Optional from sqlalchemy.orm.session import Session from src.challenges.challenge import ( ChallengeManager, ChallengeUpdater, FullEventMetadata, ) from src.models.models import UserChallenge class SendFirstTipChallengeUpdater(ChallengeUpdater): def update_user_challenges( self, session: Session, event: str, user_challenges: List[UserChallenge], step_count: Optional[int], event_metadatas: List[FullEventMetadata], starting_block: Optional[int], ): # Update the user_challenges for user_challenge in user_challenges: # Update completion user_challenge.is_complete = True send_first_tip_challenge_manager = ChallengeManager( "send-first-tip", SendFirstTipChallengeUpdater())
def test_catches_exceptions_in_single_processor(app): """Ensure that if a single processor fails, the others still succeed""" with app.app_context(): db = get_db() redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) with db.scoped_session() as session: session.add_all([ Challenge( id="test_challenge_1", type=ChallengeType.numeric, amount="5", step_count=3, active=True, ), Challenge( id="test_challenge_2", type=ChallengeType.numeric, amount="5", step_count=3, active=True, ), ]) session.commit() correct_manager = ChallengeManager("test_challenge_1", DefaultUpdater()) broken_manager = ChallengeManager("test_challenge_2", BrokenUpdater()) TEST_EVENT = "TEST_EVENT" TEST_EVENT_2 = "TEST_EVENT_2" bus.register_listener(TEST_EVENT, correct_manager) bus.register_listener(TEST_EVENT_2, broken_manager) with bus.use_scoped_dispatch_queue(): # dispatch the broken one first bus.dispatch(TEST_EVENT_2, 101, 1) bus.dispatch(TEST_EVENT, 101, 1) try: bus.process_events(session) except: # pylint: disable=W0707 raise Exception("Shouldn't have propogated error!") challenge_1_state = correct_manager.get_user_challenge_state( session, ["1"]) # Make sure that the 'correct_manager' still executes assert len(challenge_1_state) == 1 assert challenge_1_state[0].current_step_count == 1 # Make sure broken manager didn't do anything challenge_2_state = broken_manager.get_user_challenge_state( session, ["1"]) assert len(challenge_2_state) == 0 # Try the other order with bus.use_scoped_dispatch_queue(): # dispatch the correct one first bus.dispatch(TEST_EVENT, 101, 1) bus.dispatch(TEST_EVENT_2, 101, 1) try: bus.process_events(session) except: # pylint: disable=W0707 raise Exception("Shouldn't have propogated error!") challenge_1_state = correct_manager.get_user_challenge_state( session, ["1"]) assert len(challenge_1_state) == 1 assert challenge_1_state[0].current_step_count == 2 # Make sure broken manager didn't do anything challenge_2_state = broken_manager.get_user_challenge_state( session, ["1"]) assert len(challenge_2_state) == 0
def test_aggregates(app): setup_challenges(app) with app.app_context(): db = get_db() redis_conn = redis.Redis.from_url(url=REDIS_URL) with db.scoped_session() as session: bus = ChallengeEventBus(redis_conn) agg_challenge = ChallengeManager("test_challenge_3", AggregateUpdater()) agg_challenge.process(session, "test_event", []) TEST_EVENT = "TEST_EVENT" bus.register_listener( TEST_EVENT, ChallengeManager("test_challenge_1", DefaultUpdater())) bus.register_listener( TEST_EVENT, ChallengeManager("test_challenge_2", DefaultUpdater())) # - Multiple events with the same user_id but diff specifiers get created bus.register_listener(TEST_EVENT, agg_challenge) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 2}) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 3}) bus.flush() bus.process_events(session) state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) assert len(state) == 2 # Also make sure the thing is incomplete res = get_challenges(1, False, session, bus) agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] assert agg_chal["is_complete"] == False # - Multiple events with the same specifier get deduped bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 4}) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 4}) bus.flush() bus.process_events(session) state = agg_challenge.get_user_challenge_state(session, ["1-4"]) assert len(state) == 1 # - If we've maxed the # of challenges, don't create any more bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 5}) bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 6}) bus.flush() bus.process_events(session) def get_user_challenges(): return (session.query(UserChallenge).filter( UserChallenge.challenge_id == "test_challenge_3", UserChallenge.user_id == 1, ).all()) assert len(get_user_challenges()) == 5 bus.dispatch(TEST_EVENT, 100, 1, {"referred_id": 7}) bus.flush() bus.process_events(session) assert len(get_user_challenges()) == 5 # Test get_challenges res = get_challenges(1, False, session, bus) agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] assert agg_chal["is_complete"] == True # Assert all user challenges have proper finishing block # user_challenges = get_user_challenges() for uc in user_challenges: assert uc.completed_blocknumber == 100
from typing import List, Optional from sqlalchemy.orm.session import Session from src.challenges.challenge import ( ChallengeManager, ChallengeUpdater, FullEventMetadata, ) from src.models.models import UserChallenge class ConnectVerifiedChallengeUpdater(ChallengeUpdater): """Updates a connect verified challenge.""" def update_user_challenges( self, session: Session, event: str, user_challenges: List[UserChallenge], step_count: Optional[int], event_metadatas: List[FullEventMetadata], starting_block: Optional[int], ): # Update the user_challenges for user_challenge in user_challenges: # Update completion user_challenge.is_complete = True connect_verified_challenge_manager = ChallengeManager( "connect-verified", ConnectVerifiedChallengeUpdater())
def test_handle_event(app): setup_challenges(app) with app.app_context(): db = get_db() populate_mock_db_blocks(db, 99, 110) with db.scoped_session() as session: my_challenge = ChallengeManager("test_challenge_1", TestUpdater()) # First try an event with a insufficient block_number # to ensure that nothing happens my_challenge.process( session, "test_event", [ { "user_id": 1, "block_number": 99, "extra": {} }, ], ) session.flush() actual = (session.query(UserChallenge).filter( UserChallenge.challenge_id == "test_challenge_1", UserChallenge.user_id == 1, ).first()) expected = { "challenge_id": "test_challenge_1", "user_id": 1, "specifier": "1", "is_complete": False, "current_step_count": 1, "completed_blocknumber": None, } assert model_to_dictionary(actual) == expected # Now process events and make sure things change as expected my_challenge.process( session, "test_event", [ { "user_id": 1, "block_number": 100, "extra": {} }, { "user_id": 2, "block_number": 100, "extra": {} }, { "user_id": 3, "block_number": 100, "extra": {} }, # Attempt to add id 6 twice to # ensure that it doesn't cause a collision { "user_id": 6, "block_number": 100, "extra": {} }, { "user_id": 6, "block_number": 100, "extra": {} }, ], ) session.flush() updated_complete = (session.query(UserChallenge).filter( UserChallenge.challenge_id == "test_challenge_1").all()) res_dicts = list(map(model_to_dictionary, updated_complete)) expected = [ # Should have incremented step count + 1 { "challenge_id": "test_challenge_1", "user_id": 1, "specifier": "1", "is_complete": False, "current_step_count": 2, "completed_blocknumber": None, }, # Should be unchanged b/c it was already complete { "challenge_id": "test_challenge_1", "user_id": 2, "specifier": "2", "is_complete": True, "current_step_count": 3, "completed_blocknumber": 100, }, # Should be newly complete { "challenge_id": "test_challenge_1", "user_id": 3, "specifier": "3", "is_complete": True, "current_step_count": 3, "completed_blocknumber": 100, }, # Should be untouched bc user 5 wasn't included { "challenge_id": "test_challenge_1", "user_id": 5, "specifier": "5", "is_complete": False, "current_step_count": 2, "completed_blocknumber": None, }, # Should have created a brand new user 6 { "challenge_id": "test_challenge_1", "user_id": 6, "specifier": "6", "is_complete": False, "current_step_count": 1, "completed_blocknumber": None, }, ] assert expected == res_dicts
def setup_db(session): blocks = [Block(blockhash="0x1", number=1, parenthash="", is_current=True)] users = [ User( blockhash="0x1", blocknumber=1, user_id=1, is_current=True, wallet="0x38C68fF3926bf4E68289672F75ee1543117dD9B3", created_at=datetime.now(), updated_at=datetime.now(), ) ] challenges = [ Challenge( id="boolean_challenge_1", type=ChallengeType.boolean, active=True, amount="5", ), Challenge( id="boolean_challenge_2", type=ChallengeType.boolean, active=True, amount="5", ), Challenge( id="boolean_challenge_3", type=ChallengeType.boolean, active=True, amount="5", ), # No progress on this, but active # should be returned Challenge( id="boolean_challenge_4", type=ChallengeType.boolean, active=True, amount="5", ), # Inactive, with no progress Challenge( id="boolean_challenge_5", type=ChallengeType.boolean, active=False, amount="5", ), # Inactive, WITH progress Challenge( id="boolean_challenge_6", type=ChallengeType.boolean, active=False, amount="5", ), Challenge( id="trending_challenge_1", type=ChallengeType.trending, active=True, amount="5", ), Challenge( id="aggregate_challenge_1", type=ChallengeType.aggregate, active=True, amount="5", step_count=3, ), Challenge( id="aggregate_challenge_2", type=ChallengeType.aggregate, active=True, amount="5", step_count=2, ), Challenge( id="aggregate_challenge_3", type=ChallengeType.aggregate, active=True, amount="5", step_count=2, ), Challenge(id="trending_1", type=ChallengeType.trending, active=True, amount="5"), Challenge(id="trending_2", type=ChallengeType.trending, active=True, amount="5"), Challenge(id="trending_3", type=ChallengeType.trending, active=True, amount="5"), ] user_challenges = [ # Finished the first challenge, disbursed UserChallenge( challenge_id="boolean_challenge_1", user_id=1, specifier="1", is_complete=True, ), # Did finish the second challenge, did not disburse UserChallenge( challenge_id="boolean_challenge_2", user_id=1, specifier="1", is_complete=True, ), # Did not finish challenge 3 UserChallenge( challenge_id="boolean_challenge_3", user_id=1, specifier="1", is_complete=False, ), # Inactive challenge UserChallenge( challenge_id="boolean_challenge_6", user_id=1, specifier="1", is_complete=True, ), UserChallenge( challenge_id="aggregate_challenge_1", user_id=1, specifier="1-2", # compound specifiers, like if user1 invites user2 is_complete=True, ), # Ensure that a non-complete user-challenge isn't counted towards # aggregate challenge score UserChallenge( challenge_id="aggregate_challenge_1", user_id=1, specifier="1-3", is_complete=False, ), UserChallenge( challenge_id="aggregate_challenge_2", user_id=1, specifier="1-2", is_complete=True, ), UserChallenge( challenge_id="aggregate_challenge_2", user_id=1, specifier="1-3", is_complete=True, ), # Trending 1 should be finished and included UserChallenge( challenge_id="trending_1", user_id=1, specifier="06-01-2020", is_complete=True, ), # Trending 2 should not be included UserChallenge( challenge_id="trending_2", user_id=1, specifier="06-01-2020", is_complete=False, ), ] disbursements = [ ChallengeDisbursement( challenge_id="boolean_challenge_1", user_id=1, amount="5", signature="1", slot=1, specifier="1", ) ] # Wipe any existing challenges in the DB from running migrations, etc session.query(Challenge).delete() session.commit() session.add_all(blocks) session.commit() session.add_all(users) session.commit() session.add_all(challenges) session.commit() session.add_all(user_challenges) session.commit() session.add_all(disbursements) redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) challenge_types = [ "boolean_challenge_1", "boolean_challenge_2", "boolean_challenge_3", "boolean_challenge_4", "boolean_challenge_5", "boolean_challenge_6", "trending_challenge_1", "aggregate_challenge_1", "aggregate_challenge_2", "aggregate_challenge_3", "trending_1", "trending_2", "trending_3", ] for ct in challenge_types: bus.register_listener( DEFAULT_EVENT, ChallengeManager(ct, DefaultUpdater()), ) return bus
def setup_extra_metadata_test(session): blocks = [Block(blockhash="0x1", number=1, parenthash="", is_current=True)] users = [ User( blockhash="0x1", blocknumber=1, user_id=1, is_current=True, wallet="0x38C68fF3926bf4E68289672F75ee1543117dD9B3", created_at=datetime.now(), updated_at=datetime.now(), ) ] challenges = [ # Test numeric challenges # Numeric 1 with default extra data, no completion Challenge( id="numeric_1", type=ChallengeType.numeric, active=True, amount="5", step_count=5, ), # Numeric 2 with some extra data Challenge( id="numeric_2", type=ChallengeType.numeric, active=True, amount="5", step_count=5, ), ] user_challenges = [ UserChallenge( challenge_id="numeric_2", user_id=1, specifier="1", is_complete=False, current_step_count=5, ), ] session.query(Challenge).delete() session.commit() session.add_all(blocks) session.commit() session.add_all(users) session.commit() session.add_all(challenges) session.commit() session.add_all(user_challenges) session.commit() redis_conn = redis.Redis.from_url(url=REDIS_URL) bus = ChallengeEventBus(redis_conn) bus.register_listener( DEFAULT_EVENT, ChallengeManager("numeric_1", NumericCustomUpdater())) bus.register_listener( DEFAULT_EVENT, ChallengeManager("numeric_2", NumericCustomUpdater())) return bus
# Update the user_challenges for user_challenge in user_challenges: # Update step count user_challenge.current_step_count = num_tracks_per_user[ user_challenge.user_id] # Update completion user_challenge.is_complete = ( user_challenge.current_step_count is not None and user_challenge.current_step_count >= step_count) def _get_num_track_uploads_by_user(self, session: Session, user_challenges: List[UserChallenge], block_number: int): user_ids = [ user_challenge.user_id for user_challenge in user_challenges ] tracks = (session.query(Track).filter( Track.owner_id.in_(user_ids), Track.blocknumber >= block_number, Track.is_current == True, Track.is_delete == False, Track.is_unlisted == False, Track.stem_of == None, ).all()) num_tracks_per_user = Counter(map(lambda t: t.owner_id, tracks)) return num_tracks_per_user track_upload_challenge_manager = ChallengeManager( "track-upload", TrackUploadChallengeUpdater())
partial_completion.listen_streak = 1 # If last timestamp is more than 24 hours ago, update streak elif new_date - last_date >= timedelta(days=1): partial_completion.last_listen_date = new_date # Check if the user lost their streak if new_date - last_date >= timedelta(days=2): partial_completion.listen_streak = 1 else: partial_completion.listen_streak += 1 def get_override_challenge_step_count( self, session: Session, user_id: int ) -> Optional[int]: return get_listen_streak_override(session, user_id) listen_streak_challenge_manager = ChallengeManager( "listen-streak", ListenStreakChallengeUpdater() ) # Accessors def get_listen_streak_challenges( session: Session, user_ids: List[int] ) -> List[ListenStreakChallenge]: return ( session.query(ListenStreakChallenge) .filter(ListenStreakChallenge.user_id.in_(user_ids)) .all() )
user_id=metadata["extra"]["user_id"], id=metadata["extra"]["id"], rank=metadata["extra"]["rank"], type=metadata["extra"]["type"], version=metadata["extra"]["version"], week=metadata["extra"]["week"], ) for metadata in metadatas ] session.add_all(trending_results) def generate_specifier(self, user_id: int, extra: Dict) -> str: return f"{extra['week']}:{extra['rank']}" trending_track_challenge_manager = ChallengeManager("tt", TrendingChallengeUpdater()) trending_underground_track_challenge_manager = ChallengeManager( "tut", TrendingChallengeUpdater() ) trending_playlist_challenge_manager = ChallengeManager("tp", TrendingChallengeUpdater()) def is_dst(zonename, dt): """Checks if is daylight savings time During daylight savings, the clock moves forward one hr """ tz = pytz.timezone(zonename) localized = pytz.utc.localize(dt) return localized.astimezone(tz).dst() != timedelta(0)