first commit

This commit is contained in:
vanalmsick 2025-09-27 18:19:06 +01:00
commit e7f627801f
152 changed files with 35352 additions and 0 deletions

View file

View file

@ -0,0 +1,25 @@
from django.contrib import admin
from .models import CustomUser, RecalcRequest
# Register your models here.
@admin.register(CustomUser)
class CustomUserAdmin(admin.ModelAdmin):
"""Admin view of CustomUser"""
list_display = [
"username",
"first_name",
"last_name",
]
@admin.register(RecalcRequest)
class RecalcRequestAdmin(admin.ModelAdmin):
"""Admin view of RecalcRequest"""
list_display = [
"user",
"goal",
"start_datetime",
"done",
]

View file

@ -0,0 +1,71 @@
# myapp/monitor.py
from datetime import datetime, timezone
from django.conf import settings
class RateLimitExceeded(Exception):
"""Raised when the API rate limit is exceeded."""
pass
class APIRequestMonitor:
""" API request rate limiter"""
def __init__(self, limit_15min: int, limit_day: int):
self.limit_15min = limit_15min
self.limit_day = limit_day
self.current_15min_slot = self._get_15min_slot()
self.current_day = self._get_day()
self.count_15min = 0
self.count_day = 0
def _get_15min_slot(self):
now = datetime.now(timezone.utc)
return now.replace(minute=(now.minute // 15) * 15, second=0, microsecond=0)
def _get_day(self):
return datetime.now(timezone.utc).date()
def _maybe_reset_counters(self):
now_slot = self._get_15min_slot()
today = self._get_day()
if now_slot != self.current_15min_slot:
self.current_15min_slot = now_slot
self.count_15min = 0
if today != self.current_day:
self.current_day = today
self.count_day = 0
def log_request(self, response) -> bool:
self._maybe_reset_counters()
self.count_day += 1
if response.status_code == 429:
self.count_15min = self.limit_15min
raise RateLimitExceeded("API rate limit exceeded")
if self.count_15min >= self.limit_15min or self.count_day >= self.limit_day:
raise RateLimitExceeded("API rate limit probably exceeded")
self.count_15min += 1
print(f'Strava API Request (15min: {self.count_15min} / {self.limit_15min}, day: {self.count_day} / {self.limit_day})')
return True
def count_requests(self):
self._maybe_reset_counters()
return {
"requests_15min": self.count_15min,
"requests_today": self.count_day
}
def ok_workout_requests(self):
stats = self.count_requests()
return ((stats["requests_today"] <= self.limit_day * 0.8) & (stats["requests_15min"] <= self.limit_15min * 0.66))
def ok_linkage_requests(self):
stats = self.count_requests()
return ((stats["requests_today"] <= self.limit_day) & (stats["requests_15min"] <= self.limit_15min))
# Singleton instance
strava_api_monitor = APIRequestMonitor(limit_15min=settings.STRAVA_LIMIT_15MIN, limit_day=settings.STRAVA_LIMIT_DAY)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CustomUserConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'custom_user'

View file

@ -0,0 +1,336 @@
import datetime, random
from openai import OpenAI
from django.core.cache import cache
from django.conf import settings
from django.apps import apps
from django.template.loader import render_to_string
from workout_challenge.celery import app
from django.db.models import Sum, Count, Q
from django.db.models.functions import TruncDate, TruncDay
from .multipurpose import send_email
from competition.stats import get_competition_stats
@app.task()
def welcome_email(user_pk):
"""Welcome email for new users."""
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_obj = CustomUser.objects.get(pk=user_pk)
email_subject = 'Welcome to the Workout Challenge!'
email_body = render_to_string(
"email_welcome.html",
{
'first_name': user_obj.first_name,
'MAIN_HOST': settings.MAIN_HOST,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO[0] if settings.EMAIL_REPLY_TO is not None else settings.EMAIL_FROM,
'link_strava_note': user_obj.strava_refresh_token is None or user_obj.strava_refresh_token == '',
}
)
if settings.DEBUG:
with open('tmp_email.html', 'w') as file:
file.write(email_body)
send_email(subject=email_subject, body=email_body, to_email=user_obj.email)
return {'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email}
@app.task()
def send_all_log_workouts_email():
print("Scheduling log workout emails...")
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_lst = CustomUser.objects.filter(
Q(my_competitions__start_date__lte=datetime.date.today()) &
Q(my_competitions__end_date__gte=datetime.date.today())
).order_by('pk')
task_log = []
if len(user_lst) > 0:
eta_steps = max(min((60 * 60) // len(user_lst), 60), 10)
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10)
for user_obj in user_lst:
result = log_workouts_email.apply_async(args=[user_obj.pk], eta=eta)
task_log.append({'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email, 'task_id': result.task_id, 'eta': eta.isoformat()})
eta += datetime.timedelta(seconds=eta_steps)
return task_log
@app.task()
def log_workouts_email(user_pk):
"""Email reminder for users to please log their workouts."""
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_obj = CustomUser.objects.get(pk=user_pk)
workout_obj_lst = user_obj.workout_set.order_by('-start_datetime')[:3]
email_subject = 'Workout Challenge - Log Your Workouts!'
email_body = render_to_string(
"email_log_workouts.html",
{
'first_name': user_obj.first_name,
'last_workouts': workout_obj_lst,
'link_strava_note': user_obj.strava_refresh_token is None or user_obj.strava_refresh_token == '',
'MAIN_HOST': settings.MAIN_HOST,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO[0] if settings.EMAIL_REPLY_TO is not None else settings.EMAIL_FROM,
}
)
if settings.DEBUG:
with open('tmp_email.html', 'w') as file:
file.write(email_body)
send_email(subject=email_subject, body=email_body, to_email=user_obj.email)
return {'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email}
@app.task()
def send_all_competition_start_email():
print("Scheduling competition start emails...")
Competition = apps.get_model('competition', 'Competition')
competition_lst = Competition.objects.filter(start_date=datetime.date.today() + datetime.timedelta(days=1)).order_by('pk')
task_log = []
for i, competition_obj in enumerate(competition_lst):
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10) + datetime.timedelta(minutes=(15 * i))
user_lst = competition_obj.user.all().order_by('pk')
if len(user_lst) > 0:
eta_steps = max(min((60 * 60) // len(user_lst), 60), 10)
for user_obj in user_lst:
result = competition_start_email.apply_async(args=[competition_obj.pk, user_obj.pk], eta=eta)
task_log.append({'user_pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email, 'competition_pk': competition_obj.pk, 'competition_name': competition_obj.name, 'task_id': result.task_id, 'eta': eta.isoformat()})
eta += datetime.timedelta(seconds=eta_steps)
return task_log
@app.task()
def competition_start_email(competition_pk, user_pk):
"""Email for competition start tomorrow."""
Competition = apps.get_model('competition', 'Competition')
competition_obj = Competition.objects.get(pk=competition_pk)
goal_objs = competition_obj.activitygoal_set.all()
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_obj = CustomUser.objects.get(pk=user_pk)
email_subject = 'Workout Challenge - READY, SET, GO!'
email_body = render_to_string(
"email_competition_start.html",
{
'first_name': user_obj.first_name,
'MAIN_HOST': settings.MAIN_HOST,
'competition': competition_obj,
'goals': goal_objs,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO[0] if settings.EMAIL_REPLY_TO is not None else settings.EMAIL_FROM,
'goal_equalizer_note': user_obj.scaling_kcal == 1 and user_obj.scaling_distance == 1,
}
)
if settings.DEBUG:
with open('tmp_email.html', 'w') as file:
file.write(email_body)
send_email(subject=email_subject, body=email_body, to_email=user_obj.email)
return ({'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email})
@app.task()
def send_all_leaderboard_emails():
print("Scheduling leaderboard emails...")
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_lst = CustomUser.objects.filter(my_competitions__start_date__lt=datetime.date.today(), my_competitions__end_date__gte=datetime.date.today()).order_by('pk')
task_log = []
if len(user_lst) > 0:
eta_steps = max(min((60 * 60) // len(user_lst), 60), 10)
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10)
for user_obj in user_lst:
result = leaderboard_email.apply_async(args=[user_obj.pk], eta=eta)
task_log.append({'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email, 'task_id': result.task_id, 'eta': eta.isoformat()})
eta += datetime.timedelta(seconds=eta_steps)
return task_log
@app.task()
def leaderboard_email(user_pk):
"""Email to send users their leaderboard."""
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_obj = CustomUser.objects.get(pk=user_pk)
competition_all_data = []
competition_7d_data = []
for competition in user_obj.my_competitions.filter(start_date__lte=datetime.date.today(), end_date__gte=datetime.date.today()).order_by('-start_date'):
competition_all_stats = get_competition_stats(competition.pk)
competition_all_data.append({
'competition': competition_all_stats['competition'],
'leaderboard': competition_all_stats['leaderboard'],
})
competition_7d_stats = get_competition_stats(competition.pk, last_seven_days=True)
competition_7d_data.append({
'competition': competition_7d_stats['competition'],
'leaderboard': competition_7d_stats['leaderboard'],
})
email_subject = 'Workout Challenge - Your Spot on the Leaderboard!'
email_body = render_to_string(
"email_leaderboard.html",
{
'first_name': user_obj.first_name,
'MAIN_HOST': settings.MAIN_HOST,
'competitions_all': competition_all_data,
'competitions_7d': competition_7d_data,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO[0] if settings.EMAIL_REPLY_TO is not None else settings.EMAIL_FROM,
'goal_equalizer_note': user_obj.scaling_kcal == 1 and user_obj.scaling_distance == 1,
}
)
if settings.DEBUG:
with open('tmp_email.html', 'w') as file:
file.write(email_body)
send_email(subject=email_subject, body=email_body, to_email=user_obj.email)
return ({'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email})
@app.task()
def send_all_weekly_emails():
print("Scheduling weekly emails...")
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_lst = CustomUser.objects.filter(email_mid_week=True).order_by('pk')
task_log = []
if len(user_lst) > 0:
eta_steps = max(min((60 * 60) // len(user_lst), 60), 10)
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10)
for user_obj in user_lst:
result = weekly_email.apply_async(args=[user_obj.pk], eta=eta)
task_log.append({'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email, 'task_id': result.task_id, 'eta': eta.isoformat()})
eta += datetime.timedelta(seconds=eta_steps)
return task_log
def openai_quote():
if settings.OPENAI_API_KEY is None:
return None
todays_ai_quote = cache.get('todays_ai_quote', None)
if todays_ai_quote is None:
client = OpenAI(api_key=settings.OPENAI_API_KEY)
options = ["fitness", "health", "nutritional", "workout"]
selection = random.choice(options)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": f"Tell me a one sentence {selection} fact."},
],
temperature=1.0,
top_p=1.0
)
todays_ai_quote = response.choices[0].message.content
cache.set('todays_ai_quote', todays_ai_quote, 60 * 60 * 20)
print('Todays AI Quote:', todays_ai_quote)
return todays_ai_quote
def calendar_stats(user_pk):
today = datetime.date.today()
# Step 1: Find next Sunday (or today if Sunday)
days_until_sunday = (6 - today.weekday()) % 7 # weekday(): Monday=0, Sunday=6
next_sunday = today + datetime.timedelta(days=days_until_sunday)
# Step 2: Create list of days going back 5 weeks (inclusive)
dates_list = [next_sunday - datetime.timedelta(days=i) for i in range(34, -1, -1)]
Workout = apps.get_model('workouts', 'Workout')
all_workouts = Workout.objects.filter(user=user_pk).annotate(date=TruncDay('start_datetime')).values('date').annotate(count=Count('id'))
workouts_by_date = {row['date'].date().isoformat(): row['count'] for row in all_workouts}
workouts_by_week = {(settings.TIME_ZONE_OBJ.localize(datetime.datetime.combine(next_sunday, datetime.datetime.min.time())) - row['date']).days // 7: True for row in all_workouts}
streak_weeks = 0
streak_i = 0
streak_true = True
while streak_true:
if workouts_by_week.get(streak_i, False):
streak_weeks += 1
elif streak_i == 0:
pass
else:
streak_true = False
streak_i += 1
return_calendar = []
for date in dates_list:
workout_num = workouts_by_date.get(date.isoformat(), 0)
return_calendar.append({
'datetime': date,
'day': date.day,
'workout_num': workout_num,
'color': '#FFFFFF' if date == today or workout_num > 0 else ('#e5e5e5' if date > today else '#000000'),
'background_color': '#7F1D1D' if date == today else ('#075971' if workout_num > 0 else '#FFFFFF')
})
return streak_weeks, [return_calendar[i:i+7] for i in range(0, len(return_calendar), 7)]
@app.task()
def weekly_email(user_pk):
"""Email to send users their weekly update."""
CustomUser = apps.get_model('custom_user', 'CustomUser')
user_obj = CustomUser.objects.get(pk=user_pk)
Workout = apps.get_model('workouts', 'Workout')
workout_7day_stats = Workout.objects.filter(
user=user_obj,
start_datetime__gte=datetime.date.today() - datetime.timedelta(days=7)
).annotate(
day=TruncDate('start_datetime')
).aggregate(
total_duration=Sum('duration'),
total_distance=Sum('distance'),
distinct_days=Count('day', distinct=True)
)
week_streak, calendar = calendar_stats(user_pk)
todays_ai_quote = openai_quote()
email_subject = 'Workout Challenge - Your Weekly Update!'
recorded_total_duration = 0 if workout_7day_stats["total_duration"] is None else (workout_7day_stats["total_duration"].seconds // 60)
recorded_total_distance = 0 if workout_7day_stats["total_distance"] is None else workout_7day_stats["total_distance"]
recorded_distinct_days = 0 if workout_7day_stats["distinct_days"] is None else workout_7day_stats["distinct_days"]
email_body = render_to_string(
"email_weekly.html",
{
'first_name': user_obj.first_name,
'MAIN_HOST': settings.MAIN_HOST,
'calendar': calendar,
'week_streak': week_streak,
'goals': {
'active_days': None if user_obj.goal_active_days is None or user_obj.goal_active_days == '' else {'recorded': recorded_distinct_days,'target': user_obj.goal_active_days, 'percent': min(1, recorded_distinct_days / user_obj.goal_active_days) * 100, 'percent_vml': int(min(1, recorded_distinct_days / user_obj.goal_active_days) * 100 * 2.5)},
'distance': None if user_obj.goal_distance is None or user_obj.goal_distance == '' else {'recorded': recorded_total_distance,'target': user_obj.goal_distance, 'percent': min(1, recorded_total_distance / user_obj.goal_distance) * 100, 'percent_vml': int(min(1, recorded_total_distance / user_obj.goal_distance) * 100 * 2.5)},
'minutes': None if user_obj.goal_workout_minutes is None or user_obj.goal_workout_minutes == '' else {'recorded': recorded_total_duration,'target': user_obj.goal_workout_minutes, 'percent': min(1, recorded_total_duration / user_obj.goal_workout_minutes) * 100, 'percent_vml': int(min(1, recorded_total_duration / user_obj.goal_workout_minutes) * 100 * 2.5)},
},
'openai_quote': todays_ai_quote,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO[0] if settings.EMAIL_REPLY_TO is not None else settings.EMAIL_FROM,
}
)
if settings.DEBUG:
with open('tmp_email.html', 'w') as file:
file.write(email_body)
send_email(subject=email_subject, body=email_body, to_email=user_obj.email)
return {'pk': user_obj.pk, 'username': user_obj.username, 'email': user_obj.email}

View file

@ -0,0 +1,23 @@
import os
from django.conf import settings
from django.core.mail import get_connection
from django.core.mail.message import EmailMultiAlternatives
def send_email(subject, body, to_email, cc=[], reply_to=[]):
"""General function via which all emails are sent out"""
to_email = [settings.EMAIL_FROM] if (settings.DEBUG or '.local' in to_email.lower()) else [to_email]
from_email = settings.EMAIL_FROM
reply_to_email = ([from_email] if settings.EMAIL_REPLY_TO is None else settings.EMAIL_REPLY_TO) if reply_to == [] else reply_to
print(f'Email Server: {settings.EMAIL_HOST}')
connection = get_connection()
mail = EmailMultiAlternatives(
subject=subject, body="", from_email=from_email, to=to_email, cc=cc, reply_to=reply_to_email, connection=connection
)
mail.attach_alternative(body, "text/html")
mail.content_subtype = "html"
mail.send()
print(f'Email "{subject}" sent to {to_email}')

View file

@ -0,0 +1,15 @@
# filters.py
import django_filters
from .models import CustomUser
class CustomUserFilter(django_filters.FilterSet):
my = django_filters.CharFilter(method='filter_my')
def filter_my(request, queryset, *args, **kwargs):
return queryset.filter(id=request.request.user.id)
class Meta:
model = CustomUser
fields = {
'username': ['exact', 'icontains'],
}

View file

@ -0,0 +1,232 @@
from django.core.management import BaseCommand
import datetime, random
from datetime import timedelta
from competition.models import Competition, ActivityGoal, Team, Award, Points
from workouts.models import Workout
from custom_user.models import CustomUser
class Command(BaseCommand):
"""Add additional test data"""
# Show this when the user types help
help = "Adds test data"
def handle(self, *args, **options):
"""Actual Commandline executed function when manage.py command is called"""
test_users = [
{
"email": "user1@admin.local",
"password": "password",
"first_name": "Charlotte",
"last_name": "Doe",
"is_superuser": True,
"is_staff": True,
"strava_athlete_id": 123456789,
},
{
"email": "user2@admin.local",
"password": "password",
"first_name": "Tom",
"last_name": "Smith-Bloggs",
"is_superuser": True,
"is_staff": True,
},
{
"email": "user3@admin.local",
"password": "password",
"first_name": "User",
"last_name": "von der Leyen",
"is_superuser": False,
"is_staff": False,
},
]
user_obj_dict = {}
for user_i in test_users:
user_obj = CustomUser.objects.create_user(**user_i)
user_obj.set_password(user_i["password"])
user_obj.save()
user_obj_dict[user_i["email"]] = user_obj
test_competitions = [
{
"owner": CustomUser.objects.get(email="user1@admin.local"),
"name": "WHO Competition",
"join_code": "WHOComp",
"start_date": "2025-05-01",
"end_date": "2025-12-31",
"has_teams": True,
"teams": [
{
"name": "Team 1",
"members": [
"user1@admin.local",
"user2@admin.local",
]
}
],
"goals": [
{
"name": "Move",
"metric": "kcal",
"period": "week",
"goal": 600,
"max_per_day": 1_200,
"max_per_week": 6_000, # 5x the daily limit
},
{
"name": "Exercise",
"metric": "min",
"period": "day",
"goal": 30,
"max_per_day": 60,
"max_per_week": 300, # 5x the daily limit
},
],
"awards": [
{
"name": "10 Workouts (Bronze)",
"sport": "GROUP_ANY",
"threshold": 10,
"period": "end",
"reward_points": 250,
},
{
"name": "25 Workouts (Silver)",
"sport": "GROUP_ANY",
"threshold": 25,
"period": "end",
"reward_points": 500,
},
{
"name": "50 Workouts (Gold)",
"sport": "GROUP_ANY",
"threshold": 25,
"period": "end",
"reward_points": 1_000,
},
]
},
{
"owner": CustomUser.objects.get(email="user2@admin.local"),
"name": "100k 1k Competition",
"join_code": "100k1k",
"start_date": "2025-01-01",
"end_date": "2025-12-31",
"teams": [
{
"name": "Team 1",
"members": [
"user3@admin.local",
"user2@admin.local",
]
}
],
"goals": [
{
"name": "Distance",
"metric": "km",
"period": "competition",
"goal": 1_000,
"min_per_workout": 5,
},
{
"name": "Effort",
"metric": "kj",
"period": "month",
"goal": 1_000,
},
{
"name": "Workouts",
"metric": "num",
"period": "week",
"goal": 3,
"max_per_day": 2,
"max_per_week": 6,
},
]
}
]
for competitions_i in test_competitions:
goals = competitions_i.pop("goals", [])
awards = competitions_i.pop("awards", [])
teams = competitions_i.pop("teams", [])
competitions_obj = Competition(**competitions_i)
competitions_obj.save()
for goal_i in goals:
goal_obj = ActivityGoal(competition=competitions_obj, **goal_i)
goal_obj.save()
for award_i in awards:
award_obj = Award(competition=competitions_obj, **award_i)
award_obj.save()
for team_i in teams:
team_i_name = team_i.pop("name")
team_i_members = team_i.pop("members", [])
team_obj = Team(competition=competitions_obj, name=team_i_name)
team_obj.save()
for user_i in team_i_members:
user_i_obj = CustomUser.objects.get(email=user_i)
user_i_obj.my_teams.add(team_obj)
user_i_obj.my_competitions.add(competitions_obj)
user_i_obj.save()
test_workouts = []
today = datetime.datetime.now()
curr_datetime = today - datetime.timedelta(days=30*6) # 6 months ago
while curr_datetime < today:
today_entries = random.randint(0, len(test_users))
user_entries = random.sample(test_users, k=today_entries)
for user_i in user_entries:
duration_i = random.randint(15, 75)
sport_i = random.sample(['Run', 'Tennis', 'WeightTraining', 'Workout'], k=1)[0]
workout_i = {
'user': user_i['email'],
'sport_type': sport_i,
'start_datetime': curr_datetime,
'duration': timedelta(minutes=duration_i),
'intensity_category': random.sample([1, 2, 3, 4], k=1)[0],
'kcal': random.uniform(0.6, 1.3) * 10 * duration_i,
'distance': duration_i / random.uniform(5, 8) if sport_i == 'Run' else None,
'strava_id': random.randint(0, 999999999) if random.sample([True, False], k=1)[0] else None,
}
#points_i = []
#user_i_obj = CustomUser.objects.get(email=user_i['email'])
#for competitions_i in user_i_obj.my_competitions.all():
# if (curr_datetime.date() >= competitions_i.start_date) & (curr_datetime.date() <= competitions_i.end_date):
# for goal_i in competitions_i.activitygoal_set.all():
# points = duration_i * random.uniform(0.75, 2.5) / 25
# points_i.append({
# "goal": goal_i,
# "points_raw": points,
# "points_capped": points,
# })
test_workouts.append({**workout_i}) #, 'points': points_i})
curr_datetime += timedelta(minutes=random.uniform(2, 4) * 60 * 24)
for workout_i in test_workouts:
#points = workout_i.pop("points", [])
user_email = workout_i.pop("user", None)
user = CustomUser.objects.get(email=user_email)
workout_obj = Workout(user=user, **workout_i)
workout_obj.save()
#for point_i in points:
# point_obj = Points(workout=workout_obj, **point_i)
# point_obj.save()

View file

@ -0,0 +1,27 @@
from django.core.management import BaseCommand
from django.contrib.auth import get_user_model
class Command(BaseCommand):
"""Add additional test data"""
# Show this when the user types help
help = "Give user staff status"
def add_arguments(self, parser):
parser.add_argument("email", nargs="?", type=str, help="Email of the user to promote to staff")
def handle(self, *args, **options):
"""Actual Commandline executed function when manage.py command is called"""
User = get_user_model()
email = options.get("email")
if not email:
email = input("Enter the email of the user to promote to staff: ")
try:
user = User.objects.get(email=email)
user.is_staff = True
user.save()
self.stdout.write(self.style.SUCCESS(f"User {email} is now staff."))
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"No user found with email {email}"))

View file

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from celery import current_app
class Command(BaseCommand):
help = "Run a Celery task synchronously (default) or asynchronously (--async)."
def add_arguments(self, parser):
parser.add_argument("task_name", help="Name of the Celery task")
parser.add_argument("task_args", nargs="*", help="Task args and kwargs (key=value)")
parser.add_argument(
"--async", action="store_true", dest="async_mode",
help="Run task asynchronously via Celery worker"
)
def handle(self, *args, **options):
task_name = options["task_name"]
task = current_app.tasks.get(task_name)
if not task:
self.stderr.write(self.style.ERROR(f"Task '{task_name}' not found"))
return
# Parse args/kwargs
positional, keyword = [], {}
for arg in options["task_args"]:
if "=" in arg:
k, v = arg.split("=", 1)
keyword[k] = v
else:
positional.append(arg)
if options["async_mode"]:
result = task.delay(*positional, **keyword)
self.stdout.write(self.style.SUCCESS(
f"Task {task_name} dispatched asynchronously with id {result.id}"
))
else:
result = task.apply(args=positional, kwargs=keyword)
self.stdout.write(self.style.SUCCESS(
f"Task {task_name} finished synchronously with result: {result.get()}"
))

View file

@ -0,0 +1,215 @@
import requests
import qrcode, datetime
from decimal import Decimal
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from django.conf import settings
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from competition.scorer import trigger_user_change
from custom_user.emails.celery_emails import welcome_email
# Create your models here.
GENDER_CHOICES = [
('M', 'Male'),
('F', 'Female'),
('O', 'Other'),
]
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifiers
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given email and password.
"""
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
"""
Create and save a SuperUser with the given email and password.
"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
# extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)
class CustomUser(AbstractBaseUser, PermissionsMixin):
"""Custom User model - needed to use email as login and a few more additional fields"""
email = models.EmailField(_("email address"), unique=True)
first_name = models.CharField(max_length=30, null=False, blank=False)
last_name = models.CharField(max_length=40, null=True, blank=True)
gender = models.CharField(max_length=1, null=True, blank=True, choices=GENDER_CHOICES)
username = models.CharField(max_length=40, null=True, blank=True)
my_competitions = models.ManyToManyField('competition.Competition', blank=True, related_name='user')
my_teams = models.ManyToManyField('competition.Team', blank=True, related_name='user')
# personal 7 day goals
goal_active_days = models.IntegerField(null=True, blank=True, default=3)
goal_workout_minutes = models.IntegerField(null=True, blank=True, default=150)
goal_distance = models.IntegerField(null=True, blank=True, default=None)
# personal scaling factors
scaling_kcal = models.DecimalField(null=False, blank=False, default=1, max_digits=8, decimal_places=4, validators=[
MinValueValidator(Decimal('0.6666')),
MaxValueValidator(Decimal('1.3333'))
]
)
scaling_distance = models.DecimalField(null=False, blank=False, default=1, max_digits=8, decimal_places=4, validators=[
MinValueValidator(Decimal('0.6666')),
MaxValueValidator(Decimal('1.3333'))
]
)
# has_paid = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
email_mid_week = models.BooleanField(default=False)
strava_athlete_id = models.IntegerField(null=True, blank=True)
strava_allow_follow = models.BooleanField(default=True)
strava_refresh_token = models.CharField(max_length=40, null=True, blank=True)
strava_last_synced_at = models.DateTimeField(null=True, blank=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["first_name", "last_name"]
objects = CustomUserManager()
class Meta:
verbose_name = "User"
verbose_name_plural = "Users"
def __str__(self):
return self.email
def __init__(self, *args, **kwargs):
""" save initial field values to be able to detect changes """
super().__init__(*args, **kwargs)
self._original = self._dict()
#@property
def _dict(self):
""" dict of current fields and values - to detect changes """
return {f.name: round(float(self.__dict__[f.attname]), 2) if isinstance(self.__dict__.get(f.attname), (Decimal, float)) else self.__dict__.get(f.attname) for f in self._meta.fields}
def get_changed_fields(self):
""" check which fields have changed """
current = self._dict()
return {
k: (v, current.get(k))
for k, v in self._original.items()
if v != current.get(k)
}
def save(self, *args, **kwargs):
""" trigger recalculation of points_capped if workout changes """
if self.username is None or self.username == "":
if self.first_name is None or self.first_name == "":
self.username = self.email.split("@")[0]
elif self.last_name is None or self.last_name == "":
self.username = self.first_name
else:
self.username = f'{self.first_name} {".".join([i[0] for i in self.last_name.replace("-"," ").split(" ") if len(i) >= 1])}.'
is_create = self.pk is None
super().save(*args, **kwargs)
if is_create:
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=60 * 5)
welcome_email.apply_async(args=[self.pk], eta=eta)
changed = self.get_changed_fields()
trigger_user_change(
instance=self,
new=is_create,
changes=changed
)
self._original = self._dict() # reset
@receiver(m2m_changed, sender=CustomUser.my_competitions.through)
def my_competitions_changed_handler(sender, instance, action, pk_set, **kwargs):
if 'post' in action:
if isinstance(instance, CustomUser):
if 'add' in action:
# instance user obj / pk_set comp id to add
trigger_user_change(instance=instance, new=False, changes={'my_competitions': (None, list(pk_set))})
elif 'remove' in action or 'clear' in action:
# instance user obj / pk_set comp id to remove
trigger_user_change(instance=instance, new=False, changes={'my_competitions': (list(pk_set), None)})
else: # is instance of Competition
for user_id in list(pk_set):
user_obj = CustomUser.objects.get(pk=user_id)
if 'add' in action:
# instance competition obj / pk_set user id to add
trigger_user_change(instance=user_obj, new=False, changes={'my_competitions': (None, [instance.pk])})
elif 'remove' in action or 'clear' in action:
# instance competition obj / pk_set user id to remove
trigger_user_change(instance=user_obj, new=False, changes={'my_competitions': ([instance.pk], None)})
def get_strava_auth_url(user_id):
""" Generate the initial auth url the user clicks, which will re-direct back to this page providing the code."""
client_id = settings.STRAVA_CLIENT_ID
redirect_url = f"{settings.MAIN_HOST}/strava/return/?user_id={user_id}"
return f"https://www.strava.com/oauth/authorize?client_id={client_id}&response_type=code&approval_prompt=force&scope=profile:read_all,activity:read_all&redirect_uri={redirect_url}"
def make_url_qr_code(url, path):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(path)
class RecalcRequest(models.Model):
""" Recalc Request model to track which point caps need to be updated """
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=False, blank=False)
goal = models.ForeignKey('competition.ActivityGoal', on_delete=models.CASCADE, null=False, blank=False)
start_datetime = models.DateTimeField(null=False, blank=False)
done = models.BooleanField(default=False, null=False, blank=False)
def __str__(self):
return f'{self.goal} - {self.start_datetime}'

View file

@ -0,0 +1,177 @@
import datetime
from django.db.models import Min
from django.core.cache import cache
from workout_challenge.celery import app, is_task_already_executing
from django.apps import apps
from django.contrib.auth import get_user_model
def trigger_recalc_points():
last_recalc = cache.get('last_recalc_points', None)
if last_recalc is None or last_recalc < datetime.datetime.now() - datetime.timedelta(seconds=30):
cache.set('last_recalc_points', datetime.datetime.now(), 60 * 10)
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10)
recalc_points.apply_async(eta=eta)
else:
print('Recalc points task skipped because it was triggered less than 30 seconds ago')
@app.task(bind=True, time_limit=60 * 30, max_retries=3) # 30 min time limit
def recalc_points(self):
if is_task_already_executing('recalc_points'):
print('Recalc points task skipped because it is already running')
return 'Skipped because it is already running.'
print('Recalculating points...')
ActivityGoal = apps.get_model('competition', 'ActivityGoal')
Points = apps.get_model('competition', 'Points')
RecalcRequest = apps.get_model('custom_user', 'RecalcRequest')
all_tasks = RecalcRequest.objects.filter(done=False)
grouped_tasks = all_tasks.values('user', 'goal').annotate(start_datetime=Min('start_datetime'))
for task_group in grouped_tasks:
points_lst = Points.objects.filter(goal=task_group['goal'], workout__user=task_group['user'], workout__start_datetime__gte=task_group['start_datetime']).order_by('workout__start_datetime')
goal = ActivityGoal.objects.get(pk=task_group['goal'])
scorer = Scorer()
scorer.set_goal(goal)
for points in points_lst:
earned_points = scorer.calculate_points(points)
setattr(points, 'points_capped', earned_points)
points.save()
all_tasks.delete()
print('All points recalculated.')
return [{k: str(v) for k, v in i.items()} for i in grouped_tasks]
class Scorer:
def __init__(self):
self.memory_today = None
self.memory_today_points_raw = 0
self.memory_today_points_capped = 0
self.memory_this_week = None
self.memory_week_points_raw = 0
self.memory_week_points_capped = 0
def set_goal(self, goal):
self.goal = goal
self.floor_workout = 0 if goal.min_per_workout is None else goal.min_per_workout / goal.goal * 100
self.cap_workout = None if goal.max_per_workout is None else goal.max_per_workout / goal.goal * 100
self.floor_day = 0 if goal.min_per_day is None else goal.min_per_day / goal.goal * 100
self.cap_day = None if goal.max_per_day is None else goal.max_per_day / goal.goal * 100
self.floor_week = 0 if goal.min_per_week is None else goal.min_per_week / goal.goal * 100
self.cap_week = None if goal.max_per_week is None else goal.max_per_week / goal.goal * 100
def calculate_points(self, points):
# potentially reset the memory if new day / week
if points.workout.start_datetime.date() != self.memory_today:
self.memory_today = points.workout.start_datetime.date()
self.memory_today_points_raw = 0
self.memory_today_points_capped = 0
if points.workout.start_datetime.isocalendar()[1] != self.memory_this_week:
self.memory_this_week = points.workout.start_datetime.isocalendar()[1]
self.memory_week_points_raw = 0
self.memory_week_points_capped = 0
earned_points = points.points_raw
self.memory_today_points_raw += earned_points
self.memory_week_points_raw += earned_points
earned_points = points.points_raw
# workout floor
earned_points = max(earned_points - self.floor_workout, 0)
# workout cap
if self.cap_workout is not None:
adjusted_cap = self.cap_workout - self.floor_workout
earned_points = min(earned_points, adjusted_cap)
# day floor
earned_points = max(min(earned_points, self.memory_today_points_raw - self.floor_day), 0)
# day cap
if self.cap_day is not None:
max_points_to_earn_today = self.cap_day - self.floor_day
earned_points = max(min(earned_points, max_points_to_earn_today - self.memory_today_points_capped),0)
# week floor
earned_points = max(min(earned_points, self.memory_week_points_raw - self.floor_week), 0)
# week cap
if self.cap_week is not None:
max_points_to_earn_week = self.cap_week - self.floor_week
earned_points = max(min(earned_points, max_points_to_earn_week - self.memory_week_points_capped),0)
self.memory_today_points_capped += earned_points
self.memory_week_points_capped += earned_points
return earned_points
class DummyObject:
def __init__(self, **kwargs):
self.min_per_workout = None
self.max_per_workout = None
self.min_per_day = None
self.max_per_day = None
self.min_per_week = None
self.max_per_week = None
for key, value in kwargs.items():
setattr(self, key, value)
def test_scorer():
for goal_kwargs, points, expected_result in (
({'goal': 100, 'min_per_workout': 10}, [10], 0),
({'goal': 100, 'min_per_workout': 10}, [20], 10),
({'goal': 100, 'max_per_workout': 30}, [30], 30),
({'goal': 100, 'max_per_workout': 30}, [40], 30),
({'goal': 100, 'min_per_workout': 10, 'max_per_workout': 30}, [40], 20),
({'goal': 100, 'min_per_day': 10}, [10], 0),
({'goal': 100, 'min_per_day': 10}, [20], 10),
({'goal': 100, 'min_per_day': 10}, [8, 8], 6),
({'goal': 100, 'max_per_day': 30}, [20], 20),
({'goal': 100, 'max_per_day': 30}, [20, 20], 30),
({'goal': 100, 'min_per_day': 10, 'max_per_day': 30}, [8, 12, 8, 8, 14], 20),
({'goal': 100, 'min_per_week': 10}, [10], 0),
({'goal': 100, 'min_per_week': 10}, [20], 10),
({'goal': 100, 'max_per_week': 30}, [20], 20),
({'goal': 100, 'max_per_week': 30}, [20, 20], 30),
({'goal': 100, 'min_per_week': 10, 'max_per_week': 30}, [8, 12, 8, 8, 14], 20),
({'goal': 100, 'min_per_workout': 10, 'min_per_day': 20}, [5, 20, 5, 20], 15),
({'goal': 100, 'min_per_workout': 20, 'min_per_day': 10}, [5, 30, 30], 20),
({'goal': 100, 'max_per_workout': 20, 'max_per_day': 30}, [20, 25, 25, 25], 30),
({'goal': 100, 'max_per_workout': 30, 'max_per_day': 20}, [20, 25, 25, 25], 20),
({'goal': 100, 'min_per_workout': 10, 'max_per_day': 15}, [5, 5, 5, 5], 0),
({'goal': 100, 'min_per_workout': 10, 'max_per_day': 30}, [15, 35, 5, 15], 30),
):
workout = DummyObject(start_datetime=datetime.datetime.fromisoformat('2023-01-01T00:00:00'))
goal = DummyObject(**goal_kwargs)
scorer = Scorer()
scorer.set_goal(goal)
earned_points = 0
for point in points:
point_obj = DummyObject(points_raw=point, workout = workout)
earned_points += scorer.calculate_points(point_obj)
assert earned_points == expected_result, f'Expected {expected_result}, got {earned_points} for goal {goal_kwargs}'
if __name__ == '__main__':
test_scorer()

View file

@ -0,0 +1,118 @@
from rest_framework import serializers
from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.template.loader import render_to_string
from .models import CustomUser
from .emails.multipurpose import send_email
class CustomUserSerializer(serializers.ModelSerializer):
my = serializers.SerializerMethodField()
class Meta:
model = CustomUser
fields = ['id', 'my', 'email', 'first_name', 'last_name', 'gender', 'username', 'password', 'is_verified', 'email_mid_week', 'strava_athlete_id', 'strava_allow_follow', 'strava_last_synced_at', 'my_competitions', 'my_teams', 'goal_active_days', 'goal_workout_minutes', 'goal_distance', 'scaling_kcal', 'scaling_distance']
read_only_fields = ['is_verified', 'strava_athlete_id', 'strava_last_synced_at']
extra_kwargs = {
'password': {'write_only': True},
}
def get_my(self, obj):
user = self.context['request'].user
return obj.pk == user.pk
def create(self, validated_data):
user = CustomUser.objects.create_user(
email=validated_data.get('email'),
first_name=validated_data.get('first_name'),
last_name=validated_data.get('last_name', None),
password=validated_data.get('password'),
gender=validated_data.get('gender', None),
)
return user
def to_representation(self, instance):
rep = super().to_representation(instance)
user = self.context['request'].user
# Omit 'secret' fields of other users that this user is not allowed to see
if instance.pk != user.pk:
rep.pop('email', None)
rep.pop('first_name', None)
rep.pop('last_name', None)
rep.pop('gender', None)
rep.pop('password', None)
rep.pop('strava_last_synced_at', None)
if not rep['strava_allow_follow']:
rep.pop('strava_athlete_id', None)
return rep
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# If instance exists, it's an update (PUT/PATCH), make fields optional
if self.instance:
self.fields['email'].required = False
self.fields['password'].required = False
self.fields['first_name'].required = False
self.fields['last_name'].required = False
class PasswordResetSerializer(serializers.Serializer):
email = serializers.EmailField()
def validate_email(self, value):
if not CustomUser.objects.filter(email=value).exists():
# To avoid leaking info
return value
return value
def save(self, request):
email = self.validated_data['email']
users = CustomUser.objects.filter(email=email)
for user in users:
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = default_token_generator.make_token(user)
reset_url = f"{settings.MAIN_HOST}/password/reset/{uid}/{token}/"
email_subject = "Workout Challenge - Reset Your Password"
email_body = render_to_string(
"email_password_reset.html",
{
'first_name': user.first_name,
'MAIN_HOST': settings.MAIN_HOST,
'RESET_URL': reset_url,
'EMAIL_REPLY_TO': settings.EMAIL_REPLY_TO,
}
)
send_email(subject=email_subject, body=email_body, to_email=user.email)
class PasswordResetConfirmSerializer(serializers.Serializer):
uid = serializers.CharField()
token = serializers.CharField()
new_password = serializers.CharField(write_only=True)
def validate(self, attrs):
try:
uid = urlsafe_base64_decode(attrs['uid']).decode()
self.user = CustomUser.objects.get(pk=uid)
except (CustomUser.DoesNotExist, ValueError):
raise serializers.ValidationError("Invalid user.")
if not default_token_generator.check_token(self.user, attrs['token']):
raise serializers.ValidationError("Invalid or expired token.")
return attrs
def save(self):
self.user.set_password(self.validated_data['new_password'])
self.user.save()

View file

@ -0,0 +1,194 @@
import requests
import time, datetime
from django.core.cache import cache
from django.contrib.auth import get_user_model
from django.conf import settings
from django.utils import timezone
from rest_framework.response import Response
from rest_framework import status
from django.db import IntegrityError
from workout_challenge.celery import app, is_task_already_executing
from django.db.models import Q
from workouts.models import Workout
from .api_rate_limiter import strava_api_monitor, RateLimitExceeded # Import to trigger initialization
def _seconds_until_next_interval():
current_time = time.localtime()
minutes = current_time.tm_min
seconds = current_time.tm_sec
# Calculate the next interval (15, 30, 45, or 0 of the next hour)
next_interval = (minutes // 15 + 1) * 15
if next_interval == 60: # Handle the case where it rolls over to the next hour
next_interval = 0
# Calculate the seconds until the next interval
seconds_until = (next_interval - minutes) * 60 - seconds
if next_interval == 0: # Adjust for the next hour
seconds_until += 3600
return seconds_until
@app.task(bind=True, time_limit=60 * 60 * 3, max_retries=10) # 3 hour time limit
def daily_strava_sync(self, refresh_all=False):
if is_task_already_executing('daily_strava_sync'):
return 'Task already executing. Skipping.'
CustomUser = get_user_model()
user_lst = CustomUser.objects.filter(
strava_refresh_token__isnull=False,
is_active=True
)
if refresh_all is False:
user_lst = user_lst.filter(
Q(strava_last_synced_at__lt=timezone.now() - datetime.timedelta(hours=6)) |
Q(strava_last_synced_at__isnull=True)
)
user_lst = user_lst.order_by('strava_last_synced_at', 'pk')
user_lst_names = [{'pk': i.pk, 'username': i.username, 'email': i.email} for i in user_lst]
print(f'Syncing Strava for {len(user_lst)} users: {user_lst_names}')
for user in user_lst:
try:
sync_strava(user__id=user.id)
except RateLimitExceeded as exc:
sleep_time = _seconds_until_next_interval() + 60
print(f'Strava sync rate limit exceeded - sleeping for {sleep_time // 60 } mins')
raise self.retry(exc=exc, countdown=sleep_time) # retry in next Strava 15min api period
except Exception as exc:
print(f'Strava sync failed for user {user.email} - {exc}')
print('Finished syncing Strava.')
return user_lst_names
@app.task(bind=True)
def sync_strava(self, user__id, start_datetime=None):
access_token = cache.get(f"strava_access_token_{user__id}")
CustomUser = get_user_model()
user = CustomUser.objects.get(id=user__id)
all_existing_strava_activities = set(Workout.objects.all().values_list('strava_id', flat=True))
cnt_new_strava_activities = 0
cnt_updated_strava_activities = 0
if strava_api_monitor.ok_workout_requests() is False:
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
# refresh access token if expired
if access_token is None:
refresh_token = user.strava_refresh_token
client_id = settings.STRAVA_CLIENT_ID
client_secret = settings.STRAVA_CLIENT_SECRET
response = requests.post(
url='https://www.strava.com/oauth/token',
data={
'client_id': client_id,
'client_secret': client_secret,
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
}
)
strava_api_monitor.log_request(response)
response.raise_for_status()
strava_tokens = response.json()
access_token = strava_tokens.get('access_token', None)
cache.set(f"strava_access_token_{user__id}", access_token, int(strava_tokens.get('expires_in', 21600)) - 60)
# get activities
page = 1
per_page = 200
while True:
if strava_api_monitor.ok_workout_requests() is False:
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
response = requests.get(
url='https://www.strava.com/api/v3/athlete/activities',
headers={
'Authorization': f'Bearer {access_token}',
},
params={
'after': None if start_datetime is None else int(start_datetime.timestamp()),
'page': page,
'per_page': per_page,
}
)
strava_api_monitor.log_request(response)
response.raise_for_status()
activities = response.json()
for activity in activities:
activity_id = activity.get('id')
props = {
'user': user,
'strava_id': activity_id,
'sport_type': activity.get('sport_type'),
'start_datetime': datetime.datetime.fromisoformat(activity.get('start_date')),
'duration': datetime.timedelta(seconds=activity.get('moving_time')),
'distance': None if activity.get('distance') == 0 else activity.get('distance') / 1_000,
}
# if existing workout - update activity details
if activity_id in all_existing_strava_activities:
workout = Workout.objects.get(strava_id=activity_id)
for key, value in props.items():
setattr(workout, key, value)
workout.save()
cnt_updated_strava_activities += 1
# if a new workout - get activity details
else:
if strava_api_monitor.ok_workout_requests() is False:
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
response = requests.get(
url=f'https://www.strava.com/api/v3/activities/{activity_id}',
headers={
'Authorization': f'Bearer {access_token}',
},
)
strava_api_monitor.log_request(response)
response.raise_for_status()
activity_details = response.json()
avg_heart_rate = activity_details.get('average_heartrate', 0)
props['kcal'] = kcal = activity_details.get('calories', activity_details.get('kilojoules', 0) / 4.18)
props['strava_intensity_avg_watts'] = avg_watt = activity_details.get('average_watts', 0)
# estimate intensity
max_heart_rate = 180
kcal_per_ten_minute = kcal / (max(activity.get('moving_time', 60 * 30), 60) / (60 * 10))
if avg_heart_rate > max_heart_rate * 0.85 or kcal_per_ten_minute > 120 or avg_watt > 300:
props['intensity_category'] = 4
elif avg_heart_rate > max_heart_rate * 0.70 or kcal_per_ten_minute > 90 or avg_watt > 275:
props['intensity_category'] = 3
elif avg_heart_rate > max_heart_rate * 0.60 or kcal_per_ten_minute > 75 or avg_watt > 225:
props['intensity_category'] = 2
else:
props['intensity_category'] = 1
Workout.objects.create(**props)
cnt_new_strava_activities += 1
if len(activities) < per_page:
break
page += 1
strava_last_synced_at = timezone.now()
if start_datetime is None:
setattr(user, 'strava_last_synced_at', strava_last_synced_at)
user.save()
print(f'User {user__id} - fetched {cnt_new_strava_activities} new strava activities and updated {cnt_updated_strava_activities} existing strava activities')
return {'user': user__id, 'total_activities': (page - 1) * per_page + len(activities), 'new_activities': cnt_new_strava_activities, 'updated_activities': cnt_updated_strava_activities, 'sync_time': strava_last_synced_at}

View file

@ -0,0 +1,409 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> The "{{ competition.name }}" competition kicks off tomorrow! Give it a strong start, build momentum your best is waiting! But first, here's how the goals work and how to earn points. </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="200">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style="">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:45px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:1px solid #fff;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>The "{{ competition.name }}" competition kicks off tomorrow!</p>
<p>Give it a strong start, build momentum your best is waiting!</p>
<p>But first, here's how the goals work and how to earn points.</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-top:5px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-top:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:10px;padding-bottom:15px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>How to earn activity points?</h3>
<ul>
<li>Each competition has its own activity goals.</li>
<li>You earn <b>1 point for every 1%</b> progress toward a goal.</li>
<li>For example, if the goal is 100 minutes of exercise and you work out 50 minutes, you earn 50 points.</li>
<li><i>Note:</i> Some goals have <b>minimum or maximum limits</b>. Activities above/below these limits wont earn you points and are marked with an asterisk (*).</li>
<li>Hover over a goal to view its limits, or over the asterisk for more details.</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Activity Goals:</h3>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;"> {% for goal in goals %} <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:10px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">{{ goal.name }}</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goal.goal|floatformat:0 }} {{ goal.metric }} <span style="font-size:10px;">/ {{ goal.period }}</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:250%;height:12px;" fillcolor="#e5e5e5"><v:fill color="#e5e5e5" /></v:roundrect><![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;"></div>
<!--<![endif]-->
</td>
</tr> {% if goal.min_per_workout > 0 or goal.max_per_workout > 0 or goal.min_per_day > 0 or goal.max_per_day > 0 or goal.min_per_week > 0 or goal.max_per_week > 0 %} <tr>
<td style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#6b7280;padding-top: 7px;" colspan="2"><b>Limits:</b>
<ul style="margin: 0px;"> {% if goal.min_per_workout > 0 %}<li>min <b>{{ goal.min_per_workout|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ workout</span></li>{% endif %} {% if goal.max_per_workout > 0 %}<li>max <b>{{ goal.max_per_workout|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ workout</span></li>{% endif %} {% if goal.min_per_day > 0 %}<li>min <b>{{ goal.min_per_day|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ day</span></li>{% endif %} {% if goal.max_per_day > 0 %}<li>max <b>{{ goal.max_per_day|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ day</span></li>{% endif %} {% if goal.min_per_week > 0 %}<li>min <b>{{ goal.min_per_week|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ week</span></li>{% endif %} {% if goal.max_per_week > 0 %}<li>max <b>{{ goal.max_per_week|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ week</span></li>{% endif %} </ul>
</td>
</tr> {% endif %}
</table>
</td>
</tr>
</table> {% endfor %} </td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% if goal_equalizer_note %}
<!-- Goal Equalizer -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#075985" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#075985;background-color:#075985;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#075985;background-color:#075985;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;line-height:1.2;text-align:center;color:#ffffff;">Make it fair by enabling the Goal Equalizer!</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:100px;padding-bottom:20px;padding-left:100px;word-break:break-word;">
<p style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:400px;" role="presentation" width="400px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#e5e7eb;">Everyone has a unique <b>Basal Metabolic Rate (BMR)</b>, dependent on factors like age, gender, height, and weight. The default goals are calibrated for a 35y/o 1.8m tall man. To ensure a fair competition, login and personalise your goals using the "Goal Equalizer" right below your personal settings.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endif %}
<!-- Final remarks -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody> {% if goal_equalizer_note is None %} <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr> {% endif %} <tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:20px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">Login to see who else is participating, change your team, and adjust the goal equalizers:</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:2px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,146 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>The "{{ competition.name }}" competition kicks off tomorrow! Give it a strong start, build momentum your best is waiting! But first, here's how the goals work and how to earn points.</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="200px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="10px" padding-top="45px" css-class="no-invert">
Workout Challenge
</mj-text>
<mj-button background-color="#075985" color="#fff" border-radius="24px" border="1px solid #fff" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif" padding="10px 25px">
LOGIN
</mj-button>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>The "{{ competition.name }}" competition kicks off tomorrow!</p>
<p>Give it a strong start, build momentum your best is waiting!</p>
<p>But first, here's how the goals work and how to earn points.</p>
</mj-text>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
<mj-section background-color="white" padding-top="5px">
<mj-column padding-top="0px">
<mj-text padding-top="10px" padding-bottom="15px">
<h3>How to earn activity points?</h3>
<ul>
<li>Each competition has its own activity goals.</li>
<li>You earn <b>1 point for every 1%</b> progress toward a goal.</li>
<li>For example, if the goal is 100 minutes of exercise and you work out 50 minutes, you earn 50 points.</li>
<li><i>Note:</i> Some goals have <b>minimum or maximum limits</b>. Activities above/below these limits wont earn you points and are marked with an asterisk (*).</li>
<li>Hover over a goal to view its limits, or over the asterisk for more details.</li>
</ul>
</mj-text>
</mj-column>
<mj-column>
<mj-raw>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Activity Goals:</h3>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;">
{% for goal in goals %}
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:10px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">{{ goal.name }}</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goal.goal|floatformat:0 }} {{ goal.metric }} <span style="font-size:10px;">/ {{ goal.period }}</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:250%;height:12px;" fillcolor="#e5e5e5">
<v:fill color="#e5e5e5" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
</div>
<!--<![endif]-->
</td>
</tr>
{% if goal.min_per_workout > 0 or goal.max_per_workout > 0 or goal.min_per_day > 0 or goal.max_per_day > 0 or goal.min_per_week > 0 or goal.max_per_week > 0 %}
<tr>
<td style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#6b7280;padding-top: 7px;" colspan="2"><b>Limits:</b>
<ul style="margin: 0px;">
{% if goal.min_per_workout > 0 %}<li>min <b>{{ goal.min_per_workout|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ workout</span></li>{% endif %}
{% if goal.max_per_workout > 0 %}<li>max <b>{{ goal.max_per_workout|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ workout</span></li>{% endif %}
{% if goal.min_per_day > 0 %}<li>min <b>{{ goal.min_per_day|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ day</span></li>{% endif %}
{% if goal.max_per_day > 0 %}<li>max <b>{{ goal.max_per_day|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ day</span></li>{% endif %}
{% if goal.min_per_week > 0 %}<li>min <b>{{ goal.min_per_week|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ week</span></li>{% endif %}
{% if goal.max_per_week > 0 %}<li>max <b>{{ goal.max_per_week|floatformat:0 }} {{ goal.metric }}</b> <span style="font-size:10px">/ week</span></li>{% endif %}
</ul>
</td>
</tr>
{% endif %}
</table>
</td>
</tr>
</table>
{% endfor %}
</td>
</tr>
</tbody>
</table>
</mj-raw>
</mj-column>
</mj-section>
<mj-raw> {% if goal_equalizer_note %} </mj-raw>
<!-- Goal Equalizer -->
<mj-section background-color="#075985" vertical-align="middle">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px" padding-top="20px">Make it fair by enabling the Goal Equalizer!</mj-text>
<mj-divider border-color="#fff" border-style="solid" border-width="1px" padding-left="100px" padding-right="100px" padding-bottom="20px"></mj-divider>
<mj-text align="center" color="#e5e7eb" font-size="11px" padding-bottom="25px" padding-top="0px">Everyone has a unique <b>Basal Metabolic Rate (BMR)</b>, dependent on factors like age, gender, height, and weight. The default goals are calibrated for a 35y/o 1.8m tall man. To ensure a fair competition, login and personalise your goals using the "Goal Equalizer" right below your personal settings.</mj-text>
</mj-column>
</mj-section>
<mj-raw> {% endif %} </mj-raw>
<!-- Final remarks -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-raw> {% if goal_equalizer_note is None %} </mj-raw>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-raw> {% endif %} </mj-raw>
<mj-text padding-top="20px">Login to see who else is participating, change your team, and adjust the goal equalizers:</mj-text>
<mj-button align="center" background-color="#075985" color="#ffffff" border-radius="24px" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="2px" padding-top="5px">LOGIN</mj-button>
<mj-text padding-bottom="10px" padding-top="0px">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,497 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> New week - fresh start! But first let's see where you're on the Leaderbaord for last week! </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="200">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style="">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:45px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:1px solid #fff;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>Let's see where you're on the leaderboard!</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>This week's leaderboard:</h3>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- This week leaderbaord section -->{% for competition in competitions_7d %}
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:10px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;"><b><span style="color:#075985">{{ competition.competition.name }}</span> - Team</b></div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<mj-raw> {% for team in competition.leaderboard.team %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if team.rank is not None %}#{{ team.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ team.name }}</td>
<td style="padding: 0 0 0 15px;">{% if team.total_capped is not None %}{{ team.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;"><b><span style="color:#075985">{{ competition.competition.name }}</span> - Individual</b></div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<mj-raw> {% for individual in competition.leaderboard.individual %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if individual.rank is not None %}#{{ individual.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ individual.username }}</td>
<td style="padding: 0 0 0 15px;">{% if individual.total_capped is not None %}{{ individual.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endfor %}
<!-- All time leaderbaord section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>All-time leaderboard:</h3>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% for competition in competitions_all %}
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:10px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;"><b><span style="color:#075985">{{ competition.competition.name }}</span> - Team</b></div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<mj-raw> {% for team in competition.leaderboard.team %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if team.rank is not None %}#{{ team.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ team.name }}</td>
<td style="padding: 0 0 0 15px;">{% if team.total_capped is not None %}{{ team.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;"><b><span style="color:#075985">{{ competition.competition.name }}</span> - Individual</b></div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<mj-raw> {% for individual in competition.leaderboard.individual %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if individual.rank is not None %}#{{ individual.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ individual.username }}</td>
<td style="padding: 0 0 0 15px;">{% if individual.total_capped is not None %}{{ individual.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endfor %}{% if goal_equalizer_note %}
<!-- Goal Equalizer -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#075985" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#075985;background-color:#075985;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#075985;background-color:#075985;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;line-height:1.2;text-align:center;color:#ffffff;">Make it fair by enabling the Goal Equalizer!</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:100px;padding-bottom:20px;padding-left:100px;word-break:break-word;">
<p style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:400px;" role="presentation" width="400px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#e5e7eb;">Everyone has a unique <b>Basal Metabolic Rate (BMR)</b>, dependent on factors like age, gender, height, and weight. The default goals are calibrated for a 35y/o 1.8m tall man. To ensure a fair competition, login and personalise your goals using the "Goal Equalizer" right below your personal settings.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endif %}
<!-- Final remarks -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:15px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody> {% if not goal_equalizer_note %} <tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr> {% endif %} <tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:20px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">Login to see more detailed analyses, charts, and breakdowns:</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:5px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br /> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br /> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,145 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>New week - fresh start! But first let's see where you're on the Leaderbaord for last week!</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="200px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="10px" padding-top="45px" css-class="no-invert">
Workout Challenge
</mj-text>
<mj-button background-color="#075985" color="#fff" border-radius="24px" border="1px solid #fff" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif" padding="10px 25px">
LOGIN
</mj-button>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>Let's see where you're on the leaderboard!</p>
</mj-text>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-text padding-top="5px" padding-bottom="0px">
<h3>This week's leaderboard:</h3>
</mj-text>
</mj-column>
</mj-section>
<!-- This week leaderbaord section -->
<mj-raw> {% for competition in competitions_7d %} </mj-raw>
<mj-section padding-top="0px" padding-bottom="10px" background-color="white">
<mj-column>
<mj-text>
<b><span style="color:#075985">{{ competition.competition.name }}</span> - Team</b>
</mj-text>
<mj-table>
<mj-raw> {% for team in competition.leaderboard.team %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if team.rank is not None %}#{{ team.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ team.name }}</td>
<td style="padding: 0 0 0 15px;">{% if team.total_capped is not None %}{{ team.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</mj-table>
</mj-column>
<mj-column>
<mj-text>
<b><span style="color:#075985">{{ competition.competition.name }}</span> - Individual</b>
</mj-text>
<mj-table>
<mj-raw> {% for individual in competition.leaderboard.individual %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if individual.rank is not None %}#{{ individual.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ individual.username }}</td>
<td style="padding: 0 0 0 15px;">{% if individual.total_capped is not None %}{{ individual.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</mj-table>
</mj-column>
</mj-section>
<mj-raw> {% endfor %} </mj-raw>
<!-- All time leaderbaord section -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-text padding-top="5px" padding-bottom="0px">
<h3>All-time leaderboard:</h3>
</mj-text>
</mj-column>
</mj-section>
<mj-raw> {% for competition in competitions_all %} </mj-raw>
<mj-section padding-top="0px" padding-bottom="10px" background-color="white">
<mj-column>
<mj-text>
<b><span style="color:#075985">{{ competition.competition.name }}</span> - Team</b>
</mj-text>
<mj-table>
<mj-raw> {% for team in competition.leaderboard.team %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if team.rank is not None %}#{{ team.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ team.name }}</td>
<td style="padding: 0 0 0 15px;">{% if team.total_capped is not None %}{{ team.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</mj-table>
</mj-column>
<mj-column>
<mj-text>
<b><span style="color:#075985">{{ competition.competition.name }}</span> - Individual</b>
</mj-text>
<mj-table>
<mj-raw> {% for individual in competition.leaderboard.individual %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{% if individual.rank is not None %}#{{ individual.rank }}{% else %}-/-{% endif %}</td>
<td style="padding: 0 15px;">{{ individual.username }}</td>
<td style="padding: 0 0 0 15px;">{% if individual.total_capped is not None %}{{ individual.total_capped|floatformat:0 }}P{% endif %}</td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</mj-table>
</mj-column>
</mj-section>
<mj-raw> {% endfor %} </mj-raw>
<mj-raw> {% if goal_equalizer_note %} </mj-raw>
<!-- Goal Equalizer -->
<mj-section background-color="#075985" vertical-align="middle">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px" padding-top="20px">Make it fair by enabling the Goal Equalizer!</mj-text>
<mj-divider border-color="#fff" border-style="solid" border-width="1px" padding-left="100px" padding-right="100px" padding-bottom="20px"></mj-divider>
<mj-text align="center" color="#e5e7eb" font-size="11px" padding-bottom="25px" padding-top="0px">Everyone has a unique <b>Basal Metabolic Rate (BMR)</b>, dependent on factors like age, gender, height, and weight. The default goals are calibrated for a 35y/o 1.8m tall man. To ensure a fair competition, login and personalise your goals using the "Goal Equalizer" right below your personal settings.</mj-text>
</mj-column>
</mj-section>
<mj-raw> {% endif %} </mj-raw>
<!-- Final remarks -->
<mj-section padding-top="0px" padding-bottom="15px" background-color="white">
<mj-column>
<mj-raw> {% if not goal_equalizer_note %} </mj-raw>
<mj-divider padding-bottom="0px" border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-raw> {% endif %} </mj-raw>
<mj-text padding-top="20px">Login to see more detailed analyses, charts, and breakdowns:</mj-text>
<mj-button align="center" background-color="#075985" color="#fff" border-radius="24px" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="5px" padding-top="5px">LOGIN</mj-button>
<mj-text padding-bottom="0px" padding-top="0px">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
</mj-section>
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br/> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br/> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,377 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> New week, new gains! But first time to log last week's workouts before we send out the leaderboard this afternoon! </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="200">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style="">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:45px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:1px solid #fff;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>New week, new gains!</p>
<mj-raw> {% if link_strava_note %} </mj-raw>
<p>But first, time to <b>log last week's workouts & steps</b> before we send out the leaderboard this afternoon!</p>
<mj-raw> {% else %} </mj-raw>
<p>But first, time to <b>log last week's steps</b> as Strava does not automatically import steps. Your workouts should have been imported already. We will send out the leaderboard this afternoon!</p>
<mj-raw> {% endif %} </mj-raw>
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Your last workouts:</h3>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- This week leaderbaord section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<th style="padding: 0 15px 0 0;">Time</th>
<th style="padding: 0 15px;">Workout</th>
</tr>
<mj-raw> {% for item in last_workouts %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{{ item.start_datetime|date:"D, M j H:i" }}</td>
<td style="padding: 0 15px;">{% if item.sport_type == "Steps" %}{{ item.steps }}{% else %}{{ item.duration }}{% endif %} <b>{{ item.sport_type }}</b></td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</table>
</td>
</tr> {% if link_strava_note %} <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:7px;padding-bottom:17px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/dashboard" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> Log Workouts & Steps </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr> {% else %} <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:7px;padding-bottom:17px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/dashboard" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> Log Steps </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr> {% endif %}
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Link Strava -->{% if link_strava_note %}
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#075985" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#075985;background-color:#075985;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#075985;background-color:#075985;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;line-height:1.2;text-align:center;color:#ffffff;">Skip the hassle of manual logging connect Strava for automatic workout imports!</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:100px;padding-bottom:20px;padding-left:100px;word-break:break-word;">
<p style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:400px;" role="presentation" width="400px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#e5e7eb;">
<p>Every day at 4 AM, your workouts are automatically imported, only syncing the most essential data with Strava to respect your privacy:</p>
<span>• Sport type</span><br />
<span>• Start time & duration</span><br />
<span>• Workout id</span><br />
<span>• Distance, kcal, kj, avg. watt</span>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:5px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
<a href="{{ MAIN_HOST }}/strava/link" style="display:inline-block;background:#ffffff;color:#075985;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> Link Strava </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endif %}
<!-- Final remarks -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br /> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br /> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,99 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>New week, new gains! But first time to log last week's workouts before we send out the leaderboard this afternoon!</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="200px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="10px" padding-top="45px" css-class="no-invert">
Workout Challenge
</mj-text>
<mj-button background-color="#075985" color="#fff" border-radius="24px" border="1px solid #fff" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif" padding="10px 25px">
LOGIN
</mj-button>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>New week, new gains!</p>
<mj-raw> {% if link_strava_note %} </mj-raw>
<p>But first, time to <b>log last week's workouts & steps</b> before we send out the leaderboard this afternoon!</p>
<mj-raw> {% else %} </mj-raw>
<p>But first, time to <b>log last week's steps</b> as Strava does not automatically import steps. Your workouts should have been imported already. We will send out the leaderboard this afternoon!</p>
<mj-raw> {% endif %} </mj-raw>
</mj-text>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-text padding-bottom="0px">
<h3>Your last workouts:</h3>
</mj-text>
</mj-column>
</mj-section>
<!-- This week leaderbaord section -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-table>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<th style="padding: 0 15px 0 0;">Time</th>
<th style="padding: 0 15px;">Workout</th>
</tr>
<mj-raw> {% for item in last_workouts %} </mj-raw>
<tr style="border-top:1px solid #ecedee;border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<td style="padding: 0 15px 0 0;">{{ item.start_datetime|date:"D, M j H:i" }}</td>
<td style="padding: 0 15px;">{% if item.sport_type == "Steps" %}{{ item.steps }}{% else %}{{ item.duration }}{% endif %} <b>{{ item.sport_type }}</b></td>
</tr>
<mj-raw> {% endfor %} </mj-raw>
</mj-table>
<mj-raw> {% if link_strava_note %} </mj-raw>
<mj-button align="center" background-color="#075985" color="#fff" border-radius="24px" href="{{ MAIN_HOST }}/dashboard" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="17px" padding-top="7px">Log Workouts & Steps</mj-button>
<mj-raw> {% else %} </mj-raw>
<mj-button align="center" background-color="#075985" color="#fff" border-radius="24px" href="{{ MAIN_HOST }}/dashboard" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="17px" padding-top="7px">Log Steps</mj-button>
<mj-raw> {% endif %} </mj-raw>
</mj-column>
</mj-section>
<!-- Link Strava -->
<mj-raw> {% if link_strava_note %} </mj-raw>
<mj-section background-color="#075985" vertical-align="middle">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px" padding-top="20px">Skip the hassle of manual logging connect Strava for automatic workout imports!</mj-text>
<mj-divider border-color="#fff" border-style="solid" border-width="1px" padding-left="100px" padding-right="100px" padding-bottom="20px"></mj-divider>
<mj-text align="center" color="#e5e7eb" font-size="11px" padding-bottom="25px" padding-top="0px">
<p>Every day at 4 AM, your workouts are automatically imported, only syncing the most essential data with Strava to respect your privacy:</p>
<span>• Sport type</span><br/>
<span>• Start time & duration</span><br/>
<span>• Workout id</span><br/>
<span>• Distance, kcal, kj, avg. watt</span>
</mj-text>
<mj-button align="center" background-color="#ffffff" color="#075985" border-radius="24px" href="{{ MAIN_HOST }}/strava/link" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="5px" padding-top="0px">Link Strava</mj-button>
</mj-column>
</mj-section>
<mj-raw> {% endif %} </mj-raw>
<!-- Final remarks -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-text padding-bottom="10px" padding-top="5px">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
</mj-section>
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br/> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br/> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,220 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> We received a request to reset your password. Click the link below or copy and paste it into your browser to reset your password (valid for 10 minutes). </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="150">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style="">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:65px;padding-bottom:65px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>We received a request to reset your password.</p>
<p>Click the link below or copy and paste it into your browser to reset your password (valid for 10 minutes):</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:10px;padding-bottom:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ RESET_URL }}" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> Reset Password </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:15px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p align="center"><a href="{{ RESET_URL }}">{{ RESET_URL }}</a></p>
<p>If you didnt request this, you can safely ignore this email.</p>
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>We received a request to reset your password. Click the link below or copy and paste it into your browser to reset your password (valid for 10 minutes).</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="150px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="65px" padding-top="65px" css-class="no-invert">
Workout Challenge
</mj-text>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>We received a request to reset your password.</p>
<p>Click the link below or copy and paste it into your browser to reset your password (valid for 10 minutes):</p>
</mj-text>
<mj-button align="center" background-color="#075985" color="#fff" border-radius="24px" href="{{ RESET_URL }}" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="0px" padding-top="10px">Reset Password</mj-button>
<mj-text padding-top="0px" padding-bottom="15px">
<p align="center"><a href="{{ RESET_URL }}">{{ RESET_URL }}</a></p>
<p>If you didnt request this, you can safely ignore this email.</p>
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
</mj-section>
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,518 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> Keep the momentum going keep the streak alive! Aim for 150-300 minutes of moderate or 75-150 minutes of vigorous activity weekly (or an equivalent mix) spread over at least 2 days, as recommended by the WHO. </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="200">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:45px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:1px solid #fff;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>Lets check in on your week!</p>
<p>Keep the momentum going keep the streak alive!</p>
<p>Aim for 150-300 minutes of moderate or 75-150 minutes of vigorous activity weekly (or an equivalent mix) spread over at least 2 days, as recommended by the WHO.</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-top:5px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-top:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Your Streak:</h3>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;">
<table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;">
<tr class="calendar" style="width: 35px; height: 35px; text-align: center; padding: 0px;" align="center">
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">M</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">M</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">T</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">T</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">W</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">W</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">T</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">T</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">F</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">F</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">S</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">S</div>
<!--<![endif]-->
</td>
<td style="font-weight: bold;">
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="FFFFFF"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:#000000;">S</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: #FFFFFF; color: #000000;">S</div>
<!--<![endif]-->
</td>
</tr>
<mj-raw> {% for week in calendar %} </mj-raw>
<tr class="calendar" style="width: 35px; height: 35px; text-align: center; padding: 0px;" align="center">
<mj-raw> {% for day in week %} </mj-raw>
<td>
<!--[if mso]><v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="{{ day.background_color }}"><v:textbox inset="0,0,0,0"><table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" valign="middle"><font style="font-size:14px; color:{{ day.color }};">{{ day.day }}</font></td></tr></table></v:textbox></v:oval><![endif]-->
<!--[if !mso]><!-->
<div class="day" style="height: 25px; width: 25px; margin-left: auto; margin-right: auto; justify-content: center; align-items: center; display: flex; border-radius: 9999px; background-color: {{ day.background_color }}; color: {{ day.color }};">{{ day.day }}</div>
<!--<![endif]-->
</td>
<mj-raw> {% endfor %} </mj-raw>
</tr>
<mj-raw> {% endfor %} </mj-raw>
<tr>
<td colspan="7" style="padding-top:5px;padding-bottom:5px;text-align:center; font-family:Arial, sans-serif; background-color:#F3F7FA;">
<span style="display:inline-block; width:25px; height:25px; line-height:25px; text-align:center; mso-line-height-rule:exactly; mso-table-lspace:0pt; mso-table-rspace:0pt; border:2px solid #60A8D8; border-radius:50%; background-color:#60A8D8; color:#FFFFFF; font-weight:bold; font-family:Arial, sans-serif;"> &#10003; </span>
<span style="display:inline-block; height:25px; line-height: 25px; text-align: center;padding-left:5px; mso-table-lspace:0pt; mso-table-rspace:0pt;color: #6b7280;font-weight:bold;">{{ week_streak }} Week Streak</span>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Your Personal 7-Day Goals:</h3>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;"> {% if goals.active_days is not None %} <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:10px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Active Days</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.active_days.recorded }} days <span style="font-size:10px;">/ {{ goals.active_days.target }} days</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.active_days.percent_vml }}%;height:12px;" fillcolor="#4a78b5"><v:fill color="#4a78b5" /></v:roundrect><![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.active_days.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table> {% endif %} {% if goals.minutes is not None %} <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:15px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Exercise</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.minutes.recorded }} min <span style="font-size:10px;">/ {{ goals.minutes.target }} min</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.minutes.percent_vml }}%;height:12px;" fillcolor="#4a78b5"><v:fill color="#4a78b5" /></v:roundrect><![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.minutes.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table> {% endif %} {% if goals.distance is not None %} <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:15px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Distance</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.distance.recorded }} km <span style="font-size:10px;">/ {{ goals.distance.target }} km</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.distance.percent_vml }}%;height:12px;" fillcolor="#4a78b5"><v:fill color="#4a78b5" /></v:roundrect><![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.distance.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table> {% endif %} </td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% if openai_quote is not None %}
<!-- AI Health Quote -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#075985" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#075985;background-color:#075985;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#075985;background-color:#075985;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.2;text-align:center;color:#ffffff;">{{ openai_quote }}</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:100px;padding-bottom:10px;padding-left:100px;word-break:break-word;">
<p style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:400px;" role="presentation" width="400px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#e5e7eb;">
<p>by OpenAI's ChatGPT 4o</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endif %}
<!-- Final remarks -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody> {% if openai_quote is None %} <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr> {% endif %} <tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:20px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">Login to see more detailed analyses, charts, and breakdowns:</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:2px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,270 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>Keep the momentum going keep the streak alive! Aim for 150-300 minutes of moderate or 75-150 minutes of vigorous activity weekly (or an equivalent mix) spread over at least 2 days, as recommended by the WHO.</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
<mj-style inline="inline">
td div .streak {
height: 25px;
width: 25px;
margin-left: auto;
margin-right: auto;
justify-content: center;
align-items: center;
display: flex;
border: 2px solid #075971;
border-radius: 9999px;
color: #075971;
font-weight: bold;
}
td div .day {
height: 25px;
width: 25px;
margin-left: auto;
margin-right: auto;
justify-content: center;
align-items: center;
display: flex;
border-radius: 9999px;
}
.calendar {
width: 35px;
height: 35px;
text-align: center;
padding: 0px;
}
</mj-style>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="200px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="10px" padding-top="45px" css-class="no-invert">
Workout Challenge
</mj-text>
<mj-button background-color="#075985" color="#fff" border-radius="24px" border="1px solid #fff" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif" padding="10px 25px">
LOGIN
</mj-button>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>Lets check in on your week!</p>
<p>Keep the momentum going keep the streak alive!</p>
<p>Aim for 150-300 minutes of moderate or 75-150 minutes of vigorous activity weekly (or an equivalent mix) spread over at least 2 days, as recommended by the WHO.</p>
</mj-text>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
<mj-section background-color="white" padding-top="5px">
<mj-column padding-top="0px">
<mj-text padding-bottom="0px">
<h3>Your Streak:</h3>
</mj-text>
<mj-table padding-top="0px">
<tr class="calendar">
<th>M</th>
<th>T</th>
<th>W</th>
<th>T</th>
<th>F</th>
<th>S</th>
<th>S</th>
</tr>
<mj-raw> {% for week in calendar %} </mj-raw>
<tr class="calendar">
<mj-raw> {% for day in week %} </mj-raw>
<td>
<!--[if mso]>
<v:oval xmlns:v="urn:schemas-microsoft-com:vml" style="height:30px;width:30px;" strokecolor="white" strokeweight="1px" fillcolor="{{ day.background_color }}">
<v:textbox inset="0,0,0,0">
<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" valign="middle">
<font style="font-size:14px; color:{{ day.color }};">{{ day.day }}</font>
</td>
</tr>
</table>
</v:textbox>
</v:oval>
<![endif]-->
<!--[if !mso]><!-->
<div class="day" style="background-color: {{ day.background_color }}; color: {{ day.color }};">{{ day.day }}</div>
<!--<![endif]-->
</td>
<mj-raw> {% endfor %} </mj-raw>
</tr>
<mj-raw> {% endfor %} </mj-raw>
<tr>
<td colspan="7" style="padding-top:5px;padding-bottom:5px;text-align:center; font-family:Arial, sans-serif; background-color:#F3F7FA;">
<span style="display:inline-block; width:25px; height:25px; line-height:25px; text-align:center; mso-line-height-rule:exactly; mso-table-lspace:0pt; mso-table-rspace:0pt; border:2px solid #60A8D8; border-radius:50%; background-color:#60A8D8; color:#FFFFFF; font-weight:bold; font-family:Arial, sans-serif;">
&#10003;
</span>
<span style="display:inline-block; height:25px; line-height: 25px; text-align: center;padding-left:5px; mso-table-lspace:0pt; mso-table-rspace:0pt;color: #6b7280;font-weight:bold;">{{ week_streak }} Week Streak</span>
</td>
</tr>
</mj-table>
</mj-column>
<mj-column>
<mj-raw>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>Your Personal 7-Day Goals:</h3>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;">
{% if goals.active_days is not None %}
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:10px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Active Days</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.active_days.recorded }} days <span style="font-size:10px;">/ {{ goals.active_days.target }} days</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.active_days.percent_vml }}%;height:12px;" fillcolor="#4a78b5">
<v:fill color="#4a78b5" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.active_days.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
{% endif %}
{% if goals.minutes is not None %}
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:15px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Exercise</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.minutes.recorded }} min <span style="font-size:10px;">/ {{ goals.minutes.target }} min</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.minutes.percent_vml }}%;height:12px;" fillcolor="#4a78b5">
<v:fill color="#4a78b5" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.minutes.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
{% endif %}
{% if goals.distance is not None %}
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="height:15px; font-size:0; line-height:0;">&nbsp;</td>
</tr>
<tr>
<td style="background-color:#f9f9f9; border-radius:8px; padding:18px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; font-weight:bold; color: #6b7280;">Distance</td>
<td align="right" style="font-size:14px; line-height:16px; mso-line-height-rule:exactly; color: #6b7280;">{{ goals.distance.recorded }} km <span style="font-size:10px;">/ {{ goals.distance.target }} km</span></td>
</tr>
<tr>
<td colspan="2" height="12" style="padding-top:8px; line-height:0; font-size:0;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" arcsize="50%" strokecolor="#e5e5e5" strokeweight="1px" style="width:{{ goals.distance.percent_vml }}%;height:12px;" fillcolor="#4a78b5">
<v:fill color="#4a78b5" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<div style="background:#e5e5e5; border-radius:4px; width:100%; height:12px;">
<div style="background:#4a78b5; height:12px; border-radius:4px; width:{{ goals.distance.percent }}%;"></div>
</div>
<!--<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
</table>
{% endif %}
</td>
</tr>
</tbody>
</table>
</mj-raw>
</mj-column>
</mj-section>
<mj-raw> {% if openai_quote is not None %} </mj-raw>
<!-- AI Health Quote -->
<mj-section background-color="#075985" vertical-align="middle">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#ffffff" font-size="16px" padding-bottom="10px" padding-top="20px">{{ openai_quote }}</mj-text>
<mj-divider border-color="#ffffff" border-style="solid" border-width="1px" padding-left="100px" padding-right="100px" padding-bottom="10px"></mj-divider>
<mj-text align="center" color="#e5e7eb" font-size="11px" padding-bottom="0px" padding-top="0px">
<p>by OpenAI's ChatGPT 4o</p>
</mj-text>
</mj-column>
</mj-section>
<mj-raw> {% endif %} </mj-raw>
<!-- Final remarks -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-raw> {% if openai_quote is None %} </mj-raw>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-raw> {% endif %} </mj-raw>
<mj-text padding-top="20px">Login to see more detailed analyses, charts, and breakdowns:</mj-text>
<mj-button align="center" background-color="#075985" color="#ffffff" border-radius="24px" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="2px" padding-top="5px">LOGIN</mj-button>
<mj-text padding-bottom="10px" padding-top="0px">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,375 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.moz-text-html .mj-column-per-50 {
width: 50% !important;
max-width: 50%;
}
</style>
<style type="text/css">
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#E1E8ED;">
<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> Thanks for signing up to the Workout Challenge! Bookmark the page in your browser. How does the competition work? How to earn activity points? You'll find out now! </div>
<div style="background-color:#E1E8ED;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"><v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" src="{{ MAIN_HOST }}/running_email.jpg" xmlns:v="urn:schemas-microsoft-com:vml" /><![endif]-->
<div style="margin:0 auto;max-width:600px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr style="vertical-align:top;">
<td background="{{ MAIN_HOST }}/running_email.jpg" style="background:#075985 url('{{ MAIN_HOST }}/running_email.jpg') no-repeat center center / cover;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="200">
<!--[if mso | IE]><table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" ><tr><td style=""><![endif]-->
<div class="mj-hero-content" style="margin:0px auto;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td style="">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;">
<tbody>
<tr>
<td align="center" class="no-invert" style="font-size:0px;padding:10px 25px;padding-top:45px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:30px;line-height:1.2;text-align:center;color:#ffffff;">Workout Challenge</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#075985" role="presentation" style="border:1px solid #fff;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#075985;" valign="middle">
<a href="{{ MAIN_HOST }}/login" style="display:inline-block;background:#075985;color:#ffffff;font-family:Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> LOGIN </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-bottom:0px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Hi {{ first_name }},</p>
<p>Thanks for signing up to the <b>Workout Challenge</b>!</p>
<p><b>Bookmark the page:</b> <a href="{{ MAIN_HOST }}">{{ MAIN_HOST }}</a></p>
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- How-to Section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:10px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:15px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>How does it work?</h3>
<ul>
<li>Join a friend's competition or create your own.</li>
<li>For automatic workout import, link your Strava account to the Workout Challenge.</li>
<li>Before a competition starts, join a team.</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:15px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<h3>How to earn activity points?</h3>
<ul>
<li>Each competition has its own activity goals.</li>
<li>You earn <b>1 point for every 1%</b> progress toward a goal.</li>
<li>For example, if the goal is 100 minutes of exercise and you work out 50 minutes, you earn 50 points.</li>
<li><i>Note:</i> Some goals have <b>minimum or maximum limits</b>. Activities above/below these limits wont earn you points and are marked with an asterisk (*).</li>
<li>Hover over a goal to view its limits, or over the asterisk for more details.</li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% if link_strava_note %}
<!-- Link Strava -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#075985" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#075985;background-color:#075985;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#075985;background-color:#075985;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:20px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;line-height:1.2;text-align:center;color:#ffffff;">Skip the hassle of manual logging connect Strava for automatic workout imports!</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:100px;padding-bottom:20px;padding-left:100px;word-break:break-word;">
<p style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #ffffff;font-size:1px;margin:0px auto;width:400px;" role="presentation" width="400px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#e5e7eb;">
<p>Every day at 4 AM, your workouts are automatically imported, only syncing the most essential data with Strava to respect your privacy:</p>
<span>• Sport type</span><br />
<span>• Start time & duration</span><br />
<span>• Workout id</span><br />
<span>• Distance, kcal, kj, avg. watt</span>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:5px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tbody>
<tr>
<td align="center" bgcolor="#ffffff" role="presentation" style="border:none;border-radius:24px;cursor:auto;mso-padding-alt:10px 25px;background:#ffffff;" valign="middle">
<a href="{{ MAIN_HOST }}/strava/link" style="display:inline-block;background:#ffffff;color:#075985;font-family:Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:24px;" target="_blank"> Link Strava </a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]--> {% endif %}
<!-- Final remarks -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0px;padding-top:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody> {% if not link_strava_note %} <tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #E1E8ED;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr> {% endif %} <tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:5px;padding-bottom:10px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1.2;text-align:left;color:#000000;">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footnote -->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:1.2;text-align:center;color:#9a9ea6;"><span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View file

@ -0,0 +1,99 @@
<mjml>
<!-- Written with https://mjml.io/ -->
<mj-head>
<mj-preview>Thanks for signing up to the Workout Challenge! Bookmark the page in your browser. How does the competition work? How to earn activity points? You'll find out now!</mj-preview>
<mj-attributes>
<mj-text line-height="1.2" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E1E8ED">
<mj-hero mode="fixed-height" height="200px" background-url="{{ MAIN_HOST }}/running_email.jpg" background-size="cover" background-repeat="no-repeat" background-color="#075985">
<mj-text align="center" font-size="30px" color="#ffffff" padding-bottom="10px" padding-top="45px" css-class="no-invert">
Workout Challenge
</mj-text>
<mj-button background-color="#075985" color="#fff" border-radius="24px" border="1px solid #fff" href="{{ MAIN_HOST }}/login" font-family="Helvetica, Arial, sans-serif" padding="10px 25px">
LOGIN
</mj-button>
</mj-hero>
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column padding-bottom="0px">
<mj-text padding-bottom="0px">
<p>Hi {{ first_name }},</p>
<p>Thanks for signing up to the <b>Workout Challenge</b>!</p>
<p><b>Bookmark the page:</b> <a href="{{ MAIN_HOST }}">{{ MAIN_HOST }}</a></p>
</mj-text>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
</mj-column>
</mj-section>
<!-- How-to Section -->
<mj-section padding-top="10px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-text padding-top="0px" padding-bottom="15px">
<h3>How does it work?</h3>
<ul>
<li>Join a friend's competition or create your own.</li>
<li>For automatic workout import, link your Strava account to the Workout Challenge.</li>
<li>Before a competition starts, join a team.</li>
</ul>
</mj-text>
</mj-column>
<mj-column>
<mj-text padding-top="0px" padding-bottom="15px">
<h3>How to earn activity points?</h3>
<ul>
<li>Each competition has its own activity goals.</li>
<li>You earn <b>1 point for every 1%</b> progress toward a goal.</li>
<li>For example, if the goal is 100 minutes of exercise and you work out 50 minutes, you earn 50 points.</li>
<li><i>Note:</i> Some goals have <b>minimum or maximum limits</b>. Activities above/below these limits wont earn you points and are marked with an asterisk (*).</li>
<li>Hover over a goal to view its limits, or over the asterisk for more details.</li>
</ul>
</mj-text>
</mj-column>
</mj-section>
<mj-raw> {% if link_strava_note %} </mj-raw>
<!-- Link Strava -->
<mj-section background-color="#075985" vertical-align="middle">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px" padding-top="20px">Skip the hassle of manual logging connect Strava for automatic workout imports!</mj-text>
<mj-divider border-color="#fff" border-style="solid" border-width="1px" padding-left="100px" padding-right="100px" padding-bottom="20px"></mj-divider>
<mj-text align="center" color="#e5e7eb" font-size="11px" padding-bottom="25px" padding-top="0px">
<p>Every day at 4 AM, your workouts are automatically imported, only syncing the most essential data with Strava to respect your privacy:</p>
<span>• Sport type</span><br/>
<span>• Start time & duration</span><br/>
<span>• Workout id</span><br/>
<span>• Distance, kcal, kj, avg. watt</span>
</mj-text>
<mj-button align="center" background-color="#ffffff" color="#075985" border-radius="24px" href="{{ MAIN_HOST }}/strava/link" font-family="Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="5px" padding-top="0px">Link Strava</mj-button>
</mj-column>
</mj-section>
<mj-raw> {% endif %} </mj-raw>
<!-- Final remarks -->
<mj-section padding-top="0px" padding-bottom="0px" background-color="white">
<mj-column>
<mj-raw> {% if not link_strava_note %} </mj-raw>
<mj-divider border-color="#E1E8ED" border-width="2px"></mj-divider>
<mj-raw> {% endif %} </mj-raw>
<mj-text padding-bottom="10px" padding-top="5px">
<p>Enjoy the Competition.</p>
<p>Good luck!</p>
</mj-text>
</mj-column>
</mj-section>
<!-- Footnote -->
</mj-section>
<mj-section full-width="full-width">
<mj-column width="100%" vertical-align="middle">
<mj-text align="center" color="#9a9ea6" font-size="11px" padding-bottom="0px" padding-top="0">
<span class="apple-link">Workout Challenge</span>
<br> Any issues? <a href="mailto:{{ EMAIL_REPLY_TO }}">Send us an Email</a>. <br> See the <a href="https://github.com/vanalmsick/workout_challenge">Source Code</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,181 @@
import time, datetime
import requests
from rest_framework import viewsets
from rest_framework.permissions import BasePermission, IsAdminUser, SAFE_METHODS, AllowAny
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from celery.exceptions import TimeoutError
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from django.shortcuts import get_object_or_404
from django.conf import settings
from django.core.cache import cache
from .serializers import PasswordResetSerializer, PasswordResetConfirmSerializer
from .models import CustomUser
from .serializers import CustomUserSerializer
from .filters import CustomUserFilter
from .strava import sync_strava
class IsOwnerOrReadOnly(BasePermission):
""" Permission class to only allow admins and owner to edit or delete entry """
def has_permission(self, request, view):
# Only authenticated users
if request.user.is_authenticated:
return True
return False
def has_object_permission(self, request, view, obj):
# Read requests always allowed
if request.method in SAFE_METHODS:
return True # allow GET, HEAD, OPTIONS (GET is filtered at viweset level to only show allowed entries)
# Only workout user can edit workout
if hasattr(obj, 'user') and obj.user == request.user:
return True
# Only owner of competition can modify
elif hasattr(obj, 'owner') and obj.owner == request.user:
return True
# Only owner can modify goals and awards
elif hasattr(obj, 'competition') and hasattr(obj.competition, 'owner') and obj.competition.owner == request.user:
return True
# If admin allow all requests
if bool(request.user and request.user.is_staff):
return True
return False
class UserPermissionClass(BasePermission):
""" Allow unauthenticated users to POST data - i.e. for registration """
def has_permission(self, request, view):
# Only create new requsts - i.e. POST
if request.method in ('POST', 'OPTIONS'):
return True
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return obj.pk == request.user.pk
class CustomUserViewSet(viewsets.ModelViewSet):
#queryset = Competition.objects.all()
serializer_class = CustomUserSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = CustomUserFilter
permission_classes = [UserPermissionClass]
def get_queryset(self):
# return all competitions the user is owner of or a participant of
#time.sleep(3) # throttle for testing
return CustomUser.objects.filter(Q(pk=self.request.user.pk) | Q(my_competitions__in=self.request.user.my_competitions.all())).distinct().order_by('username', 'id')
def get_object(self):
lookup_value = self.kwargs.get(self.lookup_field)
# Modify filter if I ask for myself instead of the id number
if str(lookup_value).lower() in ['me', 'my', 'myself', 'i']:
lookup_value = self.request.user.id
return get_object_or_404(self.get_queryset(), pk=lookup_value)
class PasswordResetView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = PasswordResetSerializer(data=request.data)
if serializer.is_valid():
serializer.save(request=request)
return Response({"detail": "Password reset e-mail sent."})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PasswordResetConfirmView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = PasswordResetConfirmSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({"detail": "Password has been reset."})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LinkStravaView(APIView):
""" API post view for users to link with Strava. """
permission_classes = [IsAuthenticated]
def post(self, request, code):
user = request.user
client_id = settings.STRAVA_CLIENT_ID
client_secret = settings.STRAVA_CLIENT_SECRET
if client_id == 1234321 or client_secret == "ReplaceWithClientSecret":
return Response({"message": "Sever configuration error - STRAVA_CLIENT_ID and/or STRAVA_CLIENT_SECRET are not set."}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
response = requests.post(
url='https://www.strava.com/oauth/token',
data={
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'grant_type': 'authorization_code'
}
)
if response.ok is False:
return Response({"message": "Invalid Strava linkage code"}, status=status.HTTP_400_BAD_REQUEST)
strava_tokens = response.json()
setattr(user, 'strava_refresh_token', strava_tokens.get('refresh_token', None))
setattr(user, 'strava_athlete_id', strava_tokens.get('athlete', {}).get('id', None))
user.save()
cache.set(f"strava_access_token_{user.id}", strava_tokens.get('access_token', None), int(strava_tokens.get('expires_in', 21600)) - 60)
try:
running_task = sync_strava.delay(user__id=user.id, start_datetime=datetime.datetime.now() - datetime.timedelta(days=43))
try:
running_task.get(timeout=100)
except TimeoutError:
print(f"Strava sync task is still running ({running_task.id}). Don't let the user wait so long.")
except requests.exceptions.HTTPError as err:
if '401 Client Error: Unauthorized' in str(err):
return Response({'message': 'Access to activities denied by Strava. Not sufficient permissions to download activities.'}, status=status.HTTP_403_FORBIDDEN)
else:
raise Response(err.response.json(), status=err.response.status_code)
return Response({"message": "Successfully linked Strava."}, status=status.HTTP_200_OK)
class UnlinkStravaView(APIView):
""" API post view for users to unlink Strava. """
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
setattr(user, 'strava_refresh_token', None)
setattr(user, 'strava_athlete_id', None)
user.save()
return Response({"message": "Successfully unlinked Strava."}, status=status.HTTP_200_OK)
class SyncStravaView(APIView):
""" API get view for users to sync Strava. """
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
if user.strava_refresh_token is None or user.strava_refresh_token == '':
return Response({"message": "Strava is not linked."}, status=status.HTTP_400_BAD_REQUEST)
if user.strava_last_synced_at is None or user.strava_last_synced_at == '' or user.strava_last_synced_at < (timezone.now() - datetime.timedelta(minutes=59)):
sync_strava(user__id=user.id)
return Response({"message": f"Successfully synced Strava."}, status=status.HTTP_200_OK)
return Response({"message": "Too many requests! You can only request a Strava sync every 60 minutes."}, status=status.HTTP_429_TOO_MANY_REQUESTS)