Skip to content
Open
13 changes: 13 additions & 0 deletions src/chigame/users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ def generate_unique_username():
return username


class PrivacySettingsForm(forms.ModelForm):
"""Form for managing user privacy settings."""

class Meta:
model = UserProfile
fields = ["profile_visibility", "friend_request_permission", "searchable_by_strangers"]
widgets = {
"profile_visibility": forms.Select(attrs={"class": "form-control"}),
"friend_request_permission": forms.Select(attrs={"class": "form-control"}),
"searchable_by_strangers": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}


class UserProfileForm(ModelForm):
"""
Form for editing user profile information.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.20 on 2025-05-24 04:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0011_alter_friendinvitation_unique_together_and_more"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="friend_request_permission",
field=models.CharField(
choices=[("everyone", "Everyone"), ("friends_of_friends", "Friends of Friends"), ("nobody", "Nobody")],
default="everyone",
help_text="Who can send you friend requests",
max_length=20,
),
),
migrations.AddField(
model_name="userprofile",
name="profile_visibility",
field=models.CharField(
choices=[("public", "Public"), ("friends", "Friends Only"), ("private", "Private")],
default="public",
help_text="Who can view your profile",
max_length=10,
),
),
migrations.AddField(
model_name="userprofile",
name="searchable_by_strangers",
field=models.BooleanField(default=True, help_text="Allow strangers to find you in search"),
),
]
20 changes: 20 additions & 0 deletions src/chigame/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,32 @@ class UserProfile(models.Model):
that don't need a profile on the website (e.g., API-only users)
"""

VISIBILITY_CHOICES = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great addition!

("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
]

FRIEND_REQUEST_CHOICES = [
("everyone", "Everyone"),
("friends_of_friends", "Friends of Friends"),
("nobody", "Nobody"),
]

user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True, max_length=500)
date_joined = models.DateTimeField(auto_now_add=True)
profile_photo = models.ImageField(upload_to="profile_photos/", blank=True, null=True)
favorite_games = models.ManyToManyField("games.Game", blank=True, related_name="favorited_by")

profile_visibility = models.CharField(
max_length=10, choices=VISIBILITY_CHOICES, default="public", help_text="Who can view your profile"
)
friend_request_permission = models.CharField(
max_length=20, choices=FRIEND_REQUEST_CHOICES, default="everyone", help_text="Who can send you friend requests"
)
searchable_by_strangers = models.BooleanField(default=True, help_text="Allow strangers to find you in search")

@classmethod
def get_or_create_profile(cls, user: User) -> "UserProfile":
profile, created = cls.objects.get_or_create(user=user)
Expand Down
2 changes: 2 additions & 0 deletions src/chigame/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
name_update_view,
notification_detail,
notification_search_results,
privacy_settings_view,
remove_favorite_game,
remove_friend,
send_friend_invitation,
Expand All @@ -39,6 +40,7 @@
path("~update-username/", view=username_update_view, name="update-username"),
path("<int:pk>/", view=user_detail_view, name="detail"),
path("profile/<int:pk>/", view=user_profile_detail_view, name="user-profile"),
path("privacy-settings/", view=privacy_settings_view, name="privacy-settings"),
path("add_friend/<int:pk>", view=send_friend_invitation, name="add-friend"),
path("remove_friend/<int:pk>", view=remove_friend, name="remove-friend"),
path("cancel_friend_invitation/<int:pk>", view=cancel_friend_invitation, name="cancel-friend-invitation"),
Expand Down
76 changes: 73 additions & 3 deletions src/chigame/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from chigame.games.models import Game, GameList, Lobby, Player, Tournament

from .forms import FriendInvitationForm, UserProfileForm
from .forms import FriendInvitationForm, PrivacySettingsForm, UserProfileForm
from .models import (
FriendInvitation,
FriendRequestNotification,
Expand Down Expand Up @@ -231,12 +231,16 @@ def user_profile_detail_view(request, pk):
except GameList.DoesNotExist:
favorite_games = []

# Define target_user for the profile being viewed
target_user = profile.user

# for checking friendship and pending friend request status
is_friend = None
friendship_request = None
is_blocked = False
friend_request_message = None
target_user = get_object_or_404(User, pk=pk)
can_send_friend_request = False

if request.user.is_authenticated:
# check friendship or pending invitation with the target user
is_friend = target_user.friends.filter(pk=request.user.pk).exists()
Expand All @@ -249,6 +253,21 @@ def user_profile_detail_view(request, pk):
if friendship_request and friendship_request.sender == target_user and friendship_request.message:
friend_request_message = friendship_request.message

# Check if current user can send friend request via privacy settings
if profile.friend_request_permission == "everyone":
can_send_friend_request = True
elif profile.friend_request_permission == "friends_of_friends":
mutual_friends = curr_user.friends.filter(id__in=target_user.friends.all())
can_send_friend_request = mutual_friends.exists()

# Check if profile is accessible via privacy settings
if profile.profile_visibility == "private":
raise Http404("This profile is private.")
elif profile.profile_visibility == "friends" and not is_friend and request.user.is_authenticated:
raise Http404("This profile is only visible to friends.")
elif profile.profile_visibility == "friends" and not request.user.is_authenticated:
raise Http404("This profile is only visible to friends.")

# provide frontend profile + friendship status
context = {
"profile": profile,
Expand All @@ -257,6 +276,7 @@ def user_profile_detail_view(request, pk):
"favorite_games": favorite_games,
"is_blocked": is_blocked,
"friend_request_message": friend_request_message,
"can_send_friend_request": can_send_friend_request,
}
return render(request, "users/userprofile_detail.html", context=context)

Expand Down Expand Up @@ -296,6 +316,7 @@ def send_friend_invitation(request, pk):
if curr_user.id == other_user.id:
messages.error(request, "You can't send friendship invitation to yourself")
return redirect(reverse("users:user-profile", kwargs={"pk": request.user.pk}))

# check if the friendship invitation already exists
invitation = FriendInvitation.objects.filter(
Q(sender=curr_user, receiver=other_user, is_deleted=False)
Expand Down Expand Up @@ -497,7 +518,30 @@ def user_search_results(request):
profiles_list = UserProfile.objects.filter(
Q(user__username__icontains=query_input) | Q(user__name__icontains=query_input)
)
if profiles_list.count() > 0:

if request.user.is_authenticated:
filtered_profiles = []
for profile in profiles_list:
if profile.user == request.user:
continue

if not profile.searchable_by_strangers:
mutual_friends = request.user.friends.filter(id__in=profile.user.friends.all())
if not mutual_friends.exists():
continue

is_friend = profile.user.friends.filter(pk=request.user.pk).exists()
if profile.profile_visibility == "private":
continue
elif profile.profile_visibility == "friends" and not is_friend:
continue

filtered_profiles.append(profile)
profiles_list = filtered_profiles
else:
profiles_list = profiles_list.filter(profile_visibility="public", searchable_by_strangers=True)

if len(profiles_list) > 0:
context["found"] = True
context["object_list"] = profiles_list
return render(request, "pages/search_results.html", context)
Expand Down Expand Up @@ -982,6 +1026,32 @@ def notifications_by_label(request, label_id):


@login_required
def privacy_settings_view(request):
"""
Displays a user's privacy settings and allows them to manage them.

Args:
request (HttpRequest)

Returns:
HttpResponse: Rendered template with privacy settings context
"""
user = request.user
profile = UserProfile.get_or_create_profile(user)

if request.method == "POST":
form = PrivacySettingsForm(request.POST, instance=profile)
if form.is_valid():
form.save()
messages.success(request, "Privacy settings updated successfully.")
return redirect(reverse("users:user-profile", kwargs={"pk": request.user.pk}))
else:
form = PrivacySettingsForm(instance=profile)

context = {"form": form}
return render(request, "users/privacy_settings.html", context)


def toggle_profanity(request, pk):
"""
Toggle the profanity filter for a user profile.
Expand Down
6 changes: 3 additions & 3 deletions src/templates/games/favorites_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ <h1>My Favorite Games</h1>
{% for game in favorite_games %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{% url 'game-detail' pk=game.pk %}">{{ game.name }}</a>
<a href="{% url 'remove-from-favorites' pk=game.pk %}"
class="btn btn-sm btn-outline-warning">Remove</a>
<a href="{% url 'remove-from-favorites' game_id=game.pk %}"
class="btn btn-sm btn-maroon">Remove</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>You have no favorite games yet.</p>
{% endif %}
<a href="{% url 'game-list' %}" class="btn btn-primary mt-3">Browse Games</a>
<a href="{% url 'game-list' %}" class="btn btn-maroon mt-3">Browse Games</a>
</div>
{% endblock content %}
{# Adding comment to trigger #}
4 changes: 2 additions & 2 deletions src/templates/games/game_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ <h4>Number of Reviews: {{ popularity|default:0 }}</h4>
class="btn btn-outline-primary">Create a Match</a>
{% if user.is_authenticated %}
{% if game in favorites_list.games.all %}
<a href="{% url 'remove-from-favorites' pk=game.pk %}"
<a href="{% url 'remove-from-favorites' game_id=game.pk %}"
class="btn btn-outline-warning">Remove from Favorites</a>
{% else %}
<a href="{% url 'add-to-favorites' pk=game.pk %}"
<a href="{% url 'add-to-favorites' game_id=game.pk %}"
class="btn btn-outline-success">Add to Favorites</a>
{% endif %}
<a href="{% url 'game_history' game.id %}"
Expand Down
4 changes: 2 additions & 2 deletions src/templates/games/interactive-fiction/IF_game_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ <h4>Number of Reviews: {{ popularity|default:0 }}</h4>
class="btn btn-outline-primary">Play Game in New Tab</a>
{% if user.is_authenticated %}
{% if game in favorites_list.games.all %}
<a href="{% url 'remove-from-favorites' pk=game.pk %}"
<a href="{% url 'remove-from-favorites' game_id=game.pk %}"
class="btn btn-outline-warning">Remove from Favorites</a>
{% else %}
<a href="{% url 'add-to-favorites' pk=game.pk %}"
<a href="{% url 'add-to-favorites' game_id=game.pk %}"
class="btn btn-outline-success">Add to Favorites</a>
{% endif %}
{% if game_lists %}
Expand Down
55 changes: 55 additions & 0 deletions src/templates/users/privacy_settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% extends "base.html" %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great!


{% load static %}

{% block title %}
Privacy Settings
{% endblock title %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3>Privacy Settings</h3>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.profile_visibility.id_for_label }}" class="form-label">
<strong>Profile Visibility</strong>
</label>
{{ form.profile_visibility }}
<div class="form-text">{{ form.profile_visibility.help_text }}</div>
</div>
<div class="mb-3">
<label for="{{ form.friend_request_permission.id_for_label }}"
class="form-label">
<strong>Friend Request Permission</strong>
</label>
{{ form.friend_request_permission }}
<div class="form-text">{{ form.friend_request_permission.help_text }}</div>
</div>
<div class="mb-3">
<div class="form-check">
{{ form.searchable_by_strangers }}
<label for="{{ form.searchable_by_strangers.id_for_label }}"
class="form-check-label">
<strong>Searchable by Strangers</strong>
</label>
</div>
<div class="form-text">{{ form.searchable_by_strangers.help_text }}</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'users:user-profile' request.user.pk %}"
class="btn btn-secondary">Back to Profile</a>
<button type="submit" class="btn btn-primary">Save Settings</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
30 changes: 4 additions & 26 deletions src/templates/users/userprofile_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ <h3>
class="btn btn-maroon">Friend List</a>
<a href="{% url 'users:user-history' profile.user.pk %}"
class="btn btn-maroon">Stats</a>
<a href="{% url 'users:privacy-settings' %}" class="btn btn-maroon">Privacy Settings</a>
<a href="{% url 'users:edit-profile' profile.user.pk %}"
class="btn btn-maroon">Edit Profile</a>
<div class="d-flex justify-content-center align-items-center mt-1">
Expand Down Expand Up @@ -310,6 +311,9 @@ <h3>
class="btn btn-maroon">Stats</a>
{% elif is_blocked %}
<p>User not available.</p>
{% elif can_send_friend_request %}
<a href="{% url 'users:add-friend' profile.user.pk %}"
class="btn btn-maroon">Add Friend</a>
{% else %}
{% if not friendship_request %}
<a href="{% url 'users:add-friend' profile.user.pk %}"
Expand Down Expand Up @@ -416,32 +420,6 @@ <h4>
</div>
</div>
</div>
{% if profile.user == request.user %}
<div class="card mt-4">
<div class="card-header">
<h3>My Game Lists</h3>
</div>
<div class="card-body">
{% if game_lists %}
<ul class="list-group list-group-flush">
{% for game_list in game_lists %}
<li class="list-group-item">
<a href="{% url 'gamelist-detail' game_list.pk %}"
class="btn-outline-block">
<span>{{ game_list.name }}</span>
<span class="badge rounded-pill">{{ game_list.games.count }} game{{ game_list.games.count|pluralize }}</span>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">
You don't have any game lists yet. <a href="{% url 'game-list' %}">Browse games</a> to create some!
</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock content %}