Skip to content

Commit

Permalink
Merge pull request #907 from akvo/feature/884_v3_registration
Browse files Browse the repository at this point in the history
[#823 #884 #905] Approve users and updated registration process
  • Loading branch information
kardan committed Nov 13, 2014
2 parents 65a75dd + 56d49e0 commit b87463d
Show file tree
Hide file tree
Showing 18 changed files with 283 additions and 127 deletions.
1 change: 1 addition & 0 deletions akvo/rest/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
urlpatterns = patterns(
'',
url(r'^', include(router.urls)),
url(r'^employment/(?P<pk>[0-9]+)/approve/$', views.approve_employment, name='approve_employment'),
url(r'^user/(?P<pk>[0-9]+)/change_password/$', views.change_password, name='user_change_password'),
url(r'^user/(?P<pk>[0-9]+)/update_details/$', views.update_details, name='user_update_details'),
url(r'^user/(?P<pk>[0-9]+)/request_organisation/$', views.request_organisation, name='user_request_organisation'),
Expand Down
3 changes: 2 additions & 1 deletion akvo/rest/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .budget_item_label import BudgetItemLabelViewSet
from .category import CategoryViewSet
from .country import CountryViewSet
from .employment import EmploymentViewSet
from .employment import EmploymentViewSet, approve_employment
from .focus_area import FocusAreaViewSet
from .goal import GoalViewSet
from .internal_organisation_id import InternalOrganisationIDViewSet
Expand All @@ -38,6 +38,7 @@
'CategoryViewSet',
'CountryViewSet',
'EmploymentViewSet',
'approve_employment',
'FocusAreaViewSet',
'GoalViewSet',
'InternalOrganisationIDViewSet',
Expand Down
21 changes: 21 additions & 0 deletions akvo/rest/views/employment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.

from rest_framework import filters
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from akvo.rsr.models import Employment

Expand All @@ -20,3 +25,19 @@ class EmploymentViewSet(BaseRSRViewSet):
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('user', 'organisation',)

@api_view(['POST'])
@permission_classes((IsAuthenticated, ))
def approve_employment(request, pk=None):
employment = Employment.objects.get(pk=pk)
user = request.user

# Only superusers, staff members, editors and Organisation admins are allowed to approve a user
if not (user.is_superuser or user.is_staff or user.get_is_rsr_admin() or user.get_is_org_admin()):
raise PermissionDenied
if user.get_is_org_admin() and not employment.organisation in user.organisations.all():
raise PermissionDenied

employment.is_approved = True
employment.save()

return Response({'status': 'employment approved'})
3 changes: 2 additions & 1 deletion akvo/rsr/models/employment.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Meta:
def __unicode__(self):
return self.user.first_name, self.user.last_name, ":", self.organisation.name

def to_dict(self):
def to_dict(self, org_list):
country = '' if not self.country else model_to_dict(self.country)

return dict(
Expand All @@ -39,4 +39,5 @@ def to_dict(self):
is_approved=self.is_approved,
job_title=self.job_title,
country_full=country,
actions=True if self.organisation in org_list else False,
)
17 changes: 14 additions & 3 deletions akvo/rsr/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ def get_organisation_names(self):
return "\n".join([o.name for o in self.organisations.all()])
get_organisation_names.short_description = _(u'organisations')

def approved_organisations(self):
"""
return all approved organisations of the user
"""
from .organisation import Organisation
return Organisation.objects.filter(employees__user=self, employees__is_approved=True)

def updates(self):
"""
return all updates created by the user
Expand Down Expand Up @@ -254,13 +261,17 @@ def user(self):
# Support for self as profile. Use of this is deprecated
return self

def employments_dict(self):
"""Represent User as dict with employments"""
def employments_dict(self, org_list):
"""
Represent User as dict with employments.
The org_list is a list of organisations of the original user. Based on this, the original user will have
the option to approve / delete the employment.
"""
employments = Employment.objects.filter(user=self)

employments_array = []
for employment in employments:
employment_obj = employment.to_dict()
employment_obj = employment.to_dict(org_list)
employments_array.append(employment_obj)

return dict(
Expand Down
15 changes: 6 additions & 9 deletions akvo/rsr/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,11 @@ def act_on_log_entry(sender, **kwargs):


def user_organisation_request(sender, **kwargs):
if not getattr(settings, 'REGISTRATION_NOTIFICATION_EMAILS', True):
return

# Check if a new Employment is created
if kwargs['created']:
user = kwargs.get("user", False)
organisation = kwargs.get("organisation", False)
if user and organisation:
employment = kwargs.get("instance", False)
if employment:
user = employment.user
organisation = employment.organisation
users = get_user_model().objects.all()
# find all users that are:
# 1) Superusers
Expand All @@ -205,8 +202,8 @@ def user_organisation_request(sender, **kwargs):
).distinct()
rsr_send_mail_to_users(
notify,
subject='email/user_organisation_request_subject.txt',
message='email/user_organisation_request_message.txt',
subject='registration/user_organisation_request_subject.txt',
message='registration/user_organisation_request_message.txt',
subject_context={'organisation': organisation},
msg_context={'user': user, 'organisation': organisation},
)
Expand Down
17 changes: 9 additions & 8 deletions akvo/rsr/static/rsr/v3/js/src/react-my-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ var Employment = React.createClass({displayName: 'Employment',
},

render: function() {
return this.state.visible
? React.DOM.li(null, this.props.employment.organisation_full.long_name,
" - ",
React.DOM.i(null, this.props.employment.job_title,
" ",this.props.employment.country_full.name
)
)
: React.DOM.span(null);
if (this.props.employment.is_approved) {
return this.state.visible
? React.DOM.li(null, this.props.employment.organisation_full.long_name)
: React.DOM.span(null);
} else {
return this.state.visible
? React.DOM.li(null, this.props.employment.organisation_full.long_name, " ", React.DOM.i(null, "(Not approved)"))
: React.DOM.span(null);
}
}
});

Expand Down
17 changes: 9 additions & 8 deletions akvo/rsr/static/rsr/v3/js/src/react-my-details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ var Employment = React.createClass({
},

render: function() {
return this.state.visible
? <li>{this.props.employment.organisation_full.long_name}
&nbsp;-&nbsp;
<i>{this.props.employment.job_title}
&nbsp;{this.props.employment.country_full.name}
</i>
</li>
: <span/>;
if (this.props.employment.is_approved) {
return this.state.visible
? <li>{this.props.employment.organisation_full.long_name}</li>
: <span/>;
} else {
return this.state.visible
? <li>{this.props.employment.organisation_full.long_name} <i>(Not approved)</i></li>
: <span/>;
}
}
});

Expand Down
103 changes: 89 additions & 14 deletions akvo/rsr/static/rsr/v3/js/src/react-user-management.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ var ModalTrigger = ReactBootstrap.ModalTrigger;
var Button = ReactBootstrap.Button;
var Table = ReactBootstrap.Table;

var ConfirmModal = React.createClass({displayName: 'ConfirmModal',
var DeleteModal = React.createClass({displayName: 'DeleteModal',
deleteEmployment: function() {
$.ajax({
type: "DELETE",
Expand All @@ -25,7 +25,7 @@ var ConfirmModal = React.createClass({displayName: 'ConfirmModal',

render: function() {
return this.transferPropsTo(
Modal( {title:"Remove link to organisation"},
Modal( {title:"Remove user from organisation"},
React.DOM.div( {className:"modal-body"},
'Are you sure you want to remove ' + this.props.employment.user_full.first_name + ' ' + this.props.employment.user_full.last_name + ' from ' + this.props.employment.organisation_full.name + '?'
),
Expand All @@ -38,13 +38,85 @@ var ConfirmModal = React.createClass({displayName: 'ConfirmModal',
}
});

var TriggerConfirmModal = React.createClass({displayName: 'TriggerConfirmModal',
render: function () {
return (
ModalTrigger( {modal:ConfirmModal( {employment:this.props.employment, onDeleteToggle:this.props.onDeleteToggle} )},
Button( {bsStyle:"danger", bsSize:"xsmall"}, "X")
var ApproveModal = React.createClass({displayName: 'ApproveModal',
approveEmployment: function() {
$.ajax({
type: "POST",
url: "/rest/v1/employment/" + this.props.employment.id + '/approve/?format=json',
success: function(data) {
this.handleApprove();
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},

handleApprove: function() {
this.props.onRequestHide();
this.props.onApproveToggle();
},

render: function() {
return this.transferPropsTo(
Modal( {title:"Approve user"},
React.DOM.div( {className:"modal-body"},
'Are you sure you want to approve ' + this.props.employment.user_full.first_name + ' ' + this.props.employment.user_full.last_name + ' at ' + this.props.employment.organisation_full.long_name + '?'
),
React.DOM.div( {className:"modal-footer"},
Button( {onClick:this.props.onRequestHide}, "Close"),
Button( {onClick:this.approveEmployment, bsStyle:"success"}, "Approve")
)
)
);
);
}
});

var TriggerModal = React.createClass({displayName: 'TriggerModal',
getInitialState: function() {
return {
visible: false,
approved: false
};
},

componentDidMount: function() {
var visible = this.props.employment.actions;
var approved = this.props.employment.is_approved;
if (this.isMounted() && this.props.delete) {
this.setState({
visible: visible,
approved: approved
});
} else if (this.isMounted() && !this.props.delete) {
this.setState({
visible: !approved,
approved: approved
});
}
},

onApprove: function() {
this.setState({
visible: false,
approved: true
});
},

render: function () {
if (this.state.visible) {
return this.props.delete
? ModalTrigger( {modal:DeleteModal( {employment:this.props.employment, onDeleteToggle:this.props.onDeleteToggle} )},
Button( {bsStyle:"danger", bsSize:"xsmall"}, "X")
)
: ModalTrigger( {modal:ApproveModal( {employment:this.props.employment, onApproveToggle:this.onApprove} )},
Button( {bsStyle:"success", bsSize:"xsmall"}, "√")
);
} else {
return React.DOM.span(null);
}


}
});

Expand All @@ -59,8 +131,12 @@ var Employment = React.createClass({displayName: 'Employment',

render: function() {
return this.state.visible
? React.DOM.li(null, this.props.employment.organisation_full.long_name, " ", TriggerConfirmModal( {employment:this.props.employment, onDeleteToggle:this.onDelete} ))
: React.DOM.span(null);
? React.DOM.tr(null,
React.DOM.td(null, this.props.employment.organisation_full.long_name),
React.DOM.td(null, TriggerModal( {employment:this.props.employment, onDeleteToggle:this.onDelete, delete:true} )),
React.DOM.td(null, TriggerModal( {employment:this.props.employment, onDeleteToggle:this.onDelete, delete:false} ))
)
: React.DOM.tr(null);
}
});

Expand All @@ -85,7 +161,7 @@ var EmploymentList = React.createClass({displayName: 'EmploymentList',
)
});
return (
React.DOM.ul(null, employments)
React.DOM.table(null, React.DOM.tbody(null, employments))
);
}
});
Expand All @@ -97,8 +173,7 @@ var UserRow = React.createClass({displayName: 'UserRow',
React.DOM.td(null, this.props.user.email),
React.DOM.td(null, this.props.user.first_name),
React.DOM.td(null, this.props.user.last_name),
React.DOM.td(null, EmploymentList( {user:this.props.user} )),
React.DOM.td(null, React.DOM.i(null, "to do"))
React.DOM.td(null, EmploymentList( {user:this.props.user} ))
)
);
}
Expand Down Expand Up @@ -126,7 +201,7 @@ var UserTable = React.createClass({displayName: 'UserTable',
});
return (
Table( {striped:true},
React.DOM.thead(null, React.DOM.tr(null, React.DOM.th(null, "Email"),React.DOM.th(null, "First name"),React.DOM.th(null, "Last name"),React.DOM.th(null, "Organisations"),React.DOM.th(null, "Permissions"))),
React.DOM.thead(null, React.DOM.tr(null, React.DOM.th(null, "Email"),React.DOM.th(null, "First name"),React.DOM.th(null, "Last name"),React.DOM.th(null, "Organisations"))),
React.DOM.tbody(null, users)
)
);
Expand Down
Loading

0 comments on commit b87463d

Please sign in to comment.