diff --git a/rowers/dataprep.py b/rowers/dataprep.py index f6eaaba1..85bf4b22 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -65,6 +65,8 @@ queue = django_rq.get_queue('default') queuelow = django_rq.get_queue('low') queuehigh = django_rq.get_queue('default') +from rowsandall_app.settings import SITE_URL + user = settings.DATABASES['default']['USER'] password = settings.DATABASES['default']['PASSWORD'] @@ -131,6 +133,66 @@ def get_latlon(id): return [pd.Series([]), pd.Series([])] +def workout_summary_to_df( + rower, + startdate=datetime.datetime(1970,1,1), + enddate=timezone.now()+timezone.timedelta(days=1)): + + ws = Workout.objects.filter(user=rower).order_by("startdatetime") + + types = [] + names = [] + startdatetimes = [] + timezones = [] + distances = [] + durations = [] + weightcategories = [] + weightvalues = [] + notes = [] + tcx_links = [] + csv_links = [] + rscores = [] + trimps = [] + + for w in ws: + types.append(w.workouttype) + names.append(w.name) + startdatetimes.append(w.startdatetime) + timezones.append(w.timezone) + distances.append(w.distance) + durations.append(w.duration) + weightcategories.append(w.weightcategory) + weightvalues.append(w.weightvalue) + notes.append(w.notes) + tcx_link = SITE_URL+'/rowers/workout/{id}/emailtcx'.format( + id=w.id + ) + tcx_links.append(tcx_link) + csv_link = SITE_URL+'/rowers/workout/{id}/emailcsv'.format( + id=w.id + ) + csv_links.append(csv_link) + trimps.append(workout_trimp(w)) + rscore = workout_rscore(w) + rscores.append(int(rscore[0])) + + df = pd.DataFrame({ + 'name':names, + 'date':startdatetimes, + 'timezone':timezones, + 'type':types, + 'distance (m)':distances, + 'duration ':durations, + 'weight category':weightcategories, + 'weight (kg)':weightvalues, + 'notes':notes, + 'Stroke Data TCX':tcx_links, + 'Stroke Data CSV':csv_links, + 'TRIMP Training Load':trimps, + 'TSS Training Load':rscores, + }) + + return df def get_workouts(ids, userid): goodids = [] @@ -1510,13 +1572,9 @@ def repair_data(verbose=False): res = data.to_csv(w.csvfilename + '.gz', index_label='index', compression='gzip') - print 'adding csv file' else: - print w.id, ' No stroke records anywhere' w.delete() except: - print 'failed' - print str(sys.exc_info()[0]) pass # A wrapper around the rowingdata class, with some error catching @@ -1576,6 +1634,7 @@ def testdata(time, distance, pace, spm): def getrowdata_db(id=0, doclean=False, convertnewtons=True): data = read_df_sql(id) data['x_right'] = data['x_right'] / 1.0e6 + data['deltat'] = data['time'].diff() if data.empty: rowdata, row = getrowdata(id=id) diff --git a/rowers/middleware.py b/rowers/middleware.py index e9c90956..c87c6c44 100644 --- a/rowers/middleware.py +++ b/rowers/middleware.py @@ -71,5 +71,30 @@ class PowerTimeFitnessMetricMiddleWare(object): result = do_update(request.user,mode='rower') result = do_update(request.user,mode='water') +from django.shortcuts import redirect +allowed_paths = [ + '/rowers/me/delete', + '/', + '/logout', + '/logout/', + '/rowers/me/gdpr-optin/', + '/rowers/me/gdpr-optin-confirm/', + '/rowers/me/gdpr-optin', + '/rowers/me/gdpr-optin-confirm' + '/rowers/exportallworkouts/', + '/rowers/exportallworkouts', +] + +class GDPRMiddleWare(object): + def process_request(self, request): + if request.user.is_authenticated() and request.path not in allowed_paths: + r = getrower(request.user) + nexturl = request.path + if 'optin' in nexturl: + nexturl = '/rowers/list-workouts' + if not r.gdproptin: + return redirect( + '/rowers/me/gdpr-optin/?next=%s' % nexturl + ) diff --git a/rowers/models.py b/rowers/models.py index 4082994f..f9d89468 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -173,6 +173,7 @@ def update_records(url=c2url): df['Distance'] = df['Event'] df['Duration'] = 0 + print row.Duration for nr,row in df.iterrows(): if 'm' in row['Record']: df.ix[nr,'Distance'] = row['Record'][:-1] @@ -185,7 +186,6 @@ def update_records(url=c2url): tobj = datetime.datetime.strptime(row['Record'],'%H:%M:%S.%f') df.ix[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6 - print row.Duration for nr,row in df.iterrows(): try: weightcategory = row.Weight.lower() @@ -468,6 +468,8 @@ class Rower(models.Model): ('Yoga','Yoga'), ) user = models.OneToOneField(User) + gdproptin = models.BooleanField(default=False) + gdproptindate = models.DateTimeField(blank=True,null=True) # Heart Rate Zone data max = models.IntegerField(default=192,verbose_name="Max Heart Rate") @@ -606,6 +608,19 @@ class Rower(models.Model): def clean_email(self): return self.user.email.lower() +class DeactivateUserForm(forms.ModelForm): + class Meta: + model = User + fields = ['is_active'] + +class DeleteUserForm(forms.ModelForm): + delete_user = forms.BooleanField(initial=False, + label='Remove my account and all data') + + class Meta: + model = User + fields = [] + @receiver(models.signals.post_save,sender=Rower) def auto_delete_teams_on_change(sender, instance, **kwargs): if instance.rowerplan != 'coach': diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 3aad0ecf..29018c24 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -396,6 +396,10 @@ def get_workouts_session(r,ps): def update_plannedsession(ps,cd): for attr, value in cd.items(): + if attr == 'comment': + value.replace("\r\n", " "); + value.replace("\n", " "); + print value setattr(ps, attr, value) ps.save() diff --git a/rowers/tasks.py b/rowers/tasks.py index 31afcdec..29619a05 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -385,6 +385,24 @@ def handle_sendemail_hard(workoutid, useremail, return 1 +# send email when user deletes account +@app.task +def handle_sendemail_userdeleted(name, email, debug=False, **kwargs): + fullemail = 'roosendaalsander@gmail.com' + subject = 'User account deleted' + message = 'Sander,\n\n' + message += 'The user {name} ({email}) has just deleted his account'.format( + name=name, + email=email + ) + email = EmailMessage(subject,message, + 'Rowsandall ', + [fullemail]) + + res = email.send() + + return 1 + # send email to me when an unrecognized file is uploaded @app.task def handle_sendemail_unrecognized(unrecognizedfile, useremail, @@ -499,6 +517,37 @@ def handle_zip_file(emailfrom, subject, file,**kwargs): # Send email with CSV attachment +@app.task +def handle_sendemailsummary(first_name, last_name, email, csvfile, **kwargs): + # send email with attachment + fullemail = first_name + " " + last_name + " " + "<" + email + ">" + subject = "File from Rowsandall.com" + message = "Dear " + first_name + ",\n\n" + message += "Please find attached the requested summary file.\n\n" + message += "Best Regards, the Rowsandall Team" + + email = EmailMessage(subject, message, + 'Rowsandall ', + [fullemail]) + + if os.path.isfile(csvfile): + email.attach_file(csvfile) + else: + csvfile2 = csvfile + with gzip.open(csvfile + '.gz', 'rb') as f_in, open(csvfile2, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + + email.attach_file(csvfile2) + os.remove(csvfile2) + + res = email.send() + try: + os.remove(csvfile) + except: + pass + + return 1 + @app.task def handle_sendemailcsv(first_name, last_name, email, csvfile,**kwargs): diff --git a/rowers/templates/502.html b/rowers/templates/502.html index bbf6f82f..edd8094e 100644 --- a/rowers/templates/502.html +++ b/rowers/templates/502.html @@ -204,24 +204,6 @@ - diff --git a/rowers/templates/export_workouts.html b/rowers/templates/export_workouts.html new file mode 100644 index 00000000..61bec7b6 --- /dev/null +++ b/rowers/templates/export_workouts.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall Workouts Summary Export{% endblock %} + +{% block content %} +
+
+
+ + {{ form.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+

+ With this form, you can export a summary table for all workouts within a selected date range. + The table will be sent to you as a CSV file which can be opened in excel. The table contains + links to download stroke data per workout. +

+

+ By setting the start date to your registration date or earlier and the end date to today, + you will receive all workout data we are storing for you. +

+
+
+ +{% endblock %} diff --git a/rowers/templates/gdpr_optin.html b/rowers/templates/gdpr_optin.html new file mode 100644 index 00000000..939a307f --- /dev/null +++ b/rowers/templates/gdpr_optin.html @@ -0,0 +1,222 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}GDPR Opt-In{% endblock %} + +{% block content %} +
+

GDPR Opt-In

+

+ + To comply with the European Union General Data Protection Regulation, + we need to record your consent to use personal data on this website. + Please take some time to review our data policies. If you agree and + opt in, click the green button at the bottom to be taken to the site. + If you do not agree, please use the red button to delete your + account. This will irreversibly delete all your data on rowsandall.com + and remove your account. + +

+
+ +

Personal information collection

+

+ rowsandall.com may collect and use the following kinds of information: +

    +
  • information about your use of this website +
  • information that you provide for the purpose of + registering with the website +
  • information about transactions carried out over this website +
  • information that you provide for the purpose of + using this website, for instance heart rate band and weight information. +
  • any other information that you send to rowsandall.com +
+ Explicitly, the following information is collected: +
    +
  • User name, email address, encrypted password (PBKDF2 algorithm + with a SHA256 Hash and a password stretching mechanism recommended + by NIST). +
  • Your birth date. +
  • Your user consent to these GDPR compliance policies, and the + date at which you consented. Without this consent, the site cannot + be used. +
  • Weight category. With individual workouts, you may record your + actual weight during the workout. +
  • Your gender, if you decide to provide it. +
  • Heart rate zones you define. Only the actual values are stored. We do + not keep records of their evolution. +
  • Power zones and Functional Threshold power. Only the + actual values are stored. We do + not keep records of their evolution. +
  • Parameter values used to construct your Critical Power curve + (OTW and OTE). +
  • User preferences, such as the buttons and functionalities + defined in the Workflow left + panel and right panel. +
  • Tokens and their expiry dates used for sharing data with + other fitness sites. You can actually revoke these at any time. +
  • User preferences as shown on the user settings page +
  • Your favorite Flex Charts if defined +
  • The teams you are a member of. +
  • Estimated four minute, 2k and 1 hour ergometer and OTW power values, + based on the workouts you upload, and their evolution during your + usage of the site +
  • For members on the Coach plan, the names and purposes of teams. Names + of team members. (Members who delete their account will be erased from + existing teams.) +
  • Any rowing courses you uploaded +
  • Training targets and training plans +
  • Your uploaded workouts, their names, boat type, start time and date, + time zone information, total distance, duration, weight, average + and maximum heart rate, and references to their locations on third + party sites, rigging parameters (if provided), + summary information, any notes you made, privacy status and + ranking piece status. +
  • Stroke data, including, for each stroke, time, heart rate, + pace, stroke rate, work per stroke, power, average and peak + force, drive length, distance, drive speed, catch and finish angles, + slip, wash, peak force angle, effective angle, rhythm, + efficiency and distance per stroke, as well as any other + data in the data files you shared to rowsandall.com +
  • Images created on the site, from your rowing data, or uploaded + to the site. +
  • Comments you make to your and other people's workouts +
+

+ +

+ The site is only accessible to user of 16 years and older. +

+ +

Data Deletion

+ +

All the data mentioned in the previous section are stored in files + and in a database, hosted on our hosting provider's servers. Our + hosting provider is creating backups of those data. The database backups + are retained for 7 days. File backups are retained for 30 days. However, + the file names or content do not contain any links to the users. The + link to the file is stored under the user data in the database, so once + a database entry is removed, there is no way to link a file with data + to a particular user. +

+

+ When a user requests deletion of the data, his account and all data linked to his account + are removed from the database and the files are deleted. This includes all data mentioned in the + previous section. In backups, database entries will be removed after 7 days and files after + 30 days. +

+ +

Data deletion can be initiated by the user through the button on the user settings page.

+ +

Data Security

+ +

The site uses SSL to encrypt data transferred between the server and the client (web browers, + mobile apps, third party sites). Any forms are secured from Cross Site Request Forgery (CSRF) using Django's + CSRF middleware.

+ +

+ We have a double defense against reading or editing of personal data. First, we ensure that all "protected" views + are only visible to logged-in users. Only logged-in users have buttons leading to the private parts of the site. + As a second step, protecting against guessing of URL, before serving data from the database, we check explicitly that the data + is owned by the user in question, redirecting unauthorized requests to a "Permission Denied" page. Private data is collected + through POST requests to prevent them from being visible in URL data. +

+ +

rowsandall.com will take reasonable technical and organisational precautions to prevent the loss, + misuse or alteration of your personal information.

+ +

In case of loss, misuse or alteration of your personal information, we will inform you without undue delay and take measures + to prevent further misuse. In particular, we will deactivate your account, which will not delete the data but make them + inaccessible even for people who obtained the password (including yourself). We will await your instructions. If no + instructions are received within 7 days of contacting you, your account and all your data will be removed. +

+ + +

Data Sharing and access to data

+ +

+ Only the data owner can the site administrator can edit and/or delete the data. Per our data policy, the site administrator will not alter + or delete any data owned by users, unless requested so. As data are not stored on servers that are physically owner by us, or by + our hosting provider, but we use rented server space, we are technically sharing the information to agents or sub-contractors. +

+ +

+ Where rowsandall.com discloses your personal information to its agents or sub-contractors for these purposes, + the agent or sub-contractor in question will be obligated to use that personal information in accordance with the terms of this privacy statement. + Our hosting provider is based in the European Union and is bound by the same GDPR regulation as we are. +

+ +

In addition to the disclosures reasonably necessary for the purposes identified elsewhere above, rowsandall.com + may disclose your personal information to the extent that it is required to do so by law, in connection with + any legal proceedings or prospective legal proceedings, and in order to establish, exercise or defend its legal rights.

+ + +

+ Workout data and charts based on workout data can be shared to anyone by sharing the URL. Workouts have an option to be set to + "private", in which case the data are not visible to anyone except the owner. The site is not searchable for data other than + your own data, so there is no way for other people to track your workouts, unless you share them. +

+ +

+ Cross-border data transfers. Information that rowsandall.com collects may be stored and processed in and transferred + between any of the countries in which rowsandall.com operates to enable the use of the information in accordance with this privacy policy. + In addition, personal information that you submit for publication on the website will be published on the internet and + may be available around the world. + You agree to such cross-border transfers of personal information. +

+ +

+ By accepting an "invitation" to become a member of a team, or by requesting to become part of a team, you agree to automatically + share all your workout data (including workouts done prior to becoming a member of the team) to the team manager (coach) and, + depending to the team policy, to other members of the team. When you leave + a team, all your workout data will immediately become invisible to those who had access to it during your team membership, including + workouts that cover the period of time when you were member of the team. As a member of a team, you grant the team manager + permission to edit workout data + on your behalf, including the creation of charts and cross workout analysis. You also grant the team manager permission to + edit your heart rate and power settings, as well as functional threshold information and the account information accessible on your + settings page under the header "Account Information". The team manager is not able to access or change your passwords, team memberships, + favorite charts, export settings, workflow layout, or secret tokens. Also, the team manager is not able to download all your data, + not can he deactivate or delete your account. +

+ +

+ This site offers the possiblity to synchronize your data with other fitness sites. By clicking on the share or connect button (link, or + equivalent) you agree to share information between rowsandall.com and the other website. Rowsandall.com is not responsible for the privacy + policies or practices of any third party. +

+ +

Data portability

+ +

Through the "download your data" link on the user settings page, each user can download all workout data. Stroke data can be downloaded + through links in the downloaded workout data file.

+ +
+ +

+ To start or continue using the site, please give your consent by clicking on the green Opt In button below. +

+ +

+

+

+ + +
+ {% csrf_token %} + +
+ + +
+
+ +
+ +{% endblock %} diff --git a/rowers/templates/legal.html b/rowers/templates/legal.html index 37951f1b..6a4b3d35 100644 --- a/rowers/templates/legal.html +++ b/rowers/templates/legal.html @@ -159,61 +159,181 @@

Credit

This document was created using a Contractology template available at -http://www.freenetlaw.com..

+http://www.freenetlaw.com.. It was modified to reflect the GDPR requirements.

-

Personal information collection

-

rowsandall.com may collect and use the following kinds of information: -

+

Personal information collection

+

+ rowsandall.com may collect and use the following kinds of information: +

+ Explicitly, the following information is collected: + +

-

Using personal information

+

+ The site is only accessible to user of 16 years and older. +

-

rowsandall.com may use your personal information to: -

+

Data Deletion

-

Where rowsandall.com discloses your personal information to its agents or sub-contractors for these purposes, the agent or sub-contractor in question will be obligated to use that personal information in accordance with the terms of this privacy statement.

+

All the data mentioned in the previous section are stored in files + and in a database, hosted on our hosting provider's servers. Our + hosting provider is creating backups of those data. The database backups + are retained for 7 days. File backups are retained for 30 days. However, + the file names or content do not contain any links to the users. The + link to the file is stored under the user data in the database, so once + a database entry is removed, there is no way to link a file with data + to a particular user. +

+

+ When a user requests deletion of the data, his account and all data linked to his account + are removed from the database and the files are deleted. This includes all data mentioned in the + previous section. In backups, database entries will be removed after 7 days and files after + 30 days. +

-

In addition to the disclosures reasonably necessary for the purposes identified elsewhere above, rowsandall.com may disclose your personal information to the extent that it is required to do so by law, in connection with any legal proceedings or prospective legal proceedings, and in order to establish, exercise or defend its legal rights.

+

Data deletion can be initiated by the user through the button on the user settings page.

-

Securing your data

+

Data Security

-

rowsandall.com will take reasonable technical and organisational precautions to prevent the loss, misuse or alteration of your personal information.

+

The site uses SSL to encrypt data transferred between the server and the client (web browers, + mobile apps, third party sites). Any forms are secured from Cross Site Request Forgery (CSRF) using Django's + CSRF middleware.

-

rowsandall.com will store all the personal information you provide

+

+ We have a double defense against reading or editing of personal data. First, we ensure that all "protected" views + are only visible to logged-in users. Only logged-in users have buttons leading to the private parts of the site. + As a second step, protecting against guessing of URL, before serving data from the database, we check explicitly that the data + is owned by the user in question, redirecting unauthorized requests to a "Permission Denied" page. Private data is collected + through POST requests to prevent them from being visible in URL data. +

-

Cross-border data transfers

+

rowsandall.com will take reasonable technical and organisational precautions to prevent the loss, + misuse or alteration of your personal information.

-

Information that rowsandall.com collects may be stored and processed in and transferred between any of the countries in which rowsandall.com operates to enable the use of the information in accordance with this privacy policy.

+

In case of loss, misuse or alteration of your personal information, we will inform you without undue delay and take measures + to prevent further misuse. In particular, we will deactivate your account, which will not delete the data but make them + inaccessible even for people who obtained the password (including yourself). We will await your instructions. If no + instructions are received within 7 days of contacting you, your account and all your data will be removed. +

-

In addition, personal information that you submit for publication on the website will be published on the internet and may be available around the world.

-

You agree to such cross-border transfers of personal information.

+

Data Sharing and access to data

-

Updating this statement

+

+ Only the data owner can the site administrator can edit and/or delete the data. Per our data policy, the site administrator will not alter + or delete any data owned by users, unless requested so. As data are not stored on servers that are physically owner by us, or by + our hosting provider, but we use rented server space, we are technically sharing the information to agents or sub-contractors. +

-

rowsandall.com may update this privacy policy by posting a new version on this website.

+

+ Where rowsandall.com discloses your personal information to its agents or sub-contractors for these purposes, + the agent or sub-contractor in question will be obligated to use that personal information in accordance with the terms of this privacy statement. + Our hosting provider is based in the European Union and is bound by the same GDPR regulation as we are. +

-

You should check this page occasionally to ensure you are familiar with any changes.

+

In addition to the disclosures reasonably necessary for the purposes identified elsewhere above, rowsandall.com + may disclose your personal information to the extent that it is required to do so by law, in connection with + any legal proceedings or prospective legal proceedings, and in order to establish, exercise or defend its legal rights.

-

Other websites

+ +

+ Workout data and charts based on workout data can be shared to anyone by sharing the URL. Workouts have an option to be set to + "private", in which case the data are not visible to anyone except the owner. The site is not searchable for data other than + your own data, so there is no way for other people to track your workouts, unless you share them. +

-

This website connects to other websites. By clicking the connect button (link, or equivalent) you agree to share information between rowsandall.com and the other website.

+

+ Cross-border data transfers. Information that rowsandall.com collects may be stored and processed in and transferred + between any of the countries in which rowsandall.com operates to enable the use of the information in accordance with this privacy policy. + In addition, personal information that you submit for publication on the website will be published on the internet and + may be available around the world. + You agree to such cross-border transfers of personal information. +

+ +

+ By accepting an "invitation" to become a member of a team, or by requesting to become part of a team, you agree to automatically + share all your workout data (including workouts done prior to becoming a member of the team) to the team manager (coach) and, + depending to the team policy, to other members of the team. When you leave + a team, all your workout data will immediately become invisible to those who had access to it during your team membership, including + workouts that cover the period of time when you were member of the team. As a member of a team, you grant the team manager + permission to edit workout data + on your behalf, including the creation of charts and cross workout analysis. You also grant the team manager permission to + edit your heart rate and power settings, as well as functional threshold information and the account information accessible on your + settings page under the header "Account Information". The team manager is not able to access or change your passwords, team memberships, + favorite charts, export settings, workflow layout, or secret tokens. Also, the team manager is not able to download all your data, + not can he deactivate or delete your account. +

+ +

+ This site offers the possiblity to synchronize your data with other fitness sites. By clicking on the share or connect button (link, or + equivalent) you agree to share information between rowsandall.com and the other website. Rowsandall.com is not responsible for the privacy + policies or practices of any third party. +

-

rowsandall.com is not responsible for the privacy policies or practices of any third party.

+

Data portability

+ +

Through the "download your data" link on the user settings page, each user can download all workout data. Stroke data can be downloaded + through links in the downloaded workout data file.

- {% endblock content %} \ No newline at end of file + {% endblock content %} diff --git a/rowers/templates/registration_form.html b/rowers/templates/registration_form.html index 82f51fce..0e337e78 100644 --- a/rowers/templates/registration_form.html +++ b/rowers/templates/registration_form.html @@ -31,6 +31,15 @@

To use rowsandall, you need to register and agree with the Terms of Service.

Registration is free.

+ +

Some of our advanced services only work if you give us your + (approximate) birth date, sex and weight category, with 72.5 kg the + bounday between heavies and lighties for men, and 59 kg for women. +

+ +

Also, we are restricting access to the site to 16 years and older + because of EU data protection regulations.

+
{% endblock content %} diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index fc9a574b..91a0637c 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -1,11 +1,25 @@ {% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} {% block title %}Change Rower {% endblock %} {% block content %}
-

User Settings

-

Need help? Click to read the tutorial

+
+

User Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}

+

Need help? Click to read the tutorial

+
+

Heart Rate Zones

@@ -88,7 +102,11 @@

+ {% if rower.user == user %} Password Change + {% else %} +   + {% endif %}

@@ -116,7 +134,7 @@ {% csrf_token %}
- {% if rower.rowerplan == 'basic' %} + {% if rower.rowerplan == 'basic' and rower.user == user %} Upgrade {% else %}   @@ -152,6 +170,7 @@
+{% if rower.user == user %}

@@ -161,7 +180,7 @@

- +

Favorite Charts

@@ -193,7 +212,27 @@
-
+
+

+

GDPR - Data Protection

+ + +
+

+ Delete Account +

+
+

+
+
{% if grants %}

Applications

@@ -223,7 +262,7 @@ {% endif %}
- +{% endif %} diff --git a/rowers/templates/team.html b/rowers/templates/team.html index d112ea92..cc53418c 100644 --- a/rowers/templates/team.html +++ b/rowers/templates/team.html @@ -54,7 +54,7 @@ {% for member in members %} - {{ member.user.first_name }} {{ member.user.last_name }} + {{ member.user.first_name }} {{ member.user.last_name }} {% if team.manager == user %} Drop {% else %} diff --git a/rowers/templates/userprofile_deactivate.html b/rowers/templates/userprofile_deactivate.html new file mode 100644 index 00000000..7f4b2c15 --- /dev/null +++ b/rowers/templates/userprofile_deactivate.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Deactivate your account{% endblock %} + +{% block content %} +
+

Deactivate your account

+
+

Account deactivation is reversible. After you logout, you will not be + able to access your data any more, but the site admin can restore your + account.

+
{% csrf_token %} + {{ user_form.as_p}} + {% csrf_token %} + +
+ +
+ +{% endblock %} diff --git a/rowers/templates/userprofile_delete.html b/rowers/templates/userprofile_delete.html new file mode 100644 index 00000000..04331c3f --- /dev/null +++ b/rowers/templates/userprofile_delete.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Delete your account{% endblock %} + +{% block content %} +
+

Delete your account

+
+

Warning: This will remove your account and all your data. + You will not be able to recover from this action. We cannot restore + your account

+
{% csrf_token %} + {{ user_form.as_p}} + {% csrf_token %} + +
+ +
+ +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index c1947531..71c4b6fd 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -121,6 +121,7 @@ urlpatterns = [ url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), url(r'^imports/$', TemplateView.as_view(template_name='imports.html'), name='imports'), + url(r'^exportallworkouts/?$',views.workouts_summaries_email_view), url(r'^agegroupcp/(?P\d+)$',views.agegroupcpview), url(r'^agegroupcp/(?P\d+)/(?P\d+)$',views.agegroupcpview), url(r'^ajax_agegroup/(?P\d+)/(?P\w+.*)/(?P\w+.*)/(?P\d+)$', @@ -335,6 +336,10 @@ urlpatterns = [ url(r'^user-multiflex/$',views.multiflex_view), url(r'^user-multiflex$',views.multiflex_view), url(r'^user-multiflex-data$',views.multiflex_data), + url(r'^me/deactivate$',views.deactivate_user), + url(r'^me/delete$',views.remove_user), + url(r'^me/gdpr-optin-confirm/?$',views.user_gdpr_confirm), + url(r'^me/gdpr-optin/?$',views.user_gdpr_optin), url(r'^me/teams/$',views.rower_teams_view), url(r'^me/calcdps/$',views.rower_calcdps_view), url(r'^me/exportsettings/$',views.rower_exportsettings_view), @@ -357,6 +362,7 @@ urlpatterns = [ url(r'^me/request/(\w+.*)/$',views.manager_requests_view), url(r'^me/request/$',views.manager_requests_view), url(r'^me/edit/$',views.rower_edit_view), + url(r'^rower/edit/(?P\d+)$',views.rower_edit_view), url(r'^me/edit/(.+.*)/$',views.rower_edit_view), url(r'^me/c2authorize/$',views.rower_c2_authorize), url(r'^me/revokeapp/(?P\d+)$',views.rower_revokeapp_view), diff --git a/rowers/views.py b/rowers/views.py index 8da3803d..74d4b9d4 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -12,6 +12,7 @@ import yaml from PIL import Image from numbers import Number from django.views.generic.base import TemplateView +from django.contrib.auth import views as auth_views from django.db.models import Q from django import template from django.db import IntegrityError, transaction @@ -54,7 +55,7 @@ from rowers.forms import ( ) from rowers.models import ( Workout, User, Rower, WorkoutForm,FavoriteChart, - PlannedSession + PlannedSession, DeactivateUserForm,DeleteUserForm ) from rowers.models import ( RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, @@ -118,10 +119,12 @@ from rowers.plannedsessions import * from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv from rowers.tasks import ( handle_sendemail_unrecognized,handle_sendemailnewcomment, + handle_sendemailsummary, handle_sendemailnewresponse, handle_updatedps, handle_updatecp,long_test_task,long_test_task2, handle_zip_file,handle_getagegrouprecords, - handle_updatefitnessmetric + handle_updatefitnessmetric, + handle_sendemail_userdeleted, ) from scipy.signal import savgol_filter @@ -572,6 +575,82 @@ def get_thumbnails(request,id): return JSONResponse(charts) +@login_required() +def deactivate_user(request): + pk = request.user.id + user = User.objects.get(pk=pk) + user_form = DeactivateUserForm(instance=user) + if request.user.is_authenticated() and request.user.id == user.id: + if request.method == "POST": + user_form = DeactivateUserForm(request.POST, instance=user) + if user_form.is_valid(): + deactivate_user = user_form.save(commit=False) + user.is_active = False + deactivate_user.save() + url = reverse(auth_views.logout_then_login) + return HttpResponseRedirect(url) + return render(request, "userprofile_deactivate.html", { + "user_form": user_form, + }) + else: + raise PermissionDenied + +@login_required() +def user_gdpr_optin(request): + r = getrower(request.user) + r.gdproptin = False + r.gdproptindate = None + r.save() + nexturl = request.GET.get('next','/rowers/list-workouts/') + if r.gdproptin: + return HttpResponseRedirect(nexturl) + + return render(request,'gdpr_optin.html',{ + "next": nexturl + }) + +@login_required() +def user_gdpr_confirm(request): + r = getrower(request.user) + r.gdproptin = True + r.gdproptindate = timezone.now() + r.save() + + nexturl = request.GET.get('next','/rowers/list-workouts/') + + return HttpResponseRedirect(nexturl) + + + +@login_required() +def remove_user(request): + pk = request.user.id + user = User.objects.get(pk=pk) + user_form = DeleteUserForm(instance=user) + if request.user.is_authenticated() and request.user.id == user.id: + if request.method == "POST": + user_form = DeleteUserForm(request.POST,instance=user) + if user_form.is_valid(): + cd = user_form.cleaned_data + name = user.first_name+' '+user.last_name + email = user.email + + + if cd['delete_user']: + user.delete() + res = myqueue(queuehigh, + handle_sendemail_userdeleted, + name, email) + + url = reverse(auth_views.logout_then_login) + return HttpResponseRedirect(url) + return render(request, "userprofile_delete.html", { + "user_form": user_form, + }) + else: + raise PermissionDenied + + @login_required() def get_testscript(request,id): row = get_workout_permitted(request.user,id) @@ -1704,6 +1783,37 @@ def workout_gpxemail_view(request,id=0): return response +# Get Workout summary CSV file +@login_required() +def workouts_summaries_email_view(request): + r = getrower(request.user) + + if request.method == 'POST': + form = DateRangeForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + filename = 'rowsandall_workouts_{first}_{last}.csv'.format( + first=startdate, + last=enddate + ) + df = dataprep.workout_summary_to_df(r,startdate=startdate,enddate=enddate) + df.to_csv(filename,encoding='utf-8') + res = myqueue(queuehigh,handle_sendemailsummary, + r.user.first_name, + r.user.last_name, + r.user.email, + filename) + messages.info(request,'The summary CSV file was sent to you per email') + else: + form = DateRangeForm() + + return render(request,"export_workouts.html", + { + 'form':form + }) + + # Get Workout CSV file and send it to user's email address @login_required() def workout_csvemail_view(request,id=0): @@ -1867,7 +1977,6 @@ def workout_strava_upload_view(request,id=0): try: os.remove(tcxfile) except WindowsError: - print tcxfile pass url = "/rowers/workout/"+str(w.id)+"/edit" @@ -3720,7 +3829,6 @@ def rankings_view2(request,theuser=0, sex = r.sex, weightcategory = r.weightcategory) - print len(agerecords),'aap' if len(agerecords) == 0: recalc = True wcpower = [] @@ -7461,7 +7569,7 @@ def workout_stats_view(request,id=0,message="",successmessage=""): if datadf.empty: return HttpResponse("CSV data file not found") - datadf['deltat'] = datadf['time'].diff() + #datadf['deltat'] = datadf['time'].diff() workoutstateswork = [1,4,5,8,9,6,7] @@ -8134,8 +8242,6 @@ def workout_flexchart3_view(request,*args,**kwargs): yparam1 = yparam1.replace('/','_slsh_') yparam2 = yparam2.replace('/','_slsh_') - print xparam,yparam1,yparam2,'aap' - try: extrametrics.pop('originalvelo') except KeyError: @@ -10866,8 +10972,16 @@ def rower_exportsettings_view(request): # Page where user can set his details # Add email address to form so user can change his email address @login_required() -def rower_edit_view(request,message=""): - r = getrower(request.user) +def rower_edit_view(request,rowerid=0,message=""): + if rowerid==0: + r = getrower(request.user) + else: + r = Rower.objects.get(id=rowerid) + if not checkaccessuser(request.user,r): + raise PermissionDenied("You have no access to these user settings") + + rowerid = r.id + if request.method == 'POST' and "ut2" in request.POST: form = RowerForm(request.POST) if form.is_valid(): @@ -10881,7 +10995,6 @@ def rower_edit_view(request,message=""): an = cd['an'] rest = cd['rest'] try: - r = getrower(request.user) r.max = max(min(hrmax,250),10) r.ut2 = max(min(ut2,250),10) r.ut1 = max(min(ut1,250),10) @@ -10896,7 +11009,7 @@ def rower_edit_view(request,message=""): powerform = RowerPowerForm(instance=r) powerzonesform = RowerPowerZonesForm(instance=r) accountform = AccountRowerForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) return render(request, 'rower_form.html', {'form':form, 'powerzonesform':powerzonesform, @@ -10916,7 +11029,7 @@ def rower_edit_view(request,message=""): #form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) powerzonesform = RowerPowerZonesForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) accountform = AccountRowerForm(instance=r) return render(request, 'rower_form.html', {'form':form, @@ -10938,7 +11051,6 @@ def rower_edit_view(request,message=""): ftp = cd['ftp'] otwslack = cd['otwslack'] try: - r = getrower(request.user) powerfrac = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, @@ -10954,7 +11066,10 @@ def rower_edit_view(request,message=""): r.save() message = "FTP and/or OTW slack values changed." messages.info(request,message) - url = reverse(rower_edit_view) + url = reverse(rower_edit_view, + kwargs = { + 'rowerid':r.id, + }) response = HttpResponseRedirect(url) except Rower.DoesNotExist: message = "Funny. This user doesn't exist." @@ -10966,7 +11081,7 @@ def rower_edit_view(request,message=""): form = RowerForm(instance=r) #powerform = RowerPowerForm(instance=r) powerzonesform = RowerPowerZonesForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) accountform = AccountRowerForm(instance=r) return render(request, 'rower_form.html', {'form':form, @@ -10996,7 +11111,6 @@ def rower_edit_view(request,message=""): anname = cd['anname'] powerzones = [ut3name,ut2name,ut1name,atname,trname,anname] try: - r = getrower(request.user) r.pw_ut2 = pw_ut2 r.pw_ut1 = pw_ut1 r.pw_at = pw_at @@ -11008,7 +11122,7 @@ def rower_edit_view(request,message=""): messages.info(request,successmessage) form = RowerForm(instance=r) accountform = AccountRowerForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) powerform = RowerPowerForm(instance=r) powerzonesform = RowerPowerZonesForm(instance=r) return render(request, 'rower_form.html', @@ -11030,7 +11144,7 @@ def rower_edit_view(request,message=""): form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) accountform = AccountRowerForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) #powerzonesform = RowerPowerZonesForm(instance=r) message = HttpResponse("invalid form") return render(request, 'rower_form.html', @@ -11044,7 +11158,7 @@ def rower_edit_view(request,message=""): }) elif request.method == 'POST' and "weightcategory" in request.POST: accountform = AccountRowerForm(request.POST) - userform = UserForm(request.POST,instance=request.user) + userform = UserForm(request.POST,instance=r.user) if accountform.is_valid() and userform.is_valid(): # process cd = accountform.cleaned_data @@ -11059,7 +11173,7 @@ def rower_edit_view(request,message=""): showfavoritechartnotes = cd['showfavoritechartnotes'] getemailnotifications = cd['getemailnotifications'] defaulttimezone=cd['defaulttimezone'] - u = request.user + u = r.user if len(first_name): u.first_name = first_name u.last_name = last_name @@ -11068,7 +11182,6 @@ def rower_edit_view(request,message=""): u.save() - r = getrower(u) r.defaulttimezone=defaulttimezone r.weightcategory = weightcategory r.getemailnotifications = getemailnotifications @@ -11110,13 +11223,11 @@ def rower_edit_view(request,message=""): else: try: - r = getrower(request.user) - form = RowerForm(instance=r) powerform = RowerPowerForm(instance=r) powerzonesform = RowerPowerZonesForm(instance=r) accountform = AccountRowerForm(instance=r) - userform = UserForm(instance=request.user) + userform = UserForm(instance=r.user) grants = AccessToken.objects.filter(user=request.user) return render(request, 'rower_form.html', { diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 052f2af4..86f3f1c4 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -94,6 +94,7 @@ MIDDLEWARE_CLASSES = [ 'async_messages.middleware.AsyncMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'tz_detect.middleware.TimezoneMiddleware', + 'rowers.middleware.GDPRMiddleWare', 'rowers.middleware.PowerTimeFitnessMetricMiddleWare', ] @@ -118,7 +119,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.i18n', - 'context_processors.google_analytics', +# 'context_processors.google_analytics', 'context_processors.warning_message', ], # 'loaders': [ diff --git a/templates/basebase.html b/templates/basebase.html index 3ad39b0b..6aaaac8a 100644 --- a/templates/basebase.html +++ b/templates/basebase.html @@ -26,9 +26,7 @@ {% analytical_head_top %} - {% if GOOGLE_ANALYTICS_PROPERTY_ID %} - {% include "ga.html" %} - {% endif %} +