Models
Quest
<code>class Quest(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
</code>
<code>class Quest(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
</code>
class Quest(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
Badge
<code>class Badge(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
</code>
<code>class Badge(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
</code>
class Badge(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
criteria = models.JSONField() # Store criteria as JSON
reward_points = models.IntegerField(default=0)
def __str__(self):
return self.name
UserQuestProgress
<code>class UserQuestProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
quest = models.ForeignKey(Quest, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
</code>
<code>class UserQuestProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
quest = models.ForeignKey(Quest, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
</code>
class UserQuestProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
quest = models.ForeignKey(Quest, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
UserBadgeProgress
<code>class UserBadgeProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
</code>
<code>class UserBadgeProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
</code>
class UserBadgeProgress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, on_delete=models.CASCADE)
current_value = models.IntegerField(default=0)
target_value = models.IntegerField()
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.current_value >= self.target_value:
self.completed = True
if not self.completed_at:
self.completed_at = timezone.now()
super().save(*args, **kwargs)
I’m working on a social media platform where users can earn rewards (quests and badges) based on their activities. My goal is to create a flexible system that can evaluate these rewards dynamically, without needing to hardcode new conditions every time we introduce a new quest or badge.
What We Have Done So Far:
- Models for Quests and Badges: We have defined models for
Quest
,Badge
,UserQuestProgress
, andUserBadgeProgress
which track the user’s progress. - Criteria JSON: We use JSON to define the criteria for each quest or badge. For example:
<code>{"Post": {"reaction_type_within_timeframe": ["dislike", 30, 50]}}</code><code>{ "Post": { "reaction_type_within_timeframe": ["dislike", 30, 50] } } </code>
{ "Post": { "reaction_type_within_timeframe": ["dislike", 30, 50] } }
- Evaluation Function: We have an
evaluate_quest
function that processes the criteria JSON to compute the user’s progress:<code>def evaluate_quest(user, criteria):# Evaluate criteria and calculate progress</code><code>def evaluate_quest(user, criteria): # Evaluate criteria and calculate progress </code>def evaluate_quest(user, criteria): # Evaluate criteria and calculate progress
- Celery Task: When a user navigates to the quest or badge page, a Celery task is triggered to evaluate their progress and update
UserQuestProgress
orUserBadgeProgress
.
Current Challenges:
- Hardcoding Scenarios: The current implementation still requires us to hardcode new scenarios in the
evaluate_quest
function. We aim to handle various complex conditions dynamically but are struggling with unknown future scenarios. - Example Scenarios:
- Post a status update or any other post for the first time:
<code>{"Post": {"first_post": true}}</code><code>{ "Post": { "first_post": true } } </code>
{ "Post": { "first_post": true } }
- You have completed all your profile information fields:
<code>{"Profile": {"complete_profile": true}}</code><code>{ "Profile": { "complete_profile": true } } </code>
{ "Profile": { "complete_profile": true } }
- Give 50 like and/or love reactions on your followers’ posts:
<code>{"Reaction": {"reaction_type": ["like", "love"],"count": 50,"on_followers_posts": true}}</code><code>{ "Reaction": { "reaction_type": ["like", "love"], "count": 50, "on_followers_posts": true } } </code>
{ "Reaction": { "reaction_type": ["like", "love"], "count": 50, "on_followers_posts": true } }
- Received 100+ likes and 50+ comments on a single post:
<code>{"Post": {"likes": 100,"comments": 50,"single_post": true}}</code><code>{ "Post": { "likes": 100, "comments": 50, "single_post": true } } </code>
{ "Post": { "likes": 100, "comments": 50, "single_post": true } }
- The user reacted first on 50+ friends’ posts:
<code>{"Reaction": {"first_reaction": true,"on_friends_posts": true,"count": 50}}</code><code>{ "Reaction": { "first_reaction": true, "on_friends_posts": true, "count": 50 } } </code>
{ "Reaction": { "first_reaction": true, "on_friends_posts": true, "count": 50 } }
- Post a status update or any other post for the first time:
Workflow:
- When the user navigates to the quest or badge page, a Celery task starts.
- The task uses
get_or_create(user, quest|badge)
to ensure there is a progress instance. - The
evaluate_quest
function is called to compute the current and target values. - The
UserQuestProgress
andUserBadgeProgress
models have overriddensave
methods to mark completion if the current value meets or exceeds the target value. - Upon saving, a
post_save
signal notifies the user via WebSocket if a quest or badge is completed.
Current evaluate_quest
Function:
<code>def evaluate_quest(user, criteria):
criteria = json.loads(criteria)
progress = 0
total_conditions = 0
for model_name, conditions in criteria.items():
Model = apps.get_model('your_app_name', model_name) # Dynamically get the model
queryset = Model.objects.filter(user=user)
for field, condition in conditions.items():
if field == 'time_range':
start_time, end_time, target = condition
queryset = queryset.filter(
created_at__time__gte=start_time,
created_at__time__lte=end_time
)
current_value = queryset.count()
elif field == 'reaction_type_within_timeframe':
reaction_type, timeframe, target = condition
time_delta = timedelta(minutes=timeframe)
queryset = queryset.annotate(
reactions_within_timeframe=Count(
Case(
When(
reactions__reaction_type=reaction_type,
reactions__created_at__lte=ExpressionWrapper(
F('created_at') + time_delta,
output_field=DurationField()
),
then='reactions'
)
)
)
)
current_value = queryset.filter(reactions_within_timeframe__gte=target).count()
elif field == 'consecutive_days':
target_days = condition
dates = queryset.values_list('created_at', flat=True)
dates = sorted(dates)
current_streak = 0
max_streak = 0
for i in range(1, len(dates)):
if (dates[i] - dates[i - 1]).days == 1:
current_streak += 1
else:
max_streak = max(max_streak, current_streak)
current_streak = 0
max_streak = max(max_streak, current_streak)
current_value = max_streak
target = target_days
elif field == 'linked_social_networks':
target_value = condition
social_fields = [
'facebook_username', 'twitter_username', 'instagram_username',
'twitch_username', 'google_username', 'youtube_username',
'patreon_username', 'discord_username', 'deviantart_username',
'behance_username', 'dribble_username', 'artstation_username'
]
current_value = sum(bool(getattr(queryset.first(), field)) for field in social_fields)
target = target_value
else:
value, target = condition
if isinstance(value, str):
queryset = queryset.filter(**{field: value})
elif isinstance(value, dict) and 'range' in value:
start, end = value['range']
queryset = queryset.filter(**{f"{field}__gte": start, f"{field}__lte": end})
current_value = queryset.count()
# Calculate progress for this condition
if target > 0:
progress += current_value / target
total_conditions += 1
# Average progress across all conditions
if total_conditions > 0:
progress = progress / total_conditions
return progress
</code>
<code>def evaluate_quest(user, criteria):
criteria = json.loads(criteria)
progress = 0
total_conditions = 0
for model_name, conditions in criteria.items():
Model = apps.get_model('your_app_name', model_name) # Dynamically get the model
queryset = Model.objects.filter(user=user)
for field, condition in conditions.items():
if field == 'time_range':
start_time, end_time, target = condition
queryset = queryset.filter(
created_at__time__gte=start_time,
created_at__time__lte=end_time
)
current_value = queryset.count()
elif field == 'reaction_type_within_timeframe':
reaction_type, timeframe, target = condition
time_delta = timedelta(minutes=timeframe)
queryset = queryset.annotate(
reactions_within_timeframe=Count(
Case(
When(
reactions__reaction_type=reaction_type,
reactions__created_at__lte=ExpressionWrapper(
F('created_at') + time_delta,
output_field=DurationField()
),
then='reactions'
)
)
)
)
current_value = queryset.filter(reactions_within_timeframe__gte=target).count()
elif field == 'consecutive_days':
target_days = condition
dates = queryset.values_list('created_at', flat=True)
dates = sorted(dates)
current_streak = 0
max_streak = 0
for i in range(1, len(dates)):
if (dates[i] - dates[i - 1]).days == 1:
current_streak += 1
else:
max_streak = max(max_streak, current_streak)
current_streak = 0
max_streak = max(max_streak, current_streak)
current_value = max_streak
target = target_days
elif field == 'linked_social_networks':
target_value = condition
social_fields = [
'facebook_username', 'twitter_username', 'instagram_username',
'twitch_username', 'google_username', 'youtube_username',
'patreon_username', 'discord_username', 'deviantart_username',
'behance_username', 'dribble_username', 'artstation_username'
]
current_value = sum(bool(getattr(queryset.first(), field)) for field in social_fields)
target = target_value
else:
value, target = condition
if isinstance(value, str):
queryset = queryset.filter(**{field: value})
elif isinstance(value, dict) and 'range' in value:
start, end = value['range']
queryset = queryset.filter(**{f"{field}__gte": start, f"{field}__lte": end})
current_value = queryset.count()
# Calculate progress for this condition
if target > 0:
progress += current_value / target
total_conditions += 1
# Average progress across all conditions
if total_conditions > 0:
progress = progress / total_conditions
return progress
</code>
def evaluate_quest(user, criteria):
criteria = json.loads(criteria)
progress = 0
total_conditions = 0
for model_name, conditions in criteria.items():
Model = apps.get_model('your_app_name', model_name) # Dynamically get the model
queryset = Model.objects.filter(user=user)
for field, condition in conditions.items():
if field == 'time_range':
start_time, end_time, target = condition
queryset = queryset.filter(
created_at__time__gte=start_time,
created_at__time__lte=end_time
)
current_value = queryset.count()
elif field == 'reaction_type_within_timeframe':
reaction_type, timeframe, target = condition
time_delta = timedelta(minutes=timeframe)
queryset = queryset.annotate(
reactions_within_timeframe=Count(
Case(
When(
reactions__reaction_type=reaction_type,
reactions__created_at__lte=ExpressionWrapper(
F('created_at') + time_delta,
output_field=DurationField()
),
then='reactions'
)
)
)
)
current_value = queryset.filter(reactions_within_timeframe__gte=target).count()
elif field == 'consecutive_days':
target_days = condition
dates = queryset.values_list('created_at', flat=True)
dates = sorted(dates)
current_streak = 0
max_streak = 0
for i in range(1, len(dates)):
if (dates[i] - dates[i - 1]).days == 1:
current_streak += 1
else:
max_streak = max(max_streak, current_streak)
current_streak = 0
max_streak = max(max_streak, current_streak)
current_value = max_streak
target = target_days
elif field == 'linked_social_networks':
target_value = condition
social_fields = [
'facebook_username', 'twitter_username', 'instagram_username',
'twitch_username', 'google_username', 'youtube_username',
'patreon_username', 'discord_username', 'deviantart_username',
'behance_username', 'dribble_username', 'artstation_username'
]
current_value = sum(bool(getattr(queryset.first(), field)) for field in social_fields)
target = target_value
else:
value, target = condition
if isinstance(value, str):
queryset = queryset.filter(**{field: value})
elif isinstance(value, dict) and 'range' in value:
start, end = value['range']
queryset = queryset.filter(**{f"{field}__gte": start, f"{field}__lte": end})
current_value = queryset.count()
# Calculate progress for this condition
if target > 0:
progress += current_value / target
total_conditions += 1
# Average progress across all conditions
if total_conditions > 0:
progress = progress / total_conditions
return progress
Concerns:
- The current implementation still requires us to hardcode specific conditions in the
evaluate_quest
function. - We want a more dynamic approach that can handle unknown future scenarios without requiring code changes.
Request for Help:
- How can we make the
evaluate_quest
function more dynamic to handle various complex conditions without hardcoding each new scenario? - Are there any existing libraries or patterns in Django that can help achieve this flexibility?
- Examples of complex conditions that we need to handle:
- Posting a status update for the first time.
- Completing all profile information fields.
- Giving 50 likes and/or love reactions on followers’ posts.
- Receiving 100+ likes and 50+
comments on a single post.
- Reacting first on 50+ friends’ posts.
Current Workflow:
- User navigates to the quest or badge page, triggering a Celery task.
- The task evaluates all quests or badges for the user using
get_or_create(user, quest|badge)
. - The
evaluate_quest
function is called to calculate progress. - Progress is updated based on the response from the
evaluate_quest
function. - The
save
method inUserQuestProgress
andUserBadgeProgress
automatically marks completion if the current value meets or exceeds the target value. - A
post_save
signal notifies the user via WebSocket if a quest or badge is completed.
Any insights or suggestions to make this system more robust and dynamic would be greatly appreciated. Thank you!