Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Merge pull request #3923 from gratipay/team-edit
Browse files Browse the repository at this point in the history
Edit teams.
  • Loading branch information
mattbk committed Mar 2, 2016
2 parents 1b5ce10 + 0f5d63a commit 3d78e56
Show file tree
Hide file tree
Showing 10 changed files with 3,861 additions and 9 deletions.
27 changes: 27 additions & 0 deletions gratipay/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ def insert(cls, owner, **fields):
""", fields)


def update(self, **kw):
updateable = frozenset(['name', 'product_or_service', 'homepage',
'onboarding_url', 'todo_url'])

cols, vals = zip(*kw.items())
assert set(cols).issubset(updateable)

old_value = {}
for col in cols:
old_value[col] = getattr(self, col)

cols = ', '.join(cols)
placeholders = ', '.join(['%s']*len(vals))

with self.db.get_cursor() as c:
c.run("""
UPDATE teams
SET ({0}) = ({1})
WHERE id = %s
""".format(cols, placeholders), vals+(self.id,)
)
add_event(c, 'team', dict( action='update'
, id=self.id
, **old_value
))
self.set_attributes(**kw)

def create_github_review_issue(self):
"""POST to GitHub, and return the URL of the new issue.
"""
Expand Down
18 changes: 18 additions & 0 deletions gratipay/utils/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import zipfile
from cStringIO import StringIO

import requests

def imgize(image, image_type):
large = None
small = None
crops = requests.post( 'http://gip.rocks/v1',
data=image,
headers={'Content-Type': image_type}
)
if crops.status_code == 200:
zf = zipfile.ZipFile(StringIO(crops.content))
large = zf.open('160').read()
small = zf.open('48').read()

return crops.status_code, large, small
30 changes: 30 additions & 0 deletions js/gratipay/edit_team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Gratipay.edit_team = {}

Gratipay.edit_team.initForm = function() {
$form = $("#edit-team");
$buttons = $form.find("button"); // submit and cancel btns
$form.submit(Gratipay.edit_team.submitForm);
}

Gratipay.edit_team.submitForm = function(e) {
e.preventDefault();

var data = new FormData($form[0]);

$buttons.prop("disabled", true);
$.ajax({
url: $form.attr("action"),
type: $form.attr("method"),
data: data,
dataType: 'json',
processData: false,
contentType: false,
success: function(d) {
Gratipay.notification("Successfully edited team.", 'success');
setTimeout(function() {
window.location.href = "../";
}, 1000);
},
error: [Gratipay.error, function () { $buttons.prop("disabled", false); }]
});
}
3,159 changes: 3,159 additions & 0 deletions tests/py/fixtures/TestTeamEdit.yml

Large diffs are not rendered by default.

420 changes: 420 additions & 0 deletions tests/py/test_team_edit.py

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions tests/py/test_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,49 @@ def test_image_endpoint_serves_an_image(self):
team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
image = self.client.GET('/TheEnterprise/image').body # buffer
assert str(image) == IMAGE


# Update
# ======

def test_update_works(self):
team = self.make_team(slug='enterprise')
update_data = {
'name': 'Enterprise',
'product_or_service': 'We save galaxies.',
'homepage': 'http://starwars-enterprise.com/',
'onboarding_url': 'http://starwars-enterprise.com/onboarding',
'todo_url': 'http://starwars-enterprise.com/todos',
}
team.update(**update_data)
team = Team.from_slug('enterprise')
for field in update_data:
assert getattr(team, field) == update_data[field]

def test_can_only_update_allowed_fields(self):
allowed_fields = set(['name', 'product_or_service', 'homepage',
'onboarding_url', 'todo_url'])

team = self.make_team(slug='enterprise')

fields = vars(team).keys()
for field in fields:
if field not in allowed_fields:
with pytest.raises(AssertionError):
team.update(field='foo')

def test_update_records_the_old_values_as_events(self):
team = self.make_team(slug='enterprise', product_or_service='Product')
team.update(name='Enterprise', product_or_service='We save galaxies.')
event = self.db.one('SELECT * FROM events')
assert event.payload == { 'action': 'update'
, 'id': team.id
, 'name': 'The Enterprise'
, 'product_or_service': 'Product'
}

def test_update_updates_object_attributes(self):
team = self.make_team(slug='enterprise')
team.update(name='Enterprise', product_or_service='We save galaxies.')
assert team.name == 'Enterprise'
assert team.product_or_service == 'We save galaxies.'
75 changes: 75 additions & 0 deletions www/%team/edit/edit.json.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from aspen import Response

from gratipay.utils import get_team
from gratipay.utils.images import imgize

[---]

def valid_url(url):
return any(map(url.lower().startswith, ('http://', 'https://')))

request.allow('POST')

field_names = {
'name': 'Team Name',
'image': 'Image',
'product_or_service': 'Product or Service',
'homepage': 'Homepage',
'onboarding_url': 'Self-onboarding Documentation URL',
'todo_url': 'To-do URL',
}

if user.ANON:
raise Response(401, _("You need to log in to access this page."))

team = get_team(state)

if team.is_closed:
raise Response(403, _("You can't edit a closed team."))

if team.is_approved is False: # for teams under review, is_approved is None.
raise Response(403, _("You can't edit a rejected team."))

if not user.ADMIN and user.participant.username != team.owner:
raise Response(403, _("You are not authorized to access this page."))

data = request.body
fields = {}
image = None

for field in data.keys():
if field in field_names:
if field == 'image':
image = data[field].value # file upload
image_type = data[field].type
if image and image_type not in ('image/png', 'image/jpeg'):
raise Response(400, _("Please upload a PNG or JPG image."))
else:
value = data.get(field, '').strip(' ')
if not value:
raise Response(400, _("Please fill out the '{}' field.", field_names[field]))

if (field in ('homepage', 'onboarding_url', 'todo_url')
and not valid_url(value)):
raise Response(400,
_( "Please enter an http[s]:// URL for the '{}' field."
, field_names[field]
))
fields[field] = value

if fields:
team.update(**fields)

if image:
code, large, small = imgize(image, image_type)
if code == 200:
team.save_image(image, large, small, image_type)
elif code == 413:
raise Response(400, _("Please upload an image smaller than 100 kB."))
elif code == 415:
raise Response(400, _("Please upload a PNG or JPG image."))
else:
raise Response(500, _("Sorry, there was a problem saving your image. Please try again."))

[---] application/json via json_dump
team.to_dict()
72 changes: 72 additions & 0 deletions www/%team/edit/index.html.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from aspen import Response

from gratipay.utils import get_team

[---]
request.allow('GET')

if user.ANON:
raise Response(401, _("You need to log in to access this page."))

team = get_team(state)

if team.is_closed:
raise Response(403, _("You can't edit a closed team."))

if team.is_approved is False: # for teams under review, is_approved is None.
raise Response(403, _("You can't edit a rejected team."))

if not user.ADMIN and user.participant.username != team.owner:
raise Response(403, _("You are not authorized to access this page."))

title = _("Edit your team")
banner = _("Edit")
suppress_sidebar = True

[---] text/html
{% extends "templates/base.html" %}

{% block scripts %}
<script>$(document).ready(Gratipay.edit_team.initForm);</script>
{% endblock %}

{% block content %}
<style>
textarea {
width: 100%;
height: 200px;
}
input[type="file"] {
margin-left: 10px;
}
</style>

<form action="edit.json" method="POST" id = "edit-team">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">

<label><h2>{{ _("Team Name") }}</h2></label>
<input type="text" name="name" value="{{team.name}}" required autofocus>

<label><h2>{{ _("Image") }}</h2></label>
<img src="{{ team.get_image_url('small') }}" align="middle">
<input type="file" name="image" accept="image/png, image/jpeg">

<label><h2>{{ _("Product or Service") }}</h2></label>
<textarea name="product_or_service" required>{{team.product_or_service}}</textarea>

<label><h2>{{ _("Homepage") }}</h2></label>
<input type="text" name="homepage" value="{{team.homepage}}" required>

<label><h2>{{ _("Self-onboarding Documentation URL") }}</h2></label>
<input type="text" name="onboarding_url" value="{{team.onboarding_url}}" required>

<label><h2>{{ _("To-do URL") }}</h2></label>
<input type="text" name="todo_url" value="{{team.todo_url}}" required>

<br>
<br>
<button type="submit">{{ _("Modify") }}</button>
<button onclick="window.location='../';return false;">{{ _("Cancel") }}</button>

</form>
{% endblock %}
7 changes: 7 additions & 0 deletions www/%team/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,18 @@ suppress_sidebar = not(team.is_approved or user.ADMIN)
| <a href="{{ team.todo_url }}">{{ _("To-do") }}</a>
{% endif %}

{% if user.ADMIN or (not user.ANON and team.owner == user.participant.username) %}
|{{ _( "{a} Edit team {_a}"
, a='<a href="./edit">'|safe
, _a='</a>'|safe
) }}
{% else %}
| {{ _( "owned by {a}~{owner}{_a}"
, a=('<a href="/~{}/">'.format(team.owner))|safe
, owner=team.owner
, _a='</a>'|safe
) }}
{% endif %}

</p>

Expand Down
16 changes: 7 additions & 9 deletions www/teams/create.json.spt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ from aspen import Response

from gratipay.models.community import slugize
from gratipay.models.team import Team
from gratipay.utils.images import imgize

from psycopg2 import IntegrityError
[---]
request.allow('POST')
Expand Down Expand Up @@ -72,16 +74,12 @@ if request.method == 'POST':
except IntegrityError:
raise Response(400, _("Sorry, there is already a team using '{}'.", fields['slug']))

crops = requests.post('http://gip.rocks/v1', data=image, headers={'Content-Type': image_type})
if crops.status_code == 200:
zf = zipfile.ZipFile(StringIO(crops.content))
original = image
large = zf.open('160').read()
small = zf.open('48').read()
team.save_image(original, large, small, image_type)
elif crops.status_code == 413:
code, large, small = imgize(image, image_type)
if code == 200:
team.save_image(image, large, small, image_type)
elif code == 413:
raise Response(400, _("Please upload an image smaller than 100 kB."))
elif crops.status_code == 415:
elif code == 415:
raise Response(400, _("Please upload a PNG or JPG image."))
else:
raise Response(500, _("Sorry, there was a problem saving your image. Please try again."))
Expand Down

0 comments on commit 3d78e56

Please sign in to comment.