Merge pull request #139 from christianlouis/christianlouis/issue51
Add account deletion confirmation, privacy settings, and profile view templates
This commit is contained in:
+54
-6
@@ -71,32 +71,80 @@ def seed_db():
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
hashed_password=get_password_hash("password"),
|
||||
is_admin=True # Set admin privileges
|
||||
is_admin=True, # Set admin privileges
|
||||
privacy_settings={
|
||||
"email": "private",
|
||||
"full_name": "friends",
|
||||
"teams": "public",
|
||||
"points": "public",
|
||||
"achievements": "public",
|
||||
"events": "friends"
|
||||
}
|
||||
),
|
||||
User(
|
||||
username="john_quizmaster",
|
||||
email="john@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
hashed_password=get_password_hash("password123"),
|
||||
privacy_settings={
|
||||
"email": "friends",
|
||||
"full_name": "public",
|
||||
"teams": "public",
|
||||
"points": "public",
|
||||
"achievements": "public",
|
||||
"events": "public"
|
||||
}
|
||||
),
|
||||
User(
|
||||
username="sarah_johnson",
|
||||
email="sarah@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
hashed_password=get_password_hash("password123"),
|
||||
privacy_settings={
|
||||
"email": "private",
|
||||
"full_name": "public",
|
||||
"teams": "public",
|
||||
"points": "friends",
|
||||
"achievements": "public",
|
||||
"events": "friends"
|
||||
}
|
||||
),
|
||||
User(
|
||||
username="mike_peters",
|
||||
email="mike@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
hashed_password=get_password_hash("password123"),
|
||||
privacy_settings={
|
||||
"email": "private",
|
||||
"full_name": "friends",
|
||||
"teams": "public",
|
||||
"points": "public",
|
||||
"achievements": "public",
|
||||
"events": "public"
|
||||
}
|
||||
),
|
||||
User(
|
||||
username="emma_wilson",
|
||||
email="emma@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
hashed_password=get_password_hash("password123"),
|
||||
privacy_settings={
|
||||
"email": "private",
|
||||
"full_name": "private",
|
||||
"teams": "friends",
|
||||
"points": "private",
|
||||
"achievements": "friends",
|
||||
"events": "private"
|
||||
}
|
||||
),
|
||||
User(
|
||||
username="robert_brown",
|
||||
email="robert@example.com",
|
||||
hashed_password=get_password_hash("password123")
|
||||
hashed_password=get_password_hash("password123"),
|
||||
privacy_settings={
|
||||
"email": "private",
|
||||
"full_name": "friends",
|
||||
"teams": "public",
|
||||
"points": "public",
|
||||
"achievements": "public",
|
||||
"events": "friends"
|
||||
}
|
||||
),
|
||||
]
|
||||
db.add_all(users)
|
||||
|
||||
@@ -110,6 +110,12 @@ def run_migrations(engine):
|
||||
# Add first_name and last_name columns if they don't exist
|
||||
add_name_columns(connection)
|
||||
|
||||
# Add privacy_settings column if it doesn't exist
|
||||
add_privacy_settings_column(connection)
|
||||
|
||||
# Add picture_manually_deleted column if it doesn't exist
|
||||
add_picture_manually_deleted_column(connection)
|
||||
|
||||
print("Migrations completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
@@ -175,3 +181,61 @@ def add_name_columns(connection):
|
||||
print("Column last_name already exists")
|
||||
except Exception as e:
|
||||
print(f"Error adding name columns: {str(e)}")
|
||||
|
||||
def add_privacy_settings_column(connection):
|
||||
"""Add privacy_settings column to users table"""
|
||||
try:
|
||||
# Use database-agnostic way to check if column exists
|
||||
inspector = inspect(engine)
|
||||
columns = [col['name'] for col in inspector.get_columns('users')]
|
||||
|
||||
if 'privacy_settings' not in columns:
|
||||
print("Adding privacy_settings column to users table")
|
||||
|
||||
# Add column with database-specific syntax
|
||||
if engine.name == 'sqlite':
|
||||
connection.execute(text("""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN privacy_settings JSON
|
||||
"""))
|
||||
else: # MySQL
|
||||
connection.execute(text("""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN privacy_settings JSON NULL
|
||||
"""))
|
||||
|
||||
connection.commit()
|
||||
print("Successfully added privacy_settings column to users table")
|
||||
else:
|
||||
print("Column privacy_settings already exists")
|
||||
except Exception as e:
|
||||
print(f"Error adding privacy_settings column: {str(e)}")
|
||||
|
||||
def add_picture_manually_deleted_column(connection):
|
||||
"""Add picture_manually_deleted column to users table"""
|
||||
try:
|
||||
# Use database-agnostic way to check if column exists
|
||||
inspector = inspect(engine)
|
||||
columns = [col['name'] for col in inspector.get_columns('users')]
|
||||
|
||||
if 'picture_manually_deleted' not in columns:
|
||||
print("Adding picture_manually_deleted column to users table")
|
||||
|
||||
# Add column with database-specific syntax
|
||||
if engine.name == 'sqlite':
|
||||
connection.execute(text("""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN picture_manually_deleted BOOLEAN DEFAULT FALSE
|
||||
"""))
|
||||
else: # MySQL
|
||||
connection.execute(text("""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN picture_manually_deleted BOOLEAN DEFAULT FALSE
|
||||
"""))
|
||||
|
||||
connection.commit()
|
||||
print("Successfully added picture_manually_deleted column to users table")
|
||||
else:
|
||||
print("Column picture_manually_deleted already exists")
|
||||
except Exception as e:
|
||||
print(f"Error adding picture_manually_deleted column: {str(e)}")
|
||||
|
||||
@@ -39,6 +39,11 @@ class User(Base, BaseUser):
|
||||
first_name = Column(String(50), nullable=True)
|
||||
last_name = Column(String(50), nullable=True)
|
||||
picture = Column(String(255), nullable=True) # URL to profile picture
|
||||
picture_manually_deleted = Column(Boolean, default=False) # Track if user has deleted their profile picture
|
||||
|
||||
# Privacy settings - JSON field to store privacy preferences
|
||||
# Default: { "email": "private", "teams": "public", "points": "public", "achievements": "public" }
|
||||
privacy_settings = Column(JSON, nullable=True)
|
||||
|
||||
# Relationships
|
||||
memberships = relationship("TeamMembership", back_populates="user")
|
||||
@@ -62,6 +67,23 @@ class User(Base, BaseUser):
|
||||
"""Return the identity of this user."""
|
||||
return str(self.id)
|
||||
|
||||
def get_default_privacy_settings(self):
|
||||
"""Return the default privacy settings if none are set"""
|
||||
return {
|
||||
"email": "private",
|
||||
"full_name": "friends",
|
||||
"teams": "public",
|
||||
"points": "public",
|
||||
"achievements": "public",
|
||||
"events": "friends"
|
||||
}
|
||||
|
||||
def get_privacy_settings(self):
|
||||
"""Get user's privacy settings or default if not set"""
|
||||
if not self.privacy_settings:
|
||||
return self.get_default_privacy_settings()
|
||||
return self.privacy_settings
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.username}>"
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto my-10 bg-white p-8 rounded-lg shadow-md">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="fas fa-user-slash text-red-600 text-2xl"></i>
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">Account Deleted</h1>
|
||||
|
||||
<p class="text-gray-600 text-center mb-6">
|
||||
Your account has been successfully deleted. All your personal information has been removed from our system.
|
||||
</p>
|
||||
|
||||
<div class="border-t border-gray-200 w-full pt-6 mt-2">
|
||||
<p class="text-gray-600 text-center mb-4">
|
||||
We're sorry to see you go. You can always create a new account if you wish to return.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<a href="/" class="bg-irish-green hover:bg-opacity-90 text-white font-semibold py-2 px-4 rounded-md transition">
|
||||
Return to Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,152 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto my-8">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-irish-green mb-6">Privacy Settings</h1>
|
||||
|
||||
<!-- Display messages if present -->
|
||||
{% if request.query_params.message %}
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
||||
{{ request.query_params.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Display errors if present -->
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-gray-600 mb-6">
|
||||
Control who can see different parts of your profile information. Your information can be visible to everyone,
|
||||
only members of your teams, or kept private (visible only to you and admins).
|
||||
</p>
|
||||
|
||||
<form method="post" action="/auth/privacy-settings">
|
||||
<div class="space-y-6">
|
||||
<!-- Email Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Email Address</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.email == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="email_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.email == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Name Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Full Name</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.full_name == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="full_name_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.full_name == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teams Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Teams Membership</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.teams == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="teams_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.teams == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Points Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Points & Leaderboard Position</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.points == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="points_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.points == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Achievements Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Achievements</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.achievements == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="achievements_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.achievements == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Visibility -->
|
||||
<div class="border-b pb-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">Event Attendance</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% for option in privacy_options %}
|
||||
<label class="flex items-center p-4 border rounded-lg {% if privacy_settings.events == option.value %}bg-green-50 border-irish-green{% endif %}">
|
||||
<input type="radio" name="events_visibility" value="{{ option.value }}"
|
||||
{% if privacy_settings.events == option.value %}checked{% endif %}
|
||||
class="mr-2 text-irish-green focus:ring-irish-green">
|
||||
<div>
|
||||
<span class="font-medium block text-sm">{{ option.label }}</span>
|
||||
<span class="text-xs text-gray-500">{{ option.description }}</span>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center pt-4">
|
||||
<a href="/auth/profile" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-arrow-left mr-1"></i> Back to Profile
|
||||
</a>
|
||||
<button type="submit" class="bg-irish-green text-white py-2 px-6 rounded-md hover:bg-opacity-90 transition">
|
||||
Save Privacy Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 pt-4 border-t border-gray-200 text-sm text-gray-500">
|
||||
<p><i class="fas fa-shield-alt mr-1"></i> Note: Administrators can always view your complete profile information for support purposes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -9,6 +9,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Display errors if present -->
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-col md:flex-row items-center md:items-start md:space-x-8">
|
||||
<!-- Profile Image -->
|
||||
<div class="mb-6 md:mb-0">
|
||||
@@ -19,6 +26,26 @@
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Profile Picture Upload Form -->
|
||||
<div class="mt-3">
|
||||
<form action="/auth/update-profile-picture" method="post" enctype="multipart/form-data">
|
||||
<label class="block w-full bg-gray-200 text-center py-2 px-3 rounded{% if not user.picture %}-md{% else %}-t{% endif %} cursor-pointer hover:bg-gray-300 transition">
|
||||
<i class="fas fa-camera mr-1"></i> Change Picture
|
||||
<input type="file" name="file" accept="image/jpeg,image/png" class="hidden" onchange="this.form.submit()">
|
||||
</label>
|
||||
</form>
|
||||
|
||||
{% if user.picture %}
|
||||
<form action="/auth/delete-profile-picture" method="post">
|
||||
<button type="submit" class="w-full bg-red-100 text-red-700 text-center py-2 px-3 rounded-b hover:bg-red-200 transition">
|
||||
<i class="fas fa-trash-alt mr-1"></i> Remove Picture
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-xs text-gray-500 mt-1 text-center">JPG/PNG only</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Info -->
|
||||
@@ -41,6 +68,15 @@
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-irish-green">Account Settings</h2>
|
||||
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Username</h3>
|
||||
<form action="/auth/update-username" method="post" class="flex">
|
||||
<input type="text" name="username" value="{{ user.username }}" class="flex-grow border border-gray-300 rounded-l-md px-3 py-2">
|
||||
<button type="submit" class="bg-irish-green text-white px-4 py-2 rounded-r-md">Update</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">Change your username (must be unique)</p>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Change Password</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Update your password to keep your account secure.</p>
|
||||
@@ -49,13 +85,22 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-200 pb-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Privacy Settings</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Control who can see your profile information.</p>
|
||||
<a href="/auth/privacy-settings" class="inline-block bg-irish-green text-white py-2 px-4 rounded-md hover:bg-opacity-90 transition">
|
||||
Manage Privacy Settings
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h3 class="text-gray-700 font-medium mb-2">Danger Zone</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">Permanently delete your account and all of your data.</p>
|
||||
<button class="border border-red-600 text-red-600 py-2 px-4 rounded-md hover:bg-red-600 hover:text-white transition" disabled>
|
||||
Delete Account
|
||||
<span class="text-xs">(Coming Soon)</span>
|
||||
</button>
|
||||
<form action="/auth/delete-account" method="post" onsubmit="return confirm('Are you sure you want to delete your account? This action cannot be undone.');">
|
||||
<button type="submit" class="border border-red-600 text-red-600 py-2 px-4 rounded-md hover:bg-red-600 hover:text-white transition">
|
||||
Delete Account
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
|
||||
<!-- Profile Header -->
|
||||
<div class="bg-irish-green text-white p-6">
|
||||
<div class="flex flex-col sm:flex-row items-center">
|
||||
<div class="w-24 h-24 rounded-full bg-white border-4 border-white overflow-hidden mb-4 sm:mb-0 sm:mr-6">
|
||||
{% if profile.picture %}
|
||||
<img src="{{ profile.picture }}" alt="Profile" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full bg-irish-green flex items-center justify-center text-white text-4xl">
|
||||
{{ profile.username[0]|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-center sm:text-left">
|
||||
<h1 class="text-2xl font-bold">{{ profile.username }}</h1>
|
||||
<p class="text-green-100">Member since {{ profile.created_at.strftime('%B %Y') }}</p>
|
||||
<p class="mt-2">
|
||||
{% if profile.is_admin %}
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full font-medium mr-1">Admin</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Left Column: Personal Info -->
|
||||
<div class="md:col-span-1">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Profile Information</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-cream-white p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">Username</h3>
|
||||
<p class="font-medium">{{ profile.username }}</p>
|
||||
</div>
|
||||
|
||||
{% if "email" in profile %}
|
||||
<div class="bg-cream-white p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">Email</h3>
|
||||
<p>{{ profile.email }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if "first_name" in profile and "last_name" in profile %}
|
||||
<div class="bg-cream-white p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">Full Name</h3>
|
||||
<p>{{ profile.first_name }} {{ profile.last_name }}</p>
|
||||
</div>
|
||||
{% elif "first_name" in profile %}
|
||||
<div class="bg-cream-white p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">First Name</h3>
|
||||
<p>{{ profile.first_name }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-cream-white p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-600 mb-1">Account Type</h3>
|
||||
<p>{% if profile.is_admin %}Administrator{% else %}User{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Stats & Teams -->
|
||||
<div class="md:col-span-2">
|
||||
{% if profile.can_view_points %}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Statistics</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-cream-white p-4 rounded-lg text-center">
|
||||
<p class="text-sm text-gray-600">Total Points</p>
|
||||
<p class="text-2xl font-bold text-irish-green">{{ total_points }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if profile.can_view_teams and teams %}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Teams</h2>
|
||||
<div class="space-y-4">
|
||||
{% for team in teams %}
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-cream-white px-4 py-3 flex justify-between items-center">
|
||||
<div class="font-medium">{{ team.name }}</div>
|
||||
{% if team.is_captain %}
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full">Captain</span>
|
||||
{% else %}
|
||||
<span class="bg-gray-200 text-gray-700 text-xs px-2 py-1 rounded-full">Member</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<a href="/teams/{{ team.id }}" class="text-irish-green hover:underline text-sm">View Team</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if profile.can_view_achievements %}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Achievements</h2>
|
||||
<p class="text-gray-500 italic">Achievements data will be shown here</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if profile.can_view_events %}
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Recent Events</h2>
|
||||
<p class="text-gray-500 italic">Recent events will be shown here</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -78,7 +78,7 @@
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
|
||||
<a href="/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<a href="/auth/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-user mr-2"></i> Profile
|
||||
</a>
|
||||
<a href="/dashboard" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
@@ -90,13 +90,13 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/logout" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<a href="/auth/logout" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/login" class="bg-golden-ale hover:bg-opacity-90 text-black-stout px-4 py-2 rounded-md transition">Sign In</a>
|
||||
<a href="/auth/login" class="bg-golden-ale hover:bg-opacity-90 text-black-stout px-4 py-2 rounded-md transition">Sign In</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -116,14 +116,14 @@
|
||||
<a href="/leaderboard" class="hover:text-golden-ale transition py-2">Leaderboard</a>
|
||||
<a href="/dashboard/scan" class="hover:text-golden-ale transition py-2">Scan QR Code</a>
|
||||
{% if user %}
|
||||
<a href="/profile" class="hover:text-golden-ale transition py-2">Profile</a>
|
||||
<a href="/auth/profile" class="hover:text-golden-ale transition py-2">Profile</a>
|
||||
<a href="/dashboard" class="hover:text-golden-ale transition py-2">Dashboard</a>
|
||||
{% if user.is_admin %}
|
||||
<a href="/admin" class="hover:text-golden-ale transition py-2">Admin</a>
|
||||
{% endif %}
|
||||
<a href="/logout" class="hover:text-golden-ale transition py-2">Logout</a>
|
||||
<a href="/auth/logout" class="hover:text-golden-ale transition py-2">Logout</a>
|
||||
{% else %}
|
||||
<a href="/login" class="bg-golden-ale hover:bg-opacity-90 text-black-stout px-4 py-2 rounded-md transition text-center">Sign In</a>
|
||||
<a href="/auth/login" class="bg-golden-ale hover:bg-opacity-90 text-black-stout px-4 py-2 rounded-md transition text-center">Sign In</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,9 +161,9 @@
|
||||
<div>
|
||||
<h3 class="text-golden-ale font-bold mb-4">Account</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/login" class="hover:text-golden-ale transition">Sign In</a></li>
|
||||
<li><a href="/auth/login" class="hover:text-golden-ale transition">Sign In</a></li>
|
||||
<li><a href="/register" class="hover:text-golden-ale transition">Register</a></li>
|
||||
<li><a href="/profile" class="hover:text-golden-ale transition">Profile</a></li>
|
||||
<li><a href="/auth/profile" class="hover:text-golden-ale transition">Profile</a></li>
|
||||
<li><a href="/dashboard" class="hover:text-golden-ale transition">Dashboard</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<!-- User Welcome -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h1 class="text-2xl md:text-3xl font-garamond text-irish-green mb-2">Welcome, {{ user.username }}!</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Hero Section -->
|
||||
<section class="w-full py-8 md:py-16 text-center">
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-irish-green mb-4">PubQuiz League Tracker</h1>
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<!-- Features -->
|
||||
<section class="w-full py-12 bg-white rounded-lg shadow-md my-8">
|
||||
<div class="container mx-auto">
|
||||
<div class="container mx-auto px-4">
|
||||
<h2 class="text-3xl font-bold text-irish-green text-center mb-10">How It Works</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="bg-cream-white p-4 rounded-full mb-4">
|
||||
<i class="fas fa-users text-irish-green text-3xl"></i>
|
||||
|
||||
+69
-22
@@ -6,13 +6,21 @@
|
||||
<div class="bg-irish-green text-white p-6">
|
||||
<div class="flex flex-col sm:flex-row items-center">
|
||||
<div class="w-24 h-24 rounded-full bg-white border-4 border-white overflow-hidden mb-4 sm:mb-0 sm:mr-6">
|
||||
<img src="https://picsum.photos/150" alt="Profile" class="w-full h-full object-cover">
|
||||
{% if user.picture %}
|
||||
<img src="{{ user.picture }}" alt="Profile" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full bg-irish-green flex items-center justify-center text-white text-4xl">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-center sm:text-left">
|
||||
<h1 class="text-2xl font-bold">John Quizmaster</h1>
|
||||
<p class="text-green-100">Member since October 2022</p>
|
||||
<h1 class="text-2xl font-bold">{{ user.username }}</h1>
|
||||
<p class="text-green-100">Member since {{ user.created_at.strftime('%B %Y') }}</p>
|
||||
<p class="mt-2">
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full font-medium mr-1">Quiz Master</span>
|
||||
{% if user.is_admin %}
|
||||
<span class="bg-golden-ale text-black-stout text-xs px-2 py-1 rounded-full font-medium mr-1">Admin</span>
|
||||
{% endif %}
|
||||
<span class="bg-white text-irish-green text-xs px-2 py-1 rounded-full font-medium">Team Captain</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -21,45 +29,82 @@
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div class="p-6">
|
||||
<!-- Display messages if present -->
|
||||
{% if request.query_params.message %}
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
||||
{{ request.query_params.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Display errors if present -->
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Left Column: Personal Info -->
|
||||
<div class="md:col-span-1">
|
||||
<h2 class="text-xl font-semibold text-irish-green mb-4">Personal Information</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Display Name</label>
|
||||
<div class="mt-1 flex">
|
||||
<input type="text" value="John Quizmaster" readonly class="flex-grow bg-gray-100 border border-gray-300 rounded-l-md px-3 py-2">
|
||||
<button class="bg-irish-green text-white px-3 py-2 rounded-r-md">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
<form action="/auth/update-username" method="post" class="mt-1 flex">
|
||||
<input type="text" name="username" value="{{ user.username }}" class="flex-grow border border-gray-300 rounded-l-md px-3 py-2">
|
||||
<button type="submit" class="bg-irish-green text-white px-3 py-2 rounded-r-md">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">Change your username (must be unique)</p>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Email</label>
|
||||
<div class="mt-1 flex">
|
||||
<input type="email" value="john@example.com" readonly class="flex-grow bg-gray-100 border border-gray-300 rounded-l-md px-3 py-2">
|
||||
<button class="bg-irish-green text-white px-3 py-2 rounded-r-md">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<input type="email" value="{{ user.email }}" readonly class="flex-grow bg-gray-100 border border-gray-300 rounded-md px-3 py-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Picture -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Profile Picture</label>
|
||||
<form action="/auth/update-profile-picture" method="post" enctype="multipart/form-data" class="mt-1">
|
||||
<div class="flex items-center">
|
||||
<label class="flex-grow bg-golden-ale hover:bg-opacity-90 text-black font-medium py-2 px-4 rounded-l-md cursor-pointer text-center transition">
|
||||
<i class="fas fa-camera mr-1"></i> Upload Photo
|
||||
<input type="file" name="file" accept="image/jpeg,image/png" class="hidden" onchange="this.form.submit()">
|
||||
</label>
|
||||
<button type="submit" class="bg-irish-green text-white px-3 py-2 rounded-r-md">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">JPG or PNG formats only</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600">Password</label>
|
||||
<div class="mt-1">
|
||||
<button class="w-full bg-golden-ale hover:bg-opacity-90 text-black font-medium py-2 px-4 rounded-md transition">
|
||||
<a href="/auth/change-password" class="block w-full bg-golden-ale hover:bg-opacity-90 text-black font-medium py-2 px-4 rounded-md transition text-center">
|
||||
Change Password
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button class="w-full bg-irish-green hover:bg-opacity-90 text-white font-medium py-2 px-4 rounded-md transition">
|
||||
Save Changes
|
||||
</button>
|
||||
<!-- Privacy Settings -->
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-600">Privacy</label>
|
||||
<div class="mt-1">
|
||||
<a href="/auth/privacy-settings" class="block w-full bg-golden-ale hover:bg-opacity-90 text-black font-medium py-2 px-4 rounded-md transition text-center">
|
||||
<i class="fas fa-user-shield mr-1"></i> Privacy Settings
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Control who can see your information</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,9 +177,11 @@
|
||||
<p class="text-gray-600 mb-4">The following actions are irreversible. Please proceed with caution.</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-md transition">
|
||||
Delete Account
|
||||
</button>
|
||||
<form action="/auth/delete-account" method="post" onsubmit="return confirm('Are you sure you want to delete your account? This action cannot be undone.');">
|
||||
<button type="submit" class="bg-white hover:bg-red-50 text-red-600 border border-red-600 font-medium py-2 px-4 rounded-md transition">
|
||||
Delete Account
|
||||
</button>
|
||||
</form>
|
||||
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-md transition">
|
||||
Leave All Teams
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<!-- Team Header -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="bg-irish-green p-6">
|
||||
@@ -88,10 +88,18 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-200 mr-3 overflow-hidden">
|
||||
<img src="https://picsum.photos/40" alt="User" class="w-full h-full object-cover">
|
||||
{% if member.user.picture %}
|
||||
<img src="{{ member.user.picture }}" alt="User" class="w-full h-full object-cover">
|
||||
{% else %}
|
||||
<div class="w-full h-full bg-irish-green flex items-center justify-center text-white text-sm">
|
||||
{{ member.user.username[0]|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">{{ member.user.username }}</p>
|
||||
<p class="font-medium">
|
||||
<a href="/auth/user/{{ member.user.id }}" class="hover:text-irish-green">{{ member.user.username }}</a>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">Joined {{ member.joined }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+94
-92
@@ -1,107 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2 class="text-2xl font-bold mb-6">Teams</h2>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold mb-6">Teams</h2>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6" role="alert">
|
||||
<span class="block sm:inline">{{ error }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6" role="alert">
|
||||
<span class="block sm:inline">{{ error }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Available Teams</h3>
|
||||
{% if teams %}
|
||||
<ul class="space-y-3">
|
||||
{% for team in teams %}
|
||||
<li class="border-b pb-2 flex justify-between items-center">
|
||||
<a href="/teams/{{ team.id }}" class="font-medium hover:text-irish-green">{{ team.name }}</a>
|
||||
{% if user %}
|
||||
{% if team.id in user_team_ids %}
|
||||
<span class="px-3 py-1 text-sm rounded-md bg-gray-200 text-gray-800">Member</span>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Available Teams</h3>
|
||||
{% if teams %}
|
||||
<ul class="space-y-3">
|
||||
{% for team in teams %}
|
||||
<li class="border-b pb-2 flex justify-between items-center">
|
||||
<a href="/teams/{{ team.id }}" class="font-medium hover:text-irish-green">{{ team.name }}</a>
|
||||
{% if user %}
|
||||
{% if team.id in user_team_ids %}
|
||||
<span class="px-3 py-1 text-sm rounded-md bg-gray-200 text-gray-800">Member</span>
|
||||
{% else %}
|
||||
<form action="/teams/join/{{ team.id }}" method="post" class="inline">
|
||||
<button type="submit"
|
||||
class="px-3 py-1 text-sm rounded-md"
|
||||
style="background-color: var(--golden-ale); color: var(--black-stout);">
|
||||
Join Team
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form action="/teams/join/{{ team.id }}" method="post" class="inline">
|
||||
<button type="submit"
|
||||
class="px-3 py-1 text-sm rounded-md"
|
||||
style="background-color: var(--golden-ale); color: var(--black-stout);">
|
||||
Join Team
|
||||
</button>
|
||||
</form>
|
||||
<a href="/auth/login?next=/teams"
|
||||
class="px-3 py-1 text-sm rounded-md"
|
||||
style="background-color: var(--cream-white); color: var(--irish-green); border: 1px solid var(--irish-green);">
|
||||
Login to Join
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="/auth/login?next=/teams"
|
||||
class="px-3 py-1 text-sm rounded-md"
|
||||
style="background-color: var(--cream-white); color: var(--irish-green); border: 1px solid var(--irish-green);">
|
||||
Login to Join
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="italic text-gray-500">No teams available yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="italic text-gray-500">No teams available yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Create New Team</h3>
|
||||
{% if user %}
|
||||
<form action="/teams/create" method="post" class="mt-4">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-sm font-medium mb-1">Team Name</label>
|
||||
<input type="text" name="name" id="name" placeholder="Enter team name" required
|
||||
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2"
|
||||
style="border-color: var(--irish-green); focus:ring-color: var(--irish-green);">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
Create Team
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 bg-gray-100 rounded-md">
|
||||
<p class="text-center mb-2">You need to be logged in to create a team</p>
|
||||
<a href="/auth/login"
|
||||
class="block w-full text-center px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
Log In
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user and user_team_ids %}
|
||||
<div class="mt-8">
|
||||
<h2 class="text-2xl font-bold mb-4">Your Teams</h2>
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for team in teams %}
|
||||
{% if team.id in user_team_ids %}
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--irish-green);">{{ team.name }}</h3>
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">Create New Team</h3>
|
||||
{% if user %}
|
||||
<form action="/teams/create" method="post" class="mt-4">
|
||||
<div class="mb-4">
|
||||
<span class="text-gray-600">{{ team.description|default("No description available", true)|truncate(120) }}</span>
|
||||
<label for="name" class="block text-sm font-medium mb-1">Team Name</label>
|
||||
<input type="text" name="name" id="name" placeholder="Enter team name" required
|
||||
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2"
|
||||
style="border-color: var(--irish-green); focus:ring-color: var(--irish-green);">
|
||||
</div>
|
||||
<a href="/teams/{{ team.id }}"
|
||||
class="block text-center w-full px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
View Team
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
Create Team
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="p-4 bg-gray-100 rounded-md">
|
||||
<p class="text-center mb-2">You need to be logged in to create a team</p>
|
||||
<a href="/auth/login"
|
||||
class="block w-full text-center px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
Log In
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user and user_team_ids %}
|
||||
<div class="mt-8">
|
||||
<h2 class="text-2xl font-bold mb-4">Your Teams</h2>
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for team in teams %}
|
||||
{% if team.id in user_team_ids %}
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--irish-green);">{{ team.name }}</h3>
|
||||
<div class="mb-4">
|
||||
<span class="text-gray-600">{{ team.description|default("No description available", true)|truncate(120) }}</span>
|
||||
</div>
|
||||
<a href="/teams/{{ team.id }}"
|
||||
class="block text-center w-full px-4 py-2 text-white rounded-md font-medium"
|
||||
style="background-color: var(--irish-green);">
|
||||
View Team
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-10 bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">About Teams</h3>
|
||||
<p class="mb-4">
|
||||
Teams are the heart of LeagueLedger. Join an existing team or create your own to start tracking your pub quiz triumphs!
|
||||
</p>
|
||||
<p class="italic">
|
||||
Every point counts in the journey to becoming pub quiz champions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-10 bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold mb-4" style="color: var(--irish-green);">About Teams</h3>
|
||||
<p class="mb-4">
|
||||
Teams are the heart of LeagueLedger. Join an existing team or create your own to start tracking your pub quiz triumphs!
|
||||
</p>
|
||||
<p class="italic">
|
||||
Every point counts in the journey to becoming pub quiz champions.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+102
-1
@@ -3,7 +3,7 @@ Authentication utilities for LeagueLedger.
|
||||
This module provides backward compatibility with the existing code while leveraging
|
||||
the new Starlette authentication system.
|
||||
"""
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from ..db import get_db
|
||||
@@ -157,5 +157,106 @@ async def requires_admin(request: Request, db: Session = Depends(get_db)) -> Use
|
||||
|
||||
return user
|
||||
|
||||
def check_privacy_permission(
|
||||
db: Session,
|
||||
profile_user: User,
|
||||
viewing_user_id: Optional[int],
|
||||
setting_name: str
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the viewing user has permission to see a specific profile setting
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
profile_user: The user whose profile is being viewed
|
||||
viewing_user_id: The ID of the user viewing the profile (None if not logged in)
|
||||
setting_name: The name of the setting to check (email, full_name, teams, points, achievements, events)
|
||||
|
||||
Returns:
|
||||
True if viewer has permission to see the setting, False otherwise
|
||||
"""
|
||||
# Admin users can see everything
|
||||
if viewing_user_id:
|
||||
viewing_user = db.query(User).filter(User.id == viewing_user_id).first()
|
||||
if viewing_user and viewing_user.is_admin:
|
||||
return True
|
||||
|
||||
# Owner can see everything on their own profile
|
||||
if viewing_user_id and viewing_user_id == profile_user.id:
|
||||
return True
|
||||
|
||||
# Get privacy settings for this user
|
||||
privacy_settings = profile_user.get_privacy_settings()
|
||||
privacy_level = privacy_settings.get(setting_name, "private")
|
||||
|
||||
# Public settings are visible to everyone
|
||||
if privacy_level == "public":
|
||||
return True
|
||||
|
||||
# Private settings are only visible to the user and admins (handled above)
|
||||
if privacy_level == "private":
|
||||
return False
|
||||
|
||||
# For "friends" level (team members), check if viewing user is in same team
|
||||
if privacy_level == "friends" and viewing_user_id:
|
||||
# Get teams of the profile user
|
||||
profile_user_team_ids = [
|
||||
membership.team_id
|
||||
for membership in db.query(TeamMembership).filter(
|
||||
TeamMembership.user_id == profile_user.id
|
||||
).all()
|
||||
]
|
||||
|
||||
# Check if viewing user is in any of the same teams
|
||||
common_team = db.query(TeamMembership).filter(
|
||||
TeamMembership.user_id == viewing_user_id,
|
||||
TeamMembership.team_id.in_(profile_user_team_ids)
|
||||
).first()
|
||||
|
||||
return common_team is not None
|
||||
|
||||
return False
|
||||
|
||||
def get_viewable_profile_data(
|
||||
db: Session,
|
||||
profile_user: User,
|
||||
viewing_user_id: Optional[int]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get profile data respecting privacy settings
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
profile_user: The user whose profile is being viewed
|
||||
viewing_user_id: The ID of the user viewing the profile (None if not logged in)
|
||||
|
||||
Returns:
|
||||
Dictionary with profile data that the viewing user is allowed to see
|
||||
"""
|
||||
data = {
|
||||
"username": profile_user.username,
|
||||
"picture": profile_user.picture,
|
||||
"is_admin": profile_user.is_admin,
|
||||
"created_at": profile_user.created_at
|
||||
}
|
||||
|
||||
# Only include email if permission allows
|
||||
if check_privacy_permission(db, profile_user, viewing_user_id, "email"):
|
||||
data["email"] = profile_user.email
|
||||
|
||||
# Only include full name if permission allows
|
||||
if check_privacy_permission(db, profile_user, viewing_user_id, "full_name"):
|
||||
data["first_name"] = profile_user.first_name
|
||||
data["last_name"] = profile_user.last_name
|
||||
|
||||
# For teams, points, achievements, events - we'll just include permission flags
|
||||
# The actual data will be loaded by the view functions when needed
|
||||
data["can_view_teams"] = check_privacy_permission(db, profile_user, viewing_user_id, "teams")
|
||||
data["can_view_points"] = check_privacy_permission(db, profile_user, viewing_user_id, "points")
|
||||
data["can_view_achievements"] = check_privacy_permission(db, profile_user, viewing_user_id, "achievements")
|
||||
data["can_view_events"] = check_privacy_permission(db, profile_user, viewing_user_id, "events")
|
||||
|
||||
return data
|
||||
|
||||
# Note: For new code, consider using the decorators in app.auth.permissions instead
|
||||
# of these dependency functions directly
|
||||
|
||||
+387
-3
@@ -1,11 +1,13 @@
|
||||
from fastapi import APIRouter, Request, Depends, Form, HTTPException, status, BackgroundTasks
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi import APIRouter, Request, Depends, Form, HTTPException, status, BackgroundTasks, UploadFile, File
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from typing import Optional, List, Dict, Any
|
||||
import secrets
|
||||
import os
|
||||
import uuid
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from starlette.status import HTTP_303_SEE_OTHER, HTTP_302_FOUND
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
@@ -445,7 +447,10 @@ async def oauth_callback(
|
||||
user.first_name = first_name
|
||||
if last_name and not user.last_name:
|
||||
user.last_name = last_name
|
||||
if picture and not user.picture:
|
||||
|
||||
# Only update profile picture if one doesn't exist yet or if it was never manually deleted
|
||||
# We track manual deletion by setting a flag in the database
|
||||
if picture and (user.picture is None and not user.picture_manually_deleted):
|
||||
user.picture = picture
|
||||
|
||||
# Update last login time
|
||||
@@ -529,6 +534,172 @@ async def profile_page(request: Request, db: Session = Depends(get_db)):
|
||||
{"request": request, "user": user}
|
||||
)
|
||||
|
||||
@router.post("/update-profile-picture", response_class=HTMLResponse)
|
||||
async def update_profile_picture(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle profile picture upload"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Validate file type
|
||||
valid_extensions = [".jpg", ".jpeg", ".png"]
|
||||
file_ext = os.path.splitext(file.filename)[1].lower()
|
||||
|
||||
if file_ext not in valid_extensions:
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Invalid file type. Only JPG and PNG are allowed."}
|
||||
)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
upload_dir = Path("app/static/uploads/profile_pictures")
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate unique filename
|
||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
file_path = upload_dir / unique_filename
|
||||
|
||||
# Save the file
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Update user profile with the picture URL
|
||||
user.picture = f"/static/uploads/profile_pictures/{unique_filename}"
|
||||
user.picture_manually_deleted = False # Reset manual deletion flag
|
||||
db.commit()
|
||||
|
||||
# Redirect back to profile with success message
|
||||
return RedirectResponse(
|
||||
"/auth/profile?message=Profile+picture+updated+successfully",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
@router.post("/delete-profile-picture", response_class=HTMLResponse)
|
||||
async def delete_profile_picture(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle profile picture deletion"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Only proceed if user has a profile picture
|
||||
if user.picture:
|
||||
# Get the file path
|
||||
image_path = user.picture.replace("/static/", "app/static/")
|
||||
|
||||
# Try to delete the file if it exists
|
||||
try:
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
except Exception as e:
|
||||
print(f"Error deleting profile picture file: {str(e)}")
|
||||
# Continue anyway since we still want to clear the database entry
|
||||
|
||||
# Clear the picture field in the database and set the manually deleted flag
|
||||
user.picture = None
|
||||
user.picture_manually_deleted = True
|
||||
db.commit()
|
||||
|
||||
# Redirect back to profile with success message
|
||||
return RedirectResponse(
|
||||
"/auth/profile?message=Profile+picture+deleted+successfully",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
@router.post("/update-username", response_class=HTMLResponse)
|
||||
async def update_username(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle username update"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Check if username is unchanged
|
||||
if user.username == username:
|
||||
return RedirectResponse(
|
||||
"/auth/profile?message=No+changes+made+to+username",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
# Validate username
|
||||
if len(username) < 3:
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Username must be at least 3 characters long"}
|
||||
)
|
||||
|
||||
if len(username) > 30:
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Username must be less than 30 characters long"}
|
||||
)
|
||||
|
||||
# Check if username contains only allowed characters (alphanumeric, underscore, hyphen)
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Username can only contain letters, numbers, underscores and hyphens"}
|
||||
)
|
||||
|
||||
# Check if username already exists
|
||||
existing_user = db.query(User).filter(User.username == username).first()
|
||||
if existing_user:
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Username already taken"}
|
||||
)
|
||||
|
||||
# Update user's username
|
||||
old_username = user.username
|
||||
user.username = username
|
||||
|
||||
# Update session with new username
|
||||
request.session["username"] = username
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
"/auth/profile?message=Username+updated+successfully",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error updating username: {str(e)}")
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "An error occurred while updating your username"}
|
||||
)
|
||||
|
||||
@router.get("/change-password", response_class=HTMLResponse)
|
||||
async def change_password_page(request: Request, error: Optional[str] = None, message: Optional[str] = None):
|
||||
"""Change password page"""
|
||||
@@ -743,6 +914,219 @@ async def reset_password_post(
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
@router.post("/delete-account", response_class=HTMLResponse)
|
||||
async def delete_account(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle account deletion"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Don't allow admins to delete their accounts through this flow
|
||||
# to prevent accidentally removing the only admin account
|
||||
if user.is_admin:
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "Admin accounts cannot be deleted through this page. Please contact the system administrator."}
|
||||
)
|
||||
|
||||
try:
|
||||
# Handle team memberships (anonymize rather than delete)
|
||||
from ..models import TeamMembership, TeamJoinRequest, UserPoints, EventAttendee
|
||||
|
||||
# Get all team memberships
|
||||
memberships = db.query(TeamMembership).filter(TeamMembership.user_id == user_id).all()
|
||||
|
||||
# Clean up any pending join requests
|
||||
db.query(TeamJoinRequest).filter(TeamJoinRequest.user_id == user_id).delete()
|
||||
|
||||
# Instead of deleting data completely, we'll anonymize it to keep integrity
|
||||
# Update the username and email to indicate this is a deleted account
|
||||
anonymous_username = f"deleted_user_{user_id}"
|
||||
anonymous_email = f"deleted_{user_id}@deleted.user"
|
||||
|
||||
user.username = anonymous_username
|
||||
user.email = anonymous_email
|
||||
user.is_active = False
|
||||
user.hashed_password = None
|
||||
user.picture = None
|
||||
user.first_name = None
|
||||
user.last_name = None
|
||||
user.oauth_id = None
|
||||
user.oauth_provider = None
|
||||
user.additional_oauth_providers = None
|
||||
|
||||
# Mark account as deactivated
|
||||
db.commit()
|
||||
|
||||
# Clear session
|
||||
request.session.clear()
|
||||
|
||||
# Show success page
|
||||
return templates.TemplateResponse(
|
||||
"auth/account_deleted.html",
|
||||
{"request": request}
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error deleting account: {str(e)}")
|
||||
return templates.TemplateResponse(
|
||||
"auth/profile.html",
|
||||
{"request": request, "user": user, "error": "An error occurred while deleting your account. Please try again later."}
|
||||
)
|
||||
|
||||
@router.get("/privacy-settings", response_class=HTMLResponse)
|
||||
async def privacy_settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Display privacy settings page"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get current privacy settings
|
||||
privacy_settings = user.get_privacy_settings()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"auth/privacy_settings.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user,
|
||||
"privacy_settings": privacy_settings,
|
||||
"privacy_options": [
|
||||
{"value": "public", "label": "Everyone", "description": "Visible to all users"},
|
||||
{"value": "friends", "label": "Team Members", "description": "Only visible to members of your teams"},
|
||||
{"value": "private", "label": "Private", "description": "Only visible to you and admins"}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@router.post("/privacy-settings", response_class=HTMLResponse)
|
||||
async def update_privacy_settings(
|
||||
request: Request,
|
||||
email_visibility: str = Form(...),
|
||||
full_name_visibility: str = Form(...),
|
||||
teams_visibility: str = Form(...),
|
||||
points_visibility: str = Form(...),
|
||||
achievements_visibility: str = Form(...),
|
||||
events_visibility: str = Form(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Handle privacy settings update"""
|
||||
# Check if user is logged in
|
||||
user_id = request.session.get("user_id")
|
||||
if not user_id:
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Get user from database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
request.session.clear()
|
||||
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Validate inputs
|
||||
valid_options = ["public", "friends", "private"]
|
||||
privacy_settings = {
|
||||
"email": email_visibility if email_visibility in valid_options else "private",
|
||||
"full_name": full_name_visibility if full_name_visibility in valid_options else "friends",
|
||||
"teams": teams_visibility if teams_visibility in valid_options else "public",
|
||||
"points": points_visibility if points_visibility in valid_options else "public",
|
||||
"achievements": achievements_visibility if achievements_visibility in valid_options else "public",
|
||||
"events": events_visibility if events_visibility in valid_options else "friends"
|
||||
}
|
||||
|
||||
# Update user privacy settings
|
||||
user.privacy_settings = privacy_settings
|
||||
db.commit()
|
||||
|
||||
# Redirect back to privacy settings with success message
|
||||
return RedirectResponse(
|
||||
"/auth/privacy-settings?message=Privacy+settings+updated+successfully",
|
||||
status_code=HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
@router.get("/user/{user_id}", response_class=HTMLResponse)
|
||||
async def view_user_profile(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""View another user's profile with privacy settings applied"""
|
||||
# Check if the requested user exists
|
||||
profile_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not profile_user:
|
||||
return templates.TemplateResponse(
|
||||
"error.html",
|
||||
{"request": request, "error": "User not found"}
|
||||
)
|
||||
|
||||
# Get current logged-in user (if any)
|
||||
current_user_id = request.session.get("user_id")
|
||||
current_user = None
|
||||
if current_user_id:
|
||||
current_user = db.query(User).filter(User.id == current_user_id).first()
|
||||
|
||||
# Check if the user is viewing their own profile
|
||||
if current_user_id and current_user_id == user_id:
|
||||
return RedirectResponse("/auth/profile", status_code=HTTP_303_SEE_OTHER)
|
||||
|
||||
# Import privacy utilities
|
||||
from ..utils.auth import get_viewable_profile_data, check_privacy_permission
|
||||
|
||||
# Get viewable profile data based on privacy settings
|
||||
profile_data = get_viewable_profile_data(db, profile_user, current_user_id)
|
||||
|
||||
# If the user can view teams, fetch team data
|
||||
teams = []
|
||||
if profile_data["can_view_teams"]:
|
||||
from ..models import TeamMembership, Team
|
||||
team_memberships = db.query(TeamMembership, Team).join(
|
||||
Team, TeamMembership.team_id == Team.id
|
||||
).filter(
|
||||
TeamMembership.user_id == user_id
|
||||
).all()
|
||||
|
||||
teams = [
|
||||
{
|
||||
"id": team.id,
|
||||
"name": team.name,
|
||||
"is_captain": membership.is_captain
|
||||
} for membership, team in team_memberships
|
||||
]
|
||||
|
||||
# If the user can view points, fetch points data
|
||||
total_points = 0
|
||||
if profile_data["can_view_points"]:
|
||||
from ..models import UserPoints
|
||||
points_records = db.query(UserPoints).filter(UserPoints.user_id == user_id).all()
|
||||
total_points = sum(record.points for record in points_records)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"auth/view_profile.html",
|
||||
{
|
||||
"request": request,
|
||||
"profile": profile_data,
|
||||
"profile_user_id": user_id,
|
||||
"user": current_user, # Pass the current user for menu display
|
||||
"teams": teams,
|
||||
"total_points": total_points,
|
||||
}
|
||||
)
|
||||
|
||||
def validate_password_strength(password: str) -> Optional[str]:
|
||||
"""
|
||||
Validates password strength based on the following criteria:
|
||||
|
||||
Reference in New Issue
Block a user