Referral Rewards V1

This commit is contained in:
Flatlogic Bot 2026-02-28 22:50:06 +00:00
parent ce20f7b1f5
commit 82aa5f8851
23 changed files with 318 additions and 38 deletions

View File

@ -43,6 +43,9 @@ CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SAMESITE = "None"
# IMPORTANT for reverse proxy (Apache) to correctly identify HTTPS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
@ -65,6 +68,7 @@ MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'core.middleware.ReferralMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@ -156,13 +160,13 @@ STATICFILES_DIRS = [
]
# Email
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
EMAIL_HOST = os.getenv("SMTP_HOST", "127.0.0.1")
EMAIL_PORT = int(os.getenv("SMTP_PORT", "587"))
EMAIL_HOST_USER = os.getenv("SMTP_USER", "")
EMAIL_HOST_PASSWORD = os.getenv("SMTP_PASS", "")
EMAIL_USE_TLS = os.getenv("SMTP_SECURE", "tls").lower() == "tls"
EMAIL_USE_SSL = os.getenv("SMTP_SECURE", "tls").lower() == "ssl"
DEFAULT_FROM_EMAIL = os.getenv("MAIL_FROM", "no-reply@example.com")
CONTACT_EMAIL_TO = [
item.strip()
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
@ -186,4 +190,4 @@ if EMAIL_USE_SSL:
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_REDIRECT_URL = 'dashboard'
LOGOUT_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

Binary file not shown.

View File

@ -20,3 +20,6 @@ class SignupForm(UserCreationForm):
if commit:
user.save()
return user
class ResendActivationForm(forms.Form):
email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'Enter your email'}))

View File

Binary file not shown.

View File

View File

@ -0,0 +1,48 @@
import os
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
from core.tokens import account_activation_token
User = get_user_model()
class Command(BaseCommand):
help = 'Resend activation email to a specific user'
def add_arguments(self, parser):
parser.add_argument('username', type=str)
parser.add_argument('--domain', type=str, default=os.getenv("HOST_FQDN", "localhost:8000"))
def handle(self, *args, **args_options):
username = args_options['username']
domain = args_options['domain']
try:
user = User.objects.get(username=username)
if user.is_active:
self.stdout.write(self.style.WARNING(f"User '{username}' is already active."))
return
mail_subject = 'Activate your Referral Rewards account.'
message = render_to_string('core/emails/activation_email.html', {
'user': user,
'domain': domain,
'protocol': 'https', # Defaulting to https for Flatlogic
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
})
email = EmailMessage(
mail_subject, message, to=[user.email]
)
email.send()
self.stdout.write(self.style.SUCCESS(f"Successfully sent activation email to '{username}' ({user.email})."))
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User '{username}' does not exist."))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error sending email: {str(e)}"))

11
core/middleware.py Normal file
View File

@ -0,0 +1,11 @@
class ReferralMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
ref_code = request.GET.get('ref')
if ref_code:
request.session['ref'] = ref_code
response = self.get_response(request)
return response

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2026-02-28 22:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='profile',
name='referred_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referrals_made', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='Referral',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('referral_code', models.CharField(max_length=20)),
('status', models.CharField(choices=[('pending', 'Pending'), ('successful', 'Successful'), ('rewarded', 'Rewarded')], default='pending', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('referred_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_referrals', to=settings.AUTH_USER_MODEL)),
('referrer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_referrals', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -7,6 +7,7 @@ import uuid
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
referral_code = models.CharField(max_length=20, unique=True, blank=True)
referred_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='referrals_made')
points = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
@ -18,6 +19,21 @@ class Profile(models.Model):
self.referral_code = str(uuid.uuid4()).replace('-', '')[:8].upper()
super().save(*args, **kwargs)
class Referral(models.Model):
referrer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_referrals')
referred_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_referrals', null=True, blank=True)
referral_code = models.CharField(max_length=20)
status = models.CharField(max_length=20, default='pending', choices=[
('pending', 'Pending'),
('successful', 'Successful'),
('rewarded', 'Rewarded'),
])
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"Referral from {self.referrer.username} (Code: {self.referral_code})"
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:

View File

@ -25,9 +25,9 @@
<div class="card h-100 border-0 shadow-sm p-4 rounded-4 bg-white text-center">
<p class="text-muted small text-uppercase fw-bold mb-2">Total Points</p>
<h3 class="display-5 fw-bold text-primary mb-0">{{ profile.points }}</h3>
<p class="text-muted mt-2 mb-0">Points to next reward: 100</p>
<p class="text-muted mt-2 mb-0">Total successful referrals: {{ referrals.count }}</p>
<div class="progress mt-3" style="height: 8px;">
<div class="progress-bar bg-primary" style="width: {{ profile.points|default:0 }}%;"></div>
<div class="progress-bar bg-primary" style="width: {{ profile.points }}%;"></div>
</div>
</div>
</div>
@ -36,16 +36,53 @@
<div class="col-lg-8">
<div class="card h-100 border-0 shadow-sm p-4 rounded-4 bg-white">
<h4 class="mb-4">Your Referral Link</h4>
<p class="text-muted mb-4">Share this code with your friends and earn points when they sign up!</p>
<p class="text-muted mb-4">Share this link with your friends and earn points when they sign up and activate their account!</p>
<div class="input-group mb-3">
<input type="text" id="referralCode" class="form-control form-control-lg bg-light border-0" value="{{ profile.referral_code }}" readonly>
<button class="btn btn-primary px-4" type="button" onclick="copyCode()">Copy Code</button>
<input type="text" id="referralLink" class="form-control form-control-lg bg-light border-0" value="{{ referral_link }}" readonly>
<button class="btn btn-primary px-4" type="button" onclick="copyLink()">Copy Link</button>
</div>
<div class="d-flex gap-3 mt-4">
<button class="btn btn-outline-primary flex-fill py-2">Share on Twitter</button>
<button class="btn btn-outline-primary flex-fill py-2">Share on WhatsApp</button>
<a href="https://twitter.com/intent/tweet?text=Join%20me%20on%20Referral%20Rewards%21%20{{ referral_link }}" target="_blank" class="btn btn-outline-primary flex-fill py-2">Share on Twitter</a>
<a href="https://wa.me/?text=Join%20me%20on%20Referral%20Rewards%21%20{{ referral_link }}" target="_blank" class="btn btn-outline-primary flex-fill py-2">Share on WhatsApp</a>
</div>
</div>
</div>
<!-- Recent Referrals -->
<div class="col-12 mt-4">
<h4 class="mb-4">Successful Referrals</h4>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden bg-white">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="px-4 py-3 border-0">Username</th>
<th class="px-4 py-3 border-0 text-center">Date Joined</th>
<th class="px-4 py-3 border-0 text-center">Status</th>
<th class="px-4 py-3 border-0 text-end">Reward</th>
</tr>
</thead>
<tbody>
{% for referral in referrals %}
<tr>
<td class="px-4 py-3 border-0">{{ referral.referred_user.username }}</td>
<td class="px-4 py-3 border-0 text-center text-muted">{{ referral.completed_at|date:"M d, Y" }}</td>
<td class="px-4 py-3 border-0 text-center">
<span class="badge bg-success rounded-pill px-3">{{ referral.get_status_display }}</span>
</td>
<td class="px-4 py-3 border-0 text-end fw-bold text-primary">+10 Points</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="px-4 py-5 text-center text-muted">
No referrals yet. Share your link to start earning rewards!
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
@ -96,8 +133,8 @@
</div>
<script>
function copyCode() {
var copyText = document.getElementById("referralCode");
function copyLink() {
var copyText = document.getElementById("referralLink");
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
@ -115,4 +152,4 @@
}, 2000);
}
</script>
{% endblock %}
{% endblock %}

View File

@ -32,7 +32,8 @@
<input type="password" name="password" class="form-control" id="id_password" placeholder="Password" required>
</div>
<div class="text-end mb-4">
<div class="d-flex justify-content-between mb-4">
<a href="{% url 'resend_activation' %}" class="text-decoration-none small fw-bold">Resend activation?</a>
<a href="{% url 'password_reset' %}" class="text-decoration-none small fw-bold">Forgot password?</a>
</div>

View File

@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% block title %}Resend Activation - Referral Rewards{% endblock %}
{% block content %}
<div class="container py-5 mt-5">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card border-0 shadow-lg p-4 p-md-5 rounded-4">
<div class="text-center mb-4">
<h2 class="mb-3">Resend Activation</h2>
<p class="text-muted">Enter your email to receive a new activation link.</p>
</div>
<form method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="mb-4">
<label for="{{ form.email.id_for_label }}" class="form-label fw-600">Email Address</label>
{{ form.email }}
{% if form.email.errors %}
<div class="text-danger small mt-1">
{{ form.email.errors }}
</div>
{% endif %}
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mb-3">Resend Activation Link</button>
<div class="text-center mt-3">
<a href="{% url 'login' %}" class="text-decoration-none small fw-bold">Back to Login</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -6,6 +6,7 @@ urlpatterns = [
path("", views.home, name="home"),
path("signup/", views.signup, name="signup"),
path("activate/<uidb64>/<token>/", views.activate, name="activate"),
path("resend-activation/", views.resend_activation, name="resend_activation"),
path("dashboard/", views.dashboard, name="dashboard"),
path("login/", auth_views.LoginView.as_view(template_name='core/login.html'), name="login"),
path("logout/", views.logout_view, name="logout"),

View File

@ -7,13 +7,30 @@ from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.utils import timezone
from .models import Profile
from .forms import SignupForm
from .models import Profile, Referral
from .forms import SignupForm, ResendActivationForm
from .tokens import account_activation_token
User = get_user_model()
def send_activation_email(request, user):
current_site = get_current_site(request)
mail_subject = 'Activate your Referral Rewards account.'
message = render_to_string('core/emails/activation_email.html', {
'user': user,
'domain': current_site.domain,
'protocol': 'https' if request.is_secure() else 'http',
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
})
to_email = user.email
email = EmailMessage(
mail_subject, message, to=[to_email]
)
return email.send()
def home(request):
if request.user.is_authenticated:
return redirect('dashboard')
@ -29,22 +46,30 @@ def signup(request):
user.is_active = False
user.save()
# Referral logic
ref_code = request.session.get('ref')
if ref_code:
try:
referrer_profile = Profile.objects.get(referral_code=ref_code)
referrer = referrer_profile.user
# Update user's profile with referrer
user.profile.referred_by = referrer
user.profile.save()
# Create Referral record
Referral.objects.create(
referrer=referrer,
referred_user=user,
referral_code=ref_code,
status='pending'
)
except Profile.DoesNotExist:
pass # Invalid referral code, just ignore
# Send activation email
current_site = get_current_site(request)
mail_subject = 'Activate your Referral Rewards account.'
message = render_to_string('core/emails/activation_email.html', {
'user': user,
'domain': current_site.domain,
'protocol': 'https' if request.is_secure() else 'http',
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
})
to_email = form.cleaned_data.get('email')
email = EmailMessage(
mail_subject, message, to=[to_email]
)
try:
email.send()
send_activation_email(request, user)
messages.success(request, 'Please confirm your email address to complete the registration. Check your inbox.')
except Exception as e:
messages.error(request, f'Error sending email: {str(e)}. Please contact support.')
@ -54,6 +79,34 @@ def signup(request):
form = SignupForm()
return render(request, 'core/signup.html', {'form': form})
def resend_activation(request):
if request.user.is_authenticated:
return redirect('dashboard')
if request.method == 'POST':
form = ResendActivationForm(request.POST)
if form.is_valid():
email_addr = form.cleaned_data.get('email')
try:
user = User.objects.get(email=email_addr)
if not user.is_active:
send_activation_email(request, user)
messages.success(request, 'A new activation email has been sent. Please check your inbox.')
return redirect('login')
else:
messages.info(request, 'This account is already active. Please log in.')
return redirect('login')
except User.DoesNotExist:
# Security: Don't reveal if email exists, just say it's sent if it's inactive
messages.success(request, 'If an inactive account with that email exists, an activation email has been sent.')
return redirect('login')
except Exception as e:
messages.error(request, f'Error sending email: {str(e)}. Please contact support.')
else:
form = ResendActivationForm()
return render(request, 'core/resend_activation.html', {'form': form})
def activate(request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
@ -63,6 +116,21 @@ def activate(request, uidb64, token):
if user is not None and account_activation_token.check_token(user, token):
user.is_active = True
user.save()
# Complete referral if exists
referral = Referral.objects.filter(referred_user=user, status='pending').first()
if referral:
referral.status = 'successful'
referral.completed_at = timezone.now()
referral.save()
# Reward referrer (e.g. add 10 points)
referrer_profile = referral.referrer.profile
referrer_profile.points += 10
referrer_profile.save()
messages.info(request, f'You were successfully referred by {referral.referrer.username}!')
login(request, user)
messages.success(request, 'Thank you for your email confirmation. Now you can enjoy our services.')
return redirect('dashboard')
@ -72,10 +140,23 @@ def activate(request, uidb64, token):
@login_required
def dashboard(request):
# Ensure profile exists (though signal should handle it)
profile, created = Profile.objects.get_or_create(user=request.user)
return render(request, 'core/dashboard.html', {'profile': profile})
# Get successful referrals
referrals = Referral.objects.filter(referrer=request.user, status__in=['successful', 'rewarded']).order_by('-completed_at')
current_site = get_current_site(request)
domain = current_site.domain
protocol = 'https' if request.is_secure() else 'http'
referral_link = f"{protocol}://{domain}/?ref={profile.referral_code}"
context = {
'profile': profile,
'referrals': referrals,
'referral_link': referral_link,
}
return render(request, 'core/dashboard.html', context)
def logout_view(request):
logout(request)
return redirect('home')
return redirect('home')