Private
Public Access
1
0

Merge branch 'release/v22.4.7'

This commit is contained in:
2024-12-14 14:14:54 +01:00
12 changed files with 285 additions and 103 deletions
+2 -2
View File
@@ -369,7 +369,7 @@ def workout_summary_to_df(
return df return df
def resample(id, r, parent, overwrite='copy'): def resample(id, r, parent, overwrite=False):
data, row = getrowdata_db(id=id) data, row = getrowdata_db(id=id)
messages = [] messages = []
@@ -393,7 +393,7 @@ def resample(id, r, parent, overwrite='copy'):
data['pace'] = data['pace'] / 1000. data['pace'] = data['pace'] / 1000.
data['time'] = data['time'] / 1000. data['time'] = data['time'] / 1000.
if overwrite == 'overwrite': if overwrite == True:
# remove CP data # remove CP data
try: try:
cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id) cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id)
+14 -6
View File
@@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField):
class ResampleForm(forms.Form): class ResampleForm(forms.Form):
resamplechoices = ( # add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy"
('overwrite', 'Overwrite Workout'),
('copy', 'Create a Duplicate Workout')
)
resamplechoice = forms.ChoiceField( resamplechoice = forms.ChoiceField(
initial='copy', choices=resamplechoices, label='Copy behavior') required=True,
choices=((True, 'overwrite'), (False, 'create copy')),
label='Resample choice',
widget=forms.RadioSelect)
class TrainingZonesForm(forms.Form): class TrainingZonesForm(forms.Form):
@@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form):
races = VirtualRace.objects.filter( races = VirtualRace.objects.filter(
registration_closure__gt=timezone.now()) registration_closure__gt=timezone.now())
# set upload_to_X based on r.X_auto_export
for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']:
if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']:
self.fields['upload_to_'+field].initial = True
registrations = IndoorVirtualRaceResult.objects.filter( registrations = IndoorVirtualRaceResult.objects.filter(
race__in=races, race__in=races,
userid=r.id) userid=r.id)
@@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form):
upload_to_TrainingPeaks = forms.BooleanField(initial=False, upload_to_TrainingPeaks = forms.BooleanField(initial=False,
required=False, required=False,
label='Export to TrainingPeaks') label='Export to TrainingPeaks')
upload_to_Intervals = forms.BooleanField(initial=False,
required=False,
label='Export to TrainingPeaks')
# do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)') # do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)')
makeprivate = forms.BooleanField(initial=False, required=False, makeprivate = forms.BooleanField(initial=False, required=False,
label='Make Workout Private') label='Make Workout Private')
+12
View File
@@ -17,6 +17,7 @@ import os
from uuid import uuid4 from uuid import uuid4
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
import rowers.dataprep as dataprep
from rowsandall_app.settings import ( from rowsandall_app.settings import (
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL
@@ -101,6 +102,15 @@ class IntervalsIntegration(SyncIntegration):
def createworkoutdata(self, w, *args, **kwargs) -> str: def createworkoutdata(self, w, *args, **kwargs) -> str:
dozip = kwargs.get('dozip', True) dozip = kwargs.get('dozip', True)
# resample if wanted by user, not tested
if w.user.intervals_resample_to_1s:
datadf, id, msgs = dataprep.resample(
w.id, w.user, w, overwrite=False
)
w_resampled = Workout.objects.get(id=id)
filename = w_resampled.csvfilename
else:
w_resampled = None
filename = w.csvfilename filename = w.csvfilename
try: try:
row = rowingdata(csvfile=filename) row = rowingdata(csvfile=filename)
@@ -128,6 +138,8 @@ class IntervalsIntegration(SyncIntegration):
except TypeError: except TypeError:
newnotes = 'from'+w.workoutsource+' via rowsandall.com' newnotes = 'from'+w.workoutsource+' via rowsandall.com'
if w.user.intervals_resample_to_1s and w_resampled:
w_resampled.delete()
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype]) row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
if dozip: if dozip:
gzfilename = tcxfilename + '.gz' gzfilename = tcxfilename + '.gz'
+72
View File
@@ -1174,6 +1174,7 @@ class Rower(models.Model):
c2_auto_import = models.BooleanField(default=False) c2_auto_import = models.BooleanField(default=False)
intervals_auto_export = models.BooleanField(default=False) intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = models.BooleanField(default=False) intervals_auto_import = models.BooleanField(default=False)
intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField( sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True)
@@ -4560,8 +4561,79 @@ class RowerExportForm(ModelForm):
'rp3_auto_import', 'rp3_auto_import',
'intervals_auto_import', 'intervals_auto_import',
'intervals_auto_export', 'intervals_auto_export',
'intervals_resample_to_1s'
] ]
class RowerExportFormStrava(ModelForm):
class Meta:
model = Rower
fields = [
'stravaexportas',
'strava_auto_export',
'strava_auto_import',
'strava_auto_delete',
]
class RowerExportFormIntervals(ModelForm):
class Meta:
model = Rower
fields = [
'intervals_auto_import',
'intervals_auto_export',
'intervals_resample_to_1s',
]
class RowerExportFormGarmin(ModelForm):
class Meta:
model = Rower
fields = [
'garminactivity',
]
class RowerExportFormPolar(ModelForm):
class Meta:
model = Rower
fields = [
'polar_auto_import',
]
class RowerExportFormConcept2(ModelForm):
class Meta:
model = Rower
fields = [
'c2_auto_export',
'c2_auto_import',
]
class RowerExportFormSportTracks(ModelForm):
class Meta:
model = Rower
fields = [
'sporttracks_auto_export',
]
class RowerExportFormTrainingPeaks(ModelForm):
class Meta:
model = Rower
fields = [
'trainingpeaks_auto_export',
]
class RowerExportFormRP3(ModelForm):
class Meta:
model = Rower
fields = [
'rp3_auto_import',
]
class RowerExportFormNK(ModelForm):
class Meta:
model = Rower
fields = [
'nk_auto_import'
]
# Simple form to set rower's Functional Threshold Power # Simple form to set rower's Functional Threshold Power
class SimpleRowerPowerForm(ModelForm): class SimpleRowerPowerForm(ModelForm):
otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water') otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water')
+108 -40
View File
@@ -7,8 +7,10 @@
{% block main %} {% block main %}
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1> <h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
<form enctype="multipart/form-data" action="" method="post">
{% csrf_token %}
<ul class="main-content"> <ul class="main-content">
<li class="grid_2"> <li class="grid_4">
<p>You are currently connected to: <p>You are currently connected to:
{% if rower.c2token is not None and rower.c2token != '' %} {% if rower.c2token is not None and rower.c2token != '' %}
Concept2 Logbook, Concept2 Logbook,
@@ -41,20 +43,106 @@
Intervals.icu Intervals.icu
{% endif %} {% endif %}
</p> </p>
<p>
Click on the icons to establish the connection or to renew the authorization.
</p>
</li>
<li class="grid_4">
<h2>API Key</h2>
<p>{{ apikey }}</p>
<p>
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
</p>
<p>This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.</p>
</li>
{% if form.errors %} {% if form.errors %}
<li class="rounder">
<p style="color: red;"> <p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below. Please correct the error{{ form.errors|pluralize }} below.
</p> </p>
</li>
{% endif %} {% endif %}
<p> <li class="rounder">
<form enctype="multipart/form-data" action="" method="post"> <h2>NK</h2>
<table> <table>
{{ form.as_table }} {{ forms.nk.as_table }}
</table>
{% csrf_token %}
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </table>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
</li>
<li class="rounder">
<h2>Concept2</h2>
<table>
{{ forms.c2.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
</li>
<li class="rounder">
<h2>RP3</h2>
<table>
{{ forms.rp3.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
alt="connect with RP3" width="130"></a></p>
</li>
<li class="rounder">
<h2>Rojabo</h2>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p>
</li>
<li class="rounder">
<h2>Intervals.icu</h2>
<table>
{{ forms.intervals.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_logo_with_name.png"
alt="connect with intervals.icu"></a></p>
</li>
<li class="rounder">
<h2>SportTracks</h2>
<table>
{{ forms.sporttracks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
</li>
<li class="rounder">
<h2>TrainingPeaks</h2>
<table>
{{ forms.trainingpeaks.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Polar</h2>
<table>
{{ forms.polar.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Garmin Connect</h2>
<table>
{{ forms.garmin.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
you may have to set your activity to Run or Ride while rowing.
</p> </p>
{% if rower.garmintoken and rower.garmintoken != '' %} {% if rower.garmintoken and rower.garmintoken != '' %}
<p> <p>
@@ -63,22 +151,25 @@
page. Look for the "Rowsandall" app. page. Look for the "Rowsandall" app.
</p> </p>
{% endif %} {% endif %}
<p> </li>
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will <li class="rounder">
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you <h2>Strava</h2>
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" <p><input type="submit" value="Save"></p>
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so {{ forms.strava.as_p }}
you may have to set your activity to Run or Ride while rowing. <p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
</p>
<p> <p>
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
check box. check box.
</p> </p>
</li> </li>
<li class="grid_2">
{% if grants %} {% if grants %}
<li class="rounder">
<h2>Applications</h2> <h2>Applications</h2>
<p>
These applications have access to your Rowsandall data.
</p>
<table width="100%"> <table width="100%">
<thead> <thead>
<tr> <tr>
@@ -99,35 +190,12 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<li>
{% endif %} {% endif %}
<h2>API Key</h2>
<p>{{ apikey }}</p>
<p>
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
</p>
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
</li>
</ul> </ul>
<p>Click on one of the icons below to connect to the service of your </form>
choice or to renew the authorization.</p>
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
alt="connect with Polar" width="130"></a></p>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
alt="connect with RP3" width="130"></a></p>
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
alt="connect with Rojabo" width="130"></a></p>
<p><a href="/rowers/me/intervalsauthorize"><img src="/static/img/intervals_icu.png"
alt="connect with intervals.icu" height="30"></a></p>
{% endblock %} {% endblock %}
Binary file not shown.
+19 -17
View File
@@ -245,23 +245,6 @@ def do_sync(w, options, quick=False):
dologging('c2_log.log','Error C2') dologging('c2_log.log','Error C2')
pass pass
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
try:
id = strava_integration.workout_export(w)
dologging(
'strava_export_log.log',
'exporting workout {id} as {type}'.format(
id=w.id,
type=w.workouttype,
)
)
except NoTokenError: # pragma: no cover
id = 0
message = "Please connect to Strava first"
except Exception as e:
dologging('stravalog.log', e)
if do_icu_export: if do_icu_export:
intervals_integration = IntervalsIntegration(w.user.user) intervals_integration = IntervalsIntegration(w.user.user)
try: try:
@@ -334,4 +317,23 @@ def do_sync(w, options, quick=False):
dologging('tp_export.log','No Token Error') dologging('tp_export.log','No Token Error')
return 0 return 0
# we do Strava last.
if do_strava_export: # pragma: no cover
strava_integration = StravaIntegration(w.user.user)
try:
id = strava_integration.workout_export(w)
dologging(
'strava_export_log.log',
'exporting workout {id} as {type}'.format(
id=w.id,
type=w.workouttype,
)
)
except NoTokenError: # pragma: no cover
id = 0
message = "Please connect to Strava first"
except Exception as e:
dologging('stravalog.log', e)
return 1 return 1
+1
View File
@@ -662,6 +662,7 @@ urlpatterns = [
re_path(r'^me/messages/$', views.user_messages, name='user_messages'), re_path(r'^me/messages/$', views.user_messages, name='user_messages'),
re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'), re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'),
re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'), re_path(r'^me/messages/(?P<id>\d+)/markread/$', views.user_message_markread, name='user_message_markread'),
re_path(r'^me/messages/(?P<id>\d+)/delete/$', views.user_message_delete, name='user_message_delete'),
re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'), re_path(r'^me/messages/user/(?P<userid>\d+)/$', views.user_messages, name='user_messages'),
re_path(r'^me/delete/$', views.remove_user, name='remove_user'), re_path(r'^me/delete/$', views.remove_user, name='remove_user'),
re_path(r'^survey/$', views.survey, name='survey'), re_path(r'^survey/$', views.survey, name='survey'),
+7 -1
View File
@@ -179,7 +179,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo
IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert, IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert,
Condition, StaticChartRowerForm, FollowerForm, Condition, StaticChartRowerForm, FollowerForm,
VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm,
StepEditorForm, iDokladToken ) StepEditorForm, iDokladToken,
RowerExportFormStrava, RowerExportFormPolar,
RowerExportFormSportTracks, RowerExportFormTrainingPeaks,
RowerExportFormConcept2, RowerExportFormGarmin,
RowerExportFormIntervals, RowerExportFormRP3,
RowerExportFormNK,
)
from rowers.models import ( from rowers.models import (
FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet,
get_course_timezone, BaseConditionFormSet, get_course_timezone, BaseConditionFormSet,
+27 -3
View File
@@ -280,7 +280,6 @@ def user_message_delete(request,id=0): # pragma: no cover
url = reverse('user_messages') url = reverse('user_messages')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
if msg.receiver == request.user.rower: if msg.receiver == request.user.rower:
msg.delete() msg.delete()
messages.info(request,'Deleted message {id}'.format(id=id)) messages.info(request,'Deleted message {id}'.format(id=id))
@@ -458,19 +457,43 @@ def rower_exportsettings_view(request, userid=0):
'polar_auto_import': 'polartoken', 'polar_auto_import': 'polartoken',
'c2_auto_export': 'c2token', 'c2_auto_export': 'c2token',
'c2_auto_import': 'c2token', 'c2_auto_import': 'c2token',
'runkeeper_auto_export': 'runkeepertoken',
'sporttracks_auto_export': 'sporttrackstoken', 'sporttracks_auto_export': 'sporttrackstoken',
'strava_auto_export': 'stravatoken', 'strava_auto_export': 'stravatoken',
'strava_auto_import': 'stravatoken', 'strava_auto_import': 'stravatoken',
'strava_auto_delete': 'stravatoken', 'strava_auto_delete': 'stravatoken',
'trainingpeaks_auto_export': 'tptoken', 'trainingpeaks_auto_export': 'tptoken',
'rp3_auto_import': 'rp3token', 'rp3_auto_import': 'rp3token',
'nk_auto_import': 'nktoken' 'nk_auto_import': 'nktoken',
'intervals_auto_export': 'intervals_token',
'intervals_resample_to_1s': 'intervals_token',
} }
r = getrequestrowercoachee(request, userid=userid) r = getrequestrowercoachee(request, userid=userid)
forms = {
'polar': RowerExportFormPolar(instance=r),
'c2': RowerExportFormConcept2(instance=r),
'sporttracks': RowerExportFormSportTracks(instance=r),
'strava': RowerExportFormStrava(instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(instance=r),
'rp3': RowerExportFormRP3(instance=r),
'intervals': RowerExportFormIntervals(instance=r),
'nk': RowerExportFormNK(instance=r),
'garmin': RowerExportFormGarmin(instance=r),
}
if request.method == 'POST': if request.method == 'POST':
form = RowerExportForm(request.POST) form = RowerExportForm(request.POST)
forms = {
'polar': RowerExportFormPolar(request.POST, instance=r),
'c2': RowerExportFormConcept2(request.POST, instance=r),
'sporttracks': RowerExportFormSportTracks(request.POST, instance=r),
'strava': RowerExportFormStrava(request.POST, instance=r),
'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r),
'rp3': RowerExportFormRP3(request.POST, instance=r),
'intervals': RowerExportFormIntervals(request.POST, instance=r),
'nk': RowerExportFormNK(request.POST, instance=r),
'garmin': RowerExportFormGarmin(request.POST, instance=r),
}
if form.is_valid(): if form.is_valid():
cd = form.cleaned_data cd = form.cleaned_data
if r.rowerplan == 'basic': # pragma: no cover if r.rowerplan == 'basic': # pragma: no cover
@@ -529,6 +552,7 @@ def rower_exportsettings_view(request, userid=0):
return render(request, 'rower_exportsettings.html', return render(request, 'rower_exportsettings.html',
{'form': form, {'form': form,
'forms': forms,
'rower': r, 'rower': r,
'breadcrumbs': breadcrumbs, 'breadcrumbs': breadcrumbs,
'grants': grants, 'grants': grants,
-11
View File
@@ -5609,17 +5609,6 @@ def workout_upload_view(request,
return response return response
else: else:
if not is_ajax: if not is_ajax:
if r.c2_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_C2'] = True
if r.strava_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_Strava'] = True
if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_SportTracks'] = True
if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover
uploadoptions['upload_to_TrainingPeaks'] = True
form = DocumentsForm(initial=docformoptions) form = DocumentsForm(initial=docformoptions)
optionsform = UploadOptionsForm(initial=uploadoptions, optionsform = UploadOptionsForm(initial=uploadoptions,
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB