Private
Public Access
1
0

Merge branch 'feature/teammessages' into develop

This commit is contained in:
Sander Roosendaal
2017-02-17 16:36:41 +01:00
10 changed files with 576 additions and 8 deletions

View File

@@ -4,7 +4,8 @@ from django.contrib.auth.models import User
from .models import (
Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement,
Team,TeamInvite,TeamRequest
Team,TeamInvite,TeamRequest,
WorkoutComment,
)
# Register your models here so you can use them in the Admin module
@@ -36,6 +37,9 @@ class TeamInviteAdmin(admin.ModelAdmin):
class TeamRequestAdmin(admin.ModelAdmin):
list_display = ('issuedate','team','user','code')
class WorkoutCommentAdmin(admin.ModelAdmin):
list_display = ('created','user','workout')
admin.site.unregister(User)
admin.site.register(User,UserAdmin)
@@ -46,3 +50,4 @@ admin.site.register(FavoriteChart,FavoriteChartAdmin)
admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin)
admin.site.register(TeamInvite,TeamInviteAdmin)
admin.site.register(TeamRequest,TeamRequestAdmin)
admin.site.register(WorkoutComment,WorkoutCommentAdmin)

View File

@@ -855,6 +855,7 @@ class RowerForm(ModelForm):
if an>=max:
raise forms.ValidationError("AN should be lower than Max")
# An announcement that goes to the right of the workouts list
# optionally sends a tweet to our twitter account
class SiteAnnouncement(models.Model):
@@ -878,3 +879,28 @@ class SiteAnnouncement(models.Model):
except:
pass
return super(SiteAnnouncement,self).save(*args, **kwargs)
# A comment by a user on a training
class WorkoutComment(models.Model):
comment = models.TextField(max_length=300)
created = models.DateTimeField(default=timezone.now)
read = models.BooleanField(default=False)
user = models.ForeignKey(User)
workout = models.ForeignKey(Workout)
def __unicode__(self):
return u'Comment to: {w} by {u1} {u2}'.format(
w=self.workout.name,
u1 = self.user.first_name,
u2 = self.user.last_name,
)
class WorkoutCommentForm(ModelForm):
class Meta:
model = WorkoutComment
fields = ['comment',]
widgets = {
'comment': forms.Textarea,
}

View File

@@ -273,6 +273,35 @@ def handle_sendemail_invite(email,name,code,teamname,manager):
return 1
@app.task
def handle_sendemailnewcomment(first_name,
last_name,
email,
commenter_first_name,
commenter_last_name,
comment,workoutname,
workoutid):
fullemail = first_name+' '+last_name+' <'+email+'>'
subject = 'New comment on workout '+workoutname
message = 'Dear '+first_name+',\n\n'
message += commenter_first_name+' '+commenter_last_name
message += ' has written a new comment on your workout '
message += workoutname+'\n\n'
message += comment
message += '\n\n'
message += 'You can read the comment here:\n'
message += 'https://rowsandall.com/rowers/workout/'+str(workoutid)+'/comment'
email = EmailMessage(subject, message,
'Rowsandall <info@rowsandall.com>',
[fullemail])
res = email.send()
return 1
@app.task
def handle_sendemail_request(email,name,code,teamname,requestor,id):
fullemail = name+' <'+email+'>'

View File

@@ -5,6 +5,12 @@
{% block title %}Workouts{% endblock %}
{% block content %}
<script>
setTimeout("location.reload(true);",60000);
</script>
<div class="grid_12">
Select start and end date for a date range:
@@ -27,7 +33,7 @@
</div>
<div class="grid_8 alpha">
<div id="workouts_table" class="grid_8 alpha">
{% if team %}
<h3>{{ team.name }} Team Workouts</h3>
{% else %}

View File

@@ -0,0 +1,183 @@
{% extends "base.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% load tz %}
{% block title %}Change Workout {% endblock %}
{% block content %}
<div id="left" class="grid_6 alpha">
<div id="summary" class="grid_6 alpha">
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<h1>Comments {{ workout.name }}</h1>
{% localtime on %}
<table width=100%>
<tr>
<th>Rower:</th><td>{{ first_name }} {{ last_name }}</td>
</tr><tr>
<th>Date/Time:</th><td>{{ workout.startdatetime }}</td>
</tr><tr>
<th>Distance:</th><td>{{ workout.distance }}m</td>
</tr><tr>
<th>Duration:</th><td>{{ workout.duration |durationprint:"%H:%M:%S.%f" }}</td>
</tr><tr>
<th>Public link to this workout</th>
<td>
<a href="/rowers/workout/{{ workout.id }}">https://rowsandall.com/rowers/workout/{{ workout.id }}</a>
<td>
</tr><tr>
<th>Public link to interactive chart</th>
<td>
<a href="/rowers/workout/{{ workout.id }}/interactiveplot">https://rowsandall.com/rowers/workout/{{ workout.id }}/interactiveplot</a>
<td>
</tr>
</table>
{% endlocaltime %}
</div>
{% for c in comments %}
<div class="grid_6 alpha">
<div class="grid_2 alpha">
{{ c.created }}
<b>{{ c.user.first_name }} {{ c.user.last_name }}</b>
</div>
<div class="grid_4 omega">
<div class="talk-bubble tri-right left-top">
<div class="talktext">
{{ c.comment }}
</div>
</div>
</div>
</div>
{% endfor %}
<div id="form" class="grid_6 alpha">
<form enctype="multipart/form-data" action="" method="post">
<table width=100%>
{{ form.as_table }}
</table>
{% csrf_token %}
<div id="formbutton" class="grid_1 prefix_4 suffix_1 omega">
<input class="button green" type="submit" value="Save">
</div>
</form>
</div>
</div>
<div id="images" class="grid_6 omega">
<h1>Images linked to this workout</h1>
{% if graphs1 %}
{% for graph in graphs1 %}
{% if forloop.counter == 1 %}
<div id="thumb-container" class="grid_2 alpha">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% elif forloop.counter == 3 %}
<div id="thumb-container" class="grid_2 omega">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% else %}
<div id="thumb-container" class="grid_2">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% endif %}
{% endfor %}
{% for graph in graphs2 %}
{% if forloop.counter == 1 %}
<div id="thumb-container" class="grid_2 alpha">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% elif forloop.counter == 3 %}
<div id="thumb-container" class="grid_2 omega">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% else %}
<div id="thumb-container" class="grid_2">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/waiting.png'"
alt="{{ graph.filename }}" width="120" height="100"></a>
</div>
{% endif %}
{% endfor %}
{% else %}
<p> No graphs found </p>
{% endif %}
</div>
<style>
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
html, body {height: 100%; margin:5px;}
</style>
<div id="summary" class="grid_6 omega">
<h1>Workout Summary</h1>
<p>
<pre>
{{ workout.summary }}
</pre>
</p>
</div>
<div id="interactiveplot" class="grid_6 omega">
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
{{ gmscript |safe }}
<style>
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
html, body, #map_canvas {
height: 100%;
width: 100%;
margin:0;
padding: 0;
}
#map_canvas_container {position: relative; top:0; right:0; bottom:0; left:0;}
#map_canvas {position: relative; top: 0; right: 0; bottom: 0; left: 0;}
</style>
<div id="map_canvas">
{{ gmdiv|safe }}
</div>
</div>
{% endblock %}

View File

@@ -14,7 +14,7 @@
</p>
{% endif %}
<h1>Edit Workout Data</h1>
<h1>Edit Workout {{ workout.name }}</h1>
<div class="grid_6 alpha">
<div class="grid_2 alpha">
<p>
@@ -54,8 +54,14 @@
</tr><tr>
<th>Public link to this workout</th>
<td>
<a href="/rowers/workout/{{ workout.id }}">https://rowsandall.com/rowers/workout/{{ workout.id }}</a>
<td>
<a href="/rowers/workout/{{ workout.id }}">https://rowsandall.com/rowers/workout/{{ workout.id }}</a>
</td>
</tr><tr>
<th>Comments</th>
<td>
<a href="/rowers/workout/{{ workout.id }}/comment">Comment</a>
</td>
</tr><tr>
<th>Public link to interactive chart</th>
<td>

View File

@@ -8,7 +8,7 @@
<div id="workouts" class="grid_6 alpha">
<h1>Workout Data</h1>
<h1>{{ workout.name }}</h1>
<table width=100%>
<tr>
<th>Rower:</th><td>{{ first_name }} {{ last_name }}</td>
@@ -29,6 +29,12 @@
</tr><tr>
<th>Weight Category:</th><td>{{ workout.weightcategory }}</td>
</tr>
<tr>
<th>Comments</th>
<td>
<a href="/rowers/workout/{{ workout.id }}/comment">Comment</a>
</td>
</tr>
</table>
<h1>Workout Summary</h1>

View File

@@ -148,6 +148,7 @@ urlpatterns = [
url(r'^workout/(?P<id>\d+)/export/c/(?P<message>\w+.*)$',views.workout_export_view),
url(r'^workout/(?P<id>\d+)/export/s/(?P<successmessage>\w+.*)$',views.workout_export_view),
url(r'^workout/(?P<id>\d+)/export$',views.workout_export_view),
url(r'^workout/(?P<id>\d+)/comment$',views.workout_comment_view),
url(r'^workout/(\d+)/emailtcx$',views.workout_tcxemail_view),
url(r'^workout/(\d+)/emailcsv$',views.workout_csvemail_view),
url(r'^workout/compare/(\d+)/$',views.workout_comparison_list),

View File

@@ -32,7 +32,8 @@ from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart
from rowers.models import (
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData,
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
WorkoutComment,WorkoutCommentForm
)
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
from django.forms.formsets import formset_factory
@@ -58,7 +59,9 @@ from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rowers.rows import handle_uploaded_file
from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv
from rowers.tasks import handle_sendemail_unrecognized
from rowers.tasks import (
handle_sendemail_unrecognized,handle_sendemailnewcomment
)
from scipy.signal import savgol_filter
from django.shortcuts import render_to_response
@@ -3314,6 +3317,55 @@ def workout_export_view(request,id=0, message="", successmessage=""):
'message':message,
'successmessage':successmessage,
})
# list of comments to a workout
@login_required()
def workout_comment_view(request,id=0):
try:
w = Workout.objects.get(id=id)
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if w.privacy == 'private' and w.user.user != request.user:
return HttpResponseForbidden("Permission error")
# ok we're permitted
if request.method == 'POST':
r = w.user
form = WorkoutCommentForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
comment = cd['comment']
c = WorkoutComment(workout=w,user=request.user,comment=comment)
c.save()
if settings.DEBUG:
res = handle_sendemailnewcomment.delay(r.user.first_name,
r.user.last_name,
r.user.email,
c.user.first_name,
c.user.last_name,
comment,w.name,
w.id)
else:
res = queuehigh.enqueue(handle_sendemailnewcomment,r.user.first_name,
r.user.last_name,
r.user.email,
r.user.first_name,
r.user.last_name,
comment,w.name,w.id)
comments = WorkoutComment.objects.filter(workout=w).order_by("created")
form = WorkoutCommentForm()
return render(request,
'workout_comments.html',
{'workout':w,
'comments':comments,
'form':form,
})
# The basic edit page
@login_required()

View File

@@ -554,3 +554,257 @@ a.wh:hover {
.bk-canvas-map {
overflow: hidden;
}
/* container */
.container {
padding: 5% 5%;
}
/* CSS talk bubble */
.talk-bubble {
margin: 40px;
display: inline-block;
position: relative;
width: 200px;
height: auto;
background-color: lightyellow;
}
.border{
border: 8px solid #666;
}
.round{
border-radius: 30px;
-webkit-border-radius: 30px;
-moz-border-radius: 30px;
}
/* Right triangle placed top left flush. */
.tri-right.border.left-top:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: -40px;
right: auto;
top: -8px;
bottom: auto;
border: 32px solid;
border-color: #666 transparent transparent transparent;
}
.tri-right.left-top:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: -20px;
right: auto;
top: 0px;
bottom: auto;
border: 22px solid;
border-color: lightyellow transparent transparent transparent;
}
/* Right triangle, left side slightly down */
.tri-right.border.left-in:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: -40px;
right: auto;
top: 30px;
bottom: auto;
border: 20px solid;
border-color: #666 #666 transparent transparent;
}
.tri-right.left-in:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: -20px;
right: auto;
top: 38px;
bottom: auto;
border: 12px solid;
border-color: lightyellow lightyellow transparent transparent;
}
/*Right triangle, placed bottom left side slightly in*/
.tri-right.border.btm-left:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: -8px;
right: auto;
top: auto;
bottom: -40px;
border: 32px solid;
border-color: transparent transparent transparent #666;
}
.tri-right.btm-left:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: 0px;
right: auto;
top: auto;
bottom: -20px;
border: 22px solid;
border-color: transparent transparent transparent lightyellow;
}
/*Right triangle, placed bottom left side slightly in*/
.tri-right.border.btm-left-in:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: 30px;
right: auto;
top: auto;
bottom: -40px;
border: 20px solid;
border-color: #666 transparent transparent #666;
}
.tri-right.btm-left-in:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: 38px;
right: auto;
top: auto;
bottom: -20px;
border: 12px solid;
border-color: lightyellow transparent transparent lightyellow;
}
/*Right triangle, placed bottom right side slightly in*/
.tri-right.border.btm-right-in:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: 30px;
bottom: -40px;
border: 20px solid;
border-color: #666 #666 transparent transparent;
}
.tri-right.btm-right-in:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: 38px;
bottom: -20px;
border: 12px solid;
border-color: lightyellow lightyellow transparent transparent;
}
/*
left: -8px;
right: auto;
top: auto;
bottom: -40px;
border: 32px solid;
border-color: transparent transparent transparent #666;
left: 0px;
right: auto;
top: auto;
bottom: -20px;
border: 22px solid;
border-color: transparent transparent transparent lightyellow;
/*Right triangle, placed bottom right side slightly in*/
.tri-right.border.btm-right:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: -8px;
bottom: -40px;
border: 20px solid;
border-color: #666 #666 transparent transparent;
}
.tri-right.btm-right:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: 0px;
bottom: -20px;
border: 12px solid;
border-color: lightyellow lightyellow transparent transparent;
}
/* Right triangle, right side slightly down*/
.tri-right.border.right-in:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: -40px;
top: 30px;
bottom: auto;
border: 20px solid;
border-color: #666 transparent transparent #666;
}
.tri-right.right-in:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: -20px;
top: 38px;
bottom: auto;
border: 12px solid;
border-color: lightyellow transparent transparent lightyellow;
}
/* Right triangle placed top right flush. */
.tri-right.border.right-top:before {
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: -40px;
top: -8px;
bottom: auto;
border: 32px solid;
border-color: #666 transparent transparent transparent;
}
.tri-right.right-top:after{
content: ' ';
position: absolute;
width: 0;
height: 0;
left: auto;
right: -20px;
top: 0px;
bottom: auto;
border: 20px solid;
border-color: lightyellow transparent transparent transparent;
}
/* talk bubble contents */
.talktext{
padding: 1em;
text-align: left;
line-height: 1.5em;
}
.talktext p{
/* remove webkit p margins */
-webkit-margin-before: 0em;
-webkit-margin-after: 0em;
}