Building a Dynamic Quest and Badge Evaluation System in Django

Models

Quest

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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:

  1. Models for Quests and Badges: We have defined models for Quest, Badge, UserQuestProgress, and UserBadgeProgress which track the user’s progress.
  2. Criteria JSON: We use JSON to define the criteria for each quest or badge. For example:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <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]
        }
    }
    
  3. Evaluation Function: We have an evaluate_quest function that processes the criteria JSON to compute the user’s progress:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <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
    
  4. Celery Task: When a user navigates to the quest or badge page, a Celery task is triggered to evaluate their progress and update UserQuestProgress or UserBadgeProgress.

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:
      Plain text
      Copy to clipboard
      Open code in new window
      EnlighterJS 3 Syntax Highlighter
      <code>{
      "Post": {
      "first_post": true
      }
      }
      </code>
      <code>{ "Post": { "first_post": true } } </code>
      {
          "Post": {
              "first_post": true
          }
      }
      
    • You have completed all your profile information fields:
      Plain text
      Copy to clipboard
      Open code in new window
      EnlighterJS 3 Syntax Highlighter
      <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:
      Plain text
      Copy to clipboard
      Open code in new window
      EnlighterJS 3 Syntax Highlighter
      <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:
      Plain text
      Copy to clipboard
      Open code in new window
      EnlighterJS 3 Syntax Highlighter
      <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:
      Plain text
      Copy to clipboard
      Open code in new window
      EnlighterJS 3 Syntax Highlighter
      <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
          }
      }
      

Workflow:

  1. When the user navigates to the quest or badge page, a Celery task starts.
  2. The task uses get_or_create(user, quest|badge) to ensure there is a progress instance.
  3. The evaluate_quest function is called to compute the current and target values.
  4. The UserQuestProgress and UserBadgeProgress models have overridden save methods to mark completion if the current value meets or exceeds the target value.
  5. Upon saving, a post_save signal notifies the user via WebSocket if a quest or badge is completed.

Current evaluate_quest Function:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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:

  1. User navigates to the quest or badge page, triggering a Celery task.
  2. The task evaluates all quests or badges for the user using get_or_create(user, quest|badge).
  3. The evaluate_quest function is called to calculate progress.
  4. Progress is updated based on the response from the evaluate_quest function.
  5. The save method in UserQuestProgress and UserBadgeProgress automatically marks completion if the current value meets or exceeds the target value.
  6. 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!

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật