initial multi clone functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from rowers.models import Workout,Rower,Team
|
||||
from rowers.models import Workout,Rower,Team,PlannedSession
|
||||
from rowers.rows import validate_file_extension,must_be_csv,validate_image_extension
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
@@ -338,6 +338,15 @@ class DateRangeForm(forms.Form):
|
||||
class Meta:
|
||||
fields = ['startdate','enddate']
|
||||
|
||||
class SessionDateShiftForm(forms.Form):
|
||||
shiftstartdate = forms.DateField(
|
||||
initial=timezone.now(),
|
||||
widget=AdminDateWidget(),
|
||||
label='Shift to start on')
|
||||
|
||||
class Meta:
|
||||
fields = ['shiftstartdate']
|
||||
|
||||
# Form used to select workouts for the past N days
|
||||
class DeltaDaysForm(forms.Form):
|
||||
deltadays = forms.IntegerField(initial=7,required=False,label='')
|
||||
@@ -490,6 +499,13 @@ class WorkoutMultipleCompareForm(forms.Form):
|
||||
workouts = forms.ModelMultipleChoiceField(queryset=Workout.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple())
|
||||
|
||||
class PlannedSessionMultipleCloneForm(forms.Form):
|
||||
plannedsessions = forms.ModelMultipleChoiceField(
|
||||
queryset=PlannedSession.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
label='Planned Sessions'
|
||||
)
|
||||
|
||||
from rowers.metrics import axlabels
|
||||
|
||||
formaxlabels = axlabels.copy()
|
||||
|
||||
@@ -288,6 +288,10 @@ def get_dates_timeperiod(timeperiod):
|
||||
today = date.today()
|
||||
enddate = today-timezone.timedelta(days=today.weekday())-timezone.timedelta(days=1)
|
||||
startdate = enddate-timezone.timedelta(days=6)
|
||||
elif timeperiod=='nextweek':
|
||||
today = date.today()
|
||||
enddate = today-timezone.timedelta(days=today.weekday())-timezone.timedelta(days=1)+timezone.timedelta(days=7)
|
||||
startdate = enddate-timezone.timedelta(days=13)
|
||||
elif timeperiod=='lastmonth':
|
||||
today = date.today()
|
||||
startdate = today.replace(day=1)
|
||||
@@ -296,6 +300,14 @@ def get_dates_timeperiod(timeperiod):
|
||||
enddate = startdate+timezone.timedelta(days=32)
|
||||
enddate = enddate.replace(day=1)
|
||||
enddate = enddate-timezone.timedelta(days=1)
|
||||
elif timeperiod=='nextmonth':
|
||||
today = date.today()
|
||||
startdate = today.replace(day=1)
|
||||
startdate = startdate+timezone.timedelta(days=32)
|
||||
startdate = startdate.replace(day=1)
|
||||
enddate = startdate+timezone.timedelta(days=32)
|
||||
enddate = enddate.replace(day=1)
|
||||
enddate = enddate-timezone.timedelta(days=1)
|
||||
else:
|
||||
startdate = date.today()
|
||||
enddate = date.today()
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid_12 alpha">
|
||||
<div id="form" class="grid_6 alpha">
|
||||
<p>Warning: Large date ranges may take a long time to load. Huge date ranges may crash your browser.</p>
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
href="/rowers/sessions/multicreate/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multicreate/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multicreate/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
href="/rowers/sessions/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
190
rowers/templates/plannedsessions_multiclone_select.html
Normal file
190
rowers/templates/plannedsessions_multiclone_select.html
Normal file
@@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load rowerfilters %}
|
||||
|
||||
{% block title %}Workouts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
function toggle(source) {
|
||||
checkboxes = document.querySelectorAll("input[name='plannedsessions']");
|
||||
for(var i=0, n=checkboxes.length;i<n;i++) {
|
||||
checkboxes[i].checked = source.checked;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
// Get the form fields and hidden div
|
||||
var modality = $("#id_modality");
|
||||
var hidden = $("#id_waterboattype");
|
||||
|
||||
|
||||
// Hide the fields.
|
||||
// Use JS to do this in case the user doesn't have JS
|
||||
// enabled.
|
||||
|
||||
hidden.hide();
|
||||
|
||||
|
||||
|
||||
// Setup an event listener for when the state of the
|
||||
// checkbox changes.
|
||||
modality.change(function() {
|
||||
// Check to see if the checkbox is checked.
|
||||
// If it is, show the fields and populate the input.
|
||||
// If not, hide the fields.
|
||||
var Value = modality.val();
|
||||
if (Value=='water') {
|
||||
// Show the hidden fields.
|
||||
hidden.show();
|
||||
} else {
|
||||
// Make sure that the hidden fields are indeed
|
||||
// hidden.
|
||||
hidden.hide();
|
||||
|
||||
// You may also want to clear the value of the
|
||||
// hidden fields here. Just in case somebody
|
||||
// shows the fields, enters data to them and then
|
||||
// unticks the checkbox.
|
||||
//
|
||||
// This would do the job:
|
||||
//
|
||||
// $("#hidden_field").val("");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="grid_12 alpha">
|
||||
{% include "planningbuttons.html" %}
|
||||
</div>
|
||||
<div class="grid_4 alpha">
|
||||
{% if theteam %}
|
||||
<h1>Coach Overview. Team {{ theteam.name }}</h1>
|
||||
{% else %}
|
||||
<h1>Coach Overview</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="timeperiod" class="grid_2 dropdown">
|
||||
<button class="grid_2 alpha button gray small dropbtn">Select Time Period ({{ timeperiod|verbosetimeperiod }})</button>
|
||||
<div class="dropdown-content">
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/today">
|
||||
Today
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/thisweek">
|
||||
This Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/thismonth">
|
||||
This Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/lastweek">
|
||||
Last Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/lastmonth/">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/nextweek/">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/multiclone/nextmonth/">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
<div class="grid_2 dropdown">
|
||||
<button class="grid_2 alpha button green small dropbtn">
|
||||
Select Rower
|
||||
</button>
|
||||
<div class="dropdown-content">
|
||||
{% for member in user|team_rowers %}
|
||||
<a class="button green small" href="/rowers/sessions/multiclone/{{ timeperiod }}/rower/{{ member.id }}">{{ member.user.first_name }} {{ member.user.last_name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid_12 alpha">
|
||||
<div class="grid_6 alpha">
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
{% endif %}
|
||||
<div class="grid_4 alpha">
|
||||
<table>
|
||||
{{ dateform.as_table }}
|
||||
</table>
|
||||
{% csrf_token %}
|
||||
</div>
|
||||
<div class="grid_2 omega">
|
||||
<input name='daterange' class="button green" type="submit" value="Submit">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="grid_5 prefix_1 omega">
|
||||
<form id="searchform" method="get" accept-charset="utf-8">
|
||||
<div class="grid_3 prefix_1 alpha">
|
||||
<input class="searchfield" id="searchbox" name="q" type="text" placeholder="Search">
|
||||
</div>
|
||||
<div class="grid_1 omega">
|
||||
<button class="button blue small" type="submit">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<form enctype="multipart/form-data" method="post">
|
||||
<div id="workouts_table" class="grid_8 alpha">
|
||||
|
||||
|
||||
{% if plannedsessions %}
|
||||
|
||||
<input type="checkbox" onClick="toggle(this)" /> Toggle All<br/>
|
||||
|
||||
<table width="100%" class="listtable">
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p> No sessions found </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="form_settings" class="grid_4 alpha">
|
||||
<p>Select two or more planned sessions on the left,
|
||||
select the date when the new cycle starts below
|
||||
and press submit</p>
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ dateshiftform.as_table }}
|
||||
</table>
|
||||
<div class="grid_1 prefix_2 suffix_1">
|
||||
<p>
|
||||
<input name='workoutselectform' class="button green" type="submit" value="Submit">
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid_4">
|
||||
<p>You can use the date and search forms above to search through all
|
||||
sessions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -38,6 +38,14 @@
|
||||
href="/rowers/sessions/coach/lastmonth/">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/coach/nextweek/">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/coach/nextmonth/">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
@@ -48,6 +48,14 @@
|
||||
href="/rowers/sessions/manage/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/manage/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/manage/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated and user|is_manager %}
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||
Last Month
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextweek/rower/{{ rower.id }}">
|
||||
Next Week
|
||||
</a>
|
||||
<a class="button gray small alpha"
|
||||
href="/rowers/sessions/create/nextmonth/rower/{{ rower.id }}">
|
||||
Next Month
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="form_settings" class="grid_4 alpha">
|
||||
<p><b>Warning: You are on an experimental part of the site. Use at your own risk.</b></p>
|
||||
<p>Select two or more workouts on the left, set your plot settings below,
|
||||
and press submit</p>
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -193,6 +193,8 @@ def verbosetimeperiod(timeperiod):
|
||||
'thismonth': 'This Month',
|
||||
'lastmonth': 'Last Month',
|
||||
'lastweek': 'Last Week',
|
||||
'nextweek': 'Next Week',
|
||||
'nextmonth': 'Next Month',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -419,6 +419,17 @@ urlpatterns = [
|
||||
url(r'^sessions/create/(?P<timeperiod>[\w\ ]+.*)$',
|
||||
views.plannedsession_create_view),
|
||||
|
||||
url(r'^sessions/multiclone$',views.plannedsession_multiclone_view),
|
||||
url(r'^sessions/multiclone/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)/extra/(?P<extrasessions>\d+)$',
|
||||
views.plannedsession_multiclone_view),
|
||||
url(r'^sessions/multiclone/rower/(?P<rowerid>\d+)$',
|
||||
views.plannedsession_multiclone_view),
|
||||
url(
|
||||
r'^sessions/multiclone/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',
|
||||
views.plannedsession_multiclone_view),
|
||||
url(r'^sessions/multiclone/(?P<timeperiod>[\w\ ]+.*)$',
|
||||
views.plannedsession_multiclone_view),
|
||||
|
||||
url(r'^sessions/multicreate$',views.plannedsession_multicreate_view),
|
||||
url(r'^sessions/multicreate/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)/extra/(?P<extrasessions>\d+)$',
|
||||
views.plannedsession_multicreate_view),
|
||||
|
||||
108
rowers/views.py
108
rowers/views.py
@@ -50,6 +50,7 @@ from rowers.forms import (
|
||||
UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm,
|
||||
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
|
||||
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
|
||||
PlannedSessionMultipleCloneForm,SessionDateShiftForm,
|
||||
)
|
||||
from rowers.models import (
|
||||
Workout, User, Rower, WorkoutForm,FavoriteChart,
|
||||
@@ -11576,6 +11577,113 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt',
|
||||
'the_div':div,
|
||||
})
|
||||
|
||||
# Cloning sessions
|
||||
@user_passes_test(hasplannedsessions,login_url="/rowers/planmembership/",
|
||||
redirect_field_name=None)
|
||||
def plannedsession_multiclone_view(
|
||||
request,timeperiod='nextweek',
|
||||
rowerid=0,
|
||||
startdate=timezone.now()-datetime.timedelta(days=30),
|
||||
enddate=timezone.now()+datetime.timedelta(days=1)):
|
||||
|
||||
if rowerid==0:
|
||||
r = getrower(request.user)
|
||||
else:
|
||||
try:
|
||||
r = Rower.objects.get(id=rowerid)
|
||||
except Rower.DoesNotExist:
|
||||
raise Http404("This rower doesn't exist")
|
||||
if not checkaccessuser(request.user,r):
|
||||
raise PermissionDenied("You don't have access to this plan")
|
||||
|
||||
if 'startdate' in request.session:
|
||||
startdate = iso8601.parse_date(request.session['startdate'])
|
||||
|
||||
|
||||
if 'enddate' in request.session:
|
||||
enddate = iso8601.parse_date(request.session['enddate'])
|
||||
|
||||
if request.method == 'POST' and 'daterange' in request.POST:
|
||||
dateform = DateRangeForm(request.POST)
|
||||
if dateform.is_valid():
|
||||
startdate = dateform.cleaned_data['startdate']
|
||||
enddate = dateform.cleaned_data['enddate']
|
||||
startdatestring = startdate.strftime('%Y-%m-%d')
|
||||
enddatestring = enddate.strftime('%Y-%m-%d')
|
||||
request.session['startdate'] = startdatestring
|
||||
request.session['enddate'] = enddatestring
|
||||
else:
|
||||
dateform = DateRangeForm(initial={
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
})
|
||||
|
||||
if request.method == 'POST' and 'plannedsessions' in request.POST:
|
||||
form = PlannedSessionMultipleCloneForm(request.POST)
|
||||
dateshiftform = SessionDateShiftForm(request.POST)
|
||||
if form.is_valid() and dateshiftform.is_valid():
|
||||
cd = form.cleaned_data
|
||||
sps = cd['plannedsessions']
|
||||
std = min([ps.startdate for ps in sps])
|
||||
shiftstartdate = dateshiftform.cleaned_data['shiftstartdate']
|
||||
delta = shiftstartdate-std
|
||||
for ps in sps:
|
||||
rowers = ps.rower.all()
|
||||
teams = ps.team.all()
|
||||
ps.pk = None
|
||||
ps.startdate += delta
|
||||
ps.enddate += delta
|
||||
ps.save()
|
||||
for rower in rowers:
|
||||
add_rower_session(rower,ps)
|
||||
for team in teams:
|
||||
add_team_session(team,ps)
|
||||
|
||||
url = reverse(plannedsession_multicreate_view,
|
||||
kwargs = {
|
||||
'rowerid':r.id,
|
||||
'timeperiod':timeperiod,
|
||||
})
|
||||
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
startdate = datetime.datetime.combine(startdate,datetime.time())
|
||||
enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
|
||||
enddate = enddate+datetime.timedelta(days=1)
|
||||
|
||||
sps = PlannedSession.objects.filter(
|
||||
manager=request.user,
|
||||
rower__in=[r],
|
||||
startdate__lte=enddate,
|
||||
enddate__gte=startdate).order_by("startdate","enddate")
|
||||
|
||||
query = request.GET.get('q')
|
||||
if query:
|
||||
query_list = query.split()
|
||||
sps = sps.filter(
|
||||
reduce(operator.and_,
|
||||
(Q(name__icontains=q) for q in query_list)) |
|
||||
reduce(operator.and_,
|
||||
(Q(comment__icontains=q) for q in query_list))
|
||||
)
|
||||
|
||||
form = PlannedSessionMultipleCloneForm()
|
||||
form.fields["plannedsessions"].queryset = sps
|
||||
|
||||
dateshiftform = SessionDateShiftForm()
|
||||
|
||||
return render(request, 'plannedsessions_multiclone_select.html',
|
||||
{'plannedsessions':sps,
|
||||
'dateform':dateform,
|
||||
'startdate':startdate,
|
||||
'enddate':enddate,
|
||||
'form':form,
|
||||
'dateshiftform':dateshiftform,
|
||||
'rower':r,
|
||||
'timeperiod':timeperiod,
|
||||
}
|
||||
)
|
||||
|
||||
# Individual user creates training for himself
|
||||
@user_passes_test(hasplannedsessions,login_url="/rowers/planmembership/",
|
||||
redirect_field_name=None)
|
||||
|
||||
@@ -205,6 +205,13 @@
|
||||
<p> </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_1 tooltip">
|
||||
{% if user.is_authenticated %}
|
||||
<p><a class="button gray small" href="/rowers/sessions/">Plans</a></p>
|
||||
{% else %}
|
||||
<p> </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_2 tooltip">
|
||||
{% if user.is_authenticated %}
|
||||
<p>
|
||||
@@ -219,13 +226,6 @@
|
||||
{% block teams %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="grid_1 tooltip">
|
||||
{% if user.is_authenticated %}
|
||||
<p><a class="button gray small" href="/rowers/sessions/">Plans</a></p>
|
||||
{% else %}
|
||||
<p> </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_1 tooltip">
|
||||
{% block challenges %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user