Skip to content

Commit a4e450b

Browse files
Feature: Add feature to complete profile of the students if the challenge requires (#4965)
* Backend: Enhance user profile model and serializers to support additional fields and validation * Backend: Update profile serializer and challenge tests to include new fields and improve response validation * Backend: Add 'require_complete_profile' field to challenge and participant tests for improved data handling * Backend: Update participant tests to include additional address fields and improve JSON response handling * Backend: Refactor invitation error messages in participant view for improved readability and consistency
1 parent 57fb3c2 commit a4e450b

File tree

22 files changed

+1165
-180
lines changed

22 files changed

+1165
-180
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 2.2.20 on 2026-01-28 07:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("accounts", "0002_add_jwt_access_token_model"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="profile",
15+
name="address_city",
16+
field=models.CharField(blank=True, max_length=100, null=True),
17+
),
18+
migrations.AddField(
19+
model_name="profile",
20+
name="address_country",
21+
field=models.CharField(blank=True, max_length=100, null=True),
22+
),
23+
migrations.AddField(
24+
model_name="profile",
25+
name="address_state",
26+
field=models.CharField(blank=True, max_length=100, null=True),
27+
),
28+
migrations.AddField(
29+
model_name="profile",
30+
name="address_street",
31+
field=models.CharField(blank=True, max_length=500, null=True),
32+
),
33+
migrations.AddField(
34+
model_name="profile",
35+
name="university",
36+
field=models.CharField(blank=True, max_length=512, null=True),
37+
),
38+
]

apps/accounts/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,41 @@ class Profile(TimeStampedModel):
4646
github_url = models.URLField(max_length=200, null=True, blank=True)
4747
google_scholar_url = models.URLField(max_length=200, null=True, blank=True)
4848
linkedin_url = models.URLField(max_length=200, null=True, blank=True)
49+
# Student profile fields for challenges requiring complete profile
50+
address_street = models.CharField(max_length=500, null=True, blank=True)
51+
address_city = models.CharField(max_length=100, null=True, blank=True)
52+
address_state = models.CharField(max_length=100, null=True, blank=True)
53+
address_country = models.CharField(max_length=100, null=True, blank=True)
54+
university = models.CharField(max_length=512, null=True, blank=True)
4955

5056
def __str__(self):
5157
return "{}".format(self.user)
5258

59+
@property
60+
def is_complete(self):
61+
"""
62+
Check if the user's profile is complete for challenges requiring complete profile.
63+
A complete profile requires: first_name, last_name, address_street, address_city,
64+
address_state, address_country, and university.
65+
"""
66+
user = self.user
67+
required_fields = [
68+
user.first_name,
69+
user.last_name,
70+
self.address_street,
71+
self.address_city,
72+
self.address_state,
73+
self.address_country,
74+
self.university,
75+
]
76+
return all(field and field.strip() for field in required_fields)
77+
78+
def get_full_name(self):
79+
"""Returns the full name of the user."""
80+
return "{} {}".format(
81+
self.user.first_name, self.user.last_name
82+
).strip()
83+
5384
class Meta:
5485
app_label = "accounts"
5586
db_table = "user_profile"

apps/accounts/serializers.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ class ProfileSerializer(UserDetailsSerializer):
4141
linkedin_url = serializers.URLField(
4242
source="profile.linkedin_url", allow_blank=True
4343
)
44+
# Student profile fields
45+
address_street = serializers.CharField(
46+
source="profile.address_street", allow_blank=True, required=False
47+
)
48+
address_city = serializers.CharField(
49+
source="profile.address_city", allow_blank=True, required=False
50+
)
51+
address_state = serializers.CharField(
52+
source="profile.address_state", allow_blank=True, required=False
53+
)
54+
address_country = serializers.CharField(
55+
source="profile.address_country", allow_blank=True, required=False
56+
)
57+
university = serializers.CharField(
58+
source="profile.university", allow_blank=True, required=False
59+
)
60+
is_profile_complete = serializers.SerializerMethodField()
4461

4562
class Meta(UserDetailsSerializer.Meta):
4663
fields = (
@@ -53,14 +70,31 @@ class Meta(UserDetailsSerializer.Meta):
5370
"github_url",
5471
"google_scholar_url",
5572
"linkedin_url",
73+
"address_street",
74+
"address_city",
75+
"address_state",
76+
"address_country",
77+
"university",
78+
"is_profile_complete",
5679
)
5780

81+
def get_is_profile_complete(self, obj):
82+
"""Returns whether the user's profile is complete."""
83+
if hasattr(obj, "profile"):
84+
return obj.profile.is_complete
85+
return False
86+
5887
def update(self, instance, validated_data):
5988
profile_data = validated_data.pop("profile", {})
6089
affiliation = profile_data.get("affiliation")
6190
github_url = profile_data.get("github_url")
6291
google_scholar_url = profile_data.get("google_scholar_url")
6392
linkedin_url = profile_data.get("linkedin_url")
93+
address_street = profile_data.get("address_street")
94+
address_city = profile_data.get("address_city")
95+
address_state = profile_data.get("address_state")
96+
address_country = profile_data.get("address_country")
97+
university = profile_data.get("university")
6498

6599
instance = super(ProfileSerializer, self).update(
66100
instance, validated_data
@@ -72,6 +106,16 @@ def update(self, instance, validated_data):
72106
profile.github_url = github_url
73107
profile.google_scholar_url = google_scholar_url
74108
profile.linkedin_url = linkedin_url
109+
if address_street is not None:
110+
profile.address_street = address_street
111+
if address_city is not None:
112+
profile.address_city = address_city
113+
if address_state is not None:
114+
profile.address_state = address_state
115+
if address_country is not None:
116+
profile.address_country = address_country
117+
if university is not None:
118+
profile.university = university
75119
profile.save()
76120
return instance
77121

@@ -88,6 +132,11 @@ class Meta:
88132
"github_url",
89133
"google_scholar_url",
90134
"linkedin_url",
135+
"address_street",
136+
"address_city",
137+
"address_state",
138+
"address_country",
139+
"university",
91140
)
92141

93142

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 2.2.20 on 2026-01-28 07:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("challenges", "0113_add_github_branch_field_and_unique_constraint"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="challenge",
15+
name="require_complete_profile",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="If enabled, participants must have a complete "
19+
"profile (name, address, city, state, country) before "
20+
"joining this challenge.",
21+
verbose_name="Require Complete Profile",
22+
),
23+
),
24+
]

apps/challenges/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def __init__(self, *args, **kwargs):
8383
anonymous_leaderboard = models.BooleanField(default=False)
8484
participant_teams = models.ManyToManyField(ParticipantTeam, blank=True)
8585
manual_participant_approval = models.BooleanField(default=False)
86+
require_complete_profile = models.BooleanField(
87+
default=False,
88+
verbose_name="Require Complete Profile",
89+
help_text="If enabled, participants must have a complete profile (name, address, city, state, country) before joining this challenge.",
90+
)
8691
approved_participant_teams = models.ManyToManyField(
8792
ParticipantTeam,
8893
blank=True,

apps/challenges/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class Meta:
6767
"allowed_email_domains",
6868
"blocked_email_domains",
6969
"banned_email_ids",
70+
"require_complete_profile",
7071
"approved_by_admin",
7172
"forum_url",
7273
"is_docker_based",
@@ -318,6 +319,7 @@ class Meta:
318319
"allowed_email_domains",
319320
"blocked_email_domains",
320321
"banned_email_ids",
322+
"require_complete_profile",
321323
"forum_url",
322324
"remote_evaluation",
323325
"allow_resuming_submissions",

apps/challenges/utils.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,35 @@ def is_user_in_blocked_email_domains(email, challenge_pk):
406406
return False
407407

408408

409+
def get_participants_with_incomplete_profiles(participant_team):
410+
"""
411+
Check if all team members have complete profiles.
412+
413+
Arguments:
414+
participant_team {ParticipantTeam} -- The participant team to check
415+
416+
Returns:
417+
list -- List of usernames of team members with incomplete profiles
418+
"""
419+
from accounts.models import Profile
420+
from participants.models import Participant
421+
422+
incomplete_profiles = []
423+
participants = Participant.objects.filter(
424+
team=participant_team
425+
).select_related("user", "user__profile")
426+
427+
for participant in participants:
428+
try:
429+
profile = participant.user.profile
430+
if not profile.is_complete:
431+
incomplete_profiles.append(participant.user.username)
432+
except Profile.DoesNotExist:
433+
incomplete_profiles.append(participant.user.username)
434+
435+
return incomplete_profiles
436+
437+
409438
def get_unique_alpha_numeric_key(length):
410439
"""
411440
Returns unique alpha numeric key of length
@@ -536,28 +565,24 @@ def send_subscription_plans_email(challenge):
536565

537566
emails_sent += 1
538567
logger.info(
539-
"Subscription plans email sent to {} for challenge {}".format(
540-
email, challenge.pk
541-
)
568+
"Subscription plans email sent to {} for challenge "
569+
"{}".format(email, challenge.pk)
542570
)
543571
except Exception as e:
544572
logger.error(
545-
"Failed to send subscription plans email to {} for challenge {}: {}".format(
546-
email, challenge.pk, str(e)
547-
)
573+
"Failed to send subscription plans email to {} for "
574+
"challenge {}: {}".format(email, challenge.pk, str(e))
548575
)
549576

550577
logger.info(
551-
"Sent subscription plans email to {}/{} hosts for challenge {}".format(
552-
emails_sent, len(challenge_host_emails), challenge.pk
553-
)
578+
"Sent subscription plans email to {}/{} hosts for challenge "
579+
"{}".format(emails_sent, len(challenge_host_emails), challenge.pk)
554580
)
555581

556582
except Exception as e:
557583
logger.error(
558-
"Error sending subscription plans email for challenge {}: {}".format(
559-
challenge.pk, str(e)
560-
)
584+
"Error sending subscription plans email for challenge {}: "
585+
"{}".format(challenge.pk, str(e))
561586
)
562587

563588

0 commit comments

Comments
 (0)