Repository: saleor/saleor
Pull request: Add staff member management
Fixes #1143
Fixes #1171

TODO:

- [x] add create action
- [x] add delete action
- [x] update form with 'is_staff' and 'is_superuser' checkboxes
- [x] add option to promote customer to staff member
- [x] send email with password form
- [x] fix password reset view
- [x] move promoting customer staff to modal
- [x] is_staff status on staff-create on True by default
- [x] add messages after update action
- [x] add cancel button to staff-detail
- [x] add number of orders to staff confirm_delete.html
- [x] fix possibility to remove your own acount

Problem statement:
Staff management
AFAIK there's still no easy way to grant/remove `staff membership` to one, from the dashboard. 
`staff list`/`groups` could be grouped under one dropdown in 'Configuration' settings.
`staff list` could be expanded for granting/removing `staff membership`
 

Password reset doesn't work after update to Django 1.11
When accessing link to reset the password the following error happens:

```
PasswordResetConfirmView() received an invalid keyword 'set_password_form'. as_view only accepts arguments that are already attributes of the class.
```

The problem is that in the view `password_reset_confirm` in `saleor.registration.views` we build kwargs with additional arguments that are not members of `PasswordResetConfirmView`.

Hints from issue discussion:
I'm not sure what you're proposing?
I was thinking about the functionality to grant staff membership to existing customer, by his email.
Would simplify the process of creating new store admins. 
I think a more common use-case we should also solve is demoting existing staff members when someone leaves the company (remove their staff status and strip all of their permissions).
We should have views to create/edit users from dashboard which would also cover staff users management.
I would opt for keeping staff management in one place, instead of splitting it to 3 different panels, but some good linking between them will do
@Pacu2 Customers management and staff management are two very different use cases even if some people are in both groups.
Staff detail view is prepared and we need only to add there next form with all staff related fields.

Commit message: Update email sending to use celery

Change send_set_password_email in staff/emails to use Celery while sending emails.

Patch to review:
diff --git a/saleor/dashboard/customer/urls.py b/saleor/dashboard/customer/urls.py
index ffcd04b1341..f9f565f988f 100644
--- a/saleor/dashboard/customer/urls.py
+++ b/saleor/dashboard/customer/urls.py
@@ -5,5 +5,7 @@
 
 urlpatterns = [
     url(r'^$', views.customer_list, name='customers'),
-    url(r'^(?P<pk>[0-9]+)/$', views.customer_details, name='customer-details')
+    url(r'^(?P<pk>[0-9]+)/$', views.customer_details, name='customer-details'),
+    url(r'^(?P<pk>[0-9]+)/promote/$', views.customer_promote_to_staff,
+        name='customer-promote')
 ]
diff --git a/saleor/dashboard/customer/views.py b/saleor/dashboard/customer/views.py
index 380e4c75359..21f70347f0b 100644
--- a/saleor/dashboard/customer/views.py
+++ b/saleor/dashboard/customer/views.py
@@ -1,14 +1,16 @@
 from __future__ import unicode_literals
 
+from django.contrib import messages
+from django.contrib.auth.decorators import permission_required
 from django.db.models import Count, Max
-from django.shortcuts import get_object_or_404
+from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
-from django.contrib.auth.decorators import permission_required
+from django.utils.translation import pgettext_lazy
 
 from ...core.utils import get_paginator_items
 from ...userprofile.models import User
 from ...settings import DASHBOARD_PAGINATE_BY
-from ..views import staff_member_required
+from ..views import staff_member_required, superuser_required
 
 
 @staff_member_required
@@ -38,3 +40,18 @@ def customer_details(request, pk):
     customer_orders = customer.orders.all()
     ctx = {'customer': customer, 'customer_orders': customer_orders}
     return TemplateResponse(request, 'dashboard/customer/detail.html', ctx)
+
+
+@superuser_required
+def customer_promote_to_staff(request, pk):
+    customer = get_object_or_404(User, pk=pk)
+    if request.method == 'POST':
+        customer.is_staff = True
+        msg = pgettext_lazy(
+            'Dashboard message',
+            'Customer %s promoted to staff member') % customer
+        messages.success(request, msg)
+        return redirect('dashboard:staff-list')
+    return TemplateResponse(
+        request, 'dashboard/customer/modal/confirm_promote.html',
+        {'customer': customer})
diff --git a/saleor/dashboard/group/views.py b/saleor/dashboard/group/views.py
index 1074c04f88a..6de243931f9 100644
--- a/saleor/dashboard/group/views.py
+++ b/saleor/dashboard/group/views.py
@@ -6,6 +6,8 @@
 from django.template.response import TemplateResponse
 from django.utils.translation import pgettext_lazy
 
+from ...core.utils import get_paginator_items
+from ...settings import DASHBOARD_PAGINATE_BY
 from ..views import superuser_required
 from .forms import GroupPermissionsForm
 
@@ -14,6 +16,8 @@
 def group_list(request):
     groups = [{'name': group, 'permissions': group.permissions.all()}
               for group in Group.objects.all().prefetch_related('permissions')]
+    groups = get_paginator_items(
+        groups, DASHBOARD_PAGINATE_BY, request.GET.get('page'))
     ctx = {'groups': groups}
     return TemplateResponse(request, 'dashboard/group/list.html', ctx)
 
@@ -59,6 +63,4 @@ def group_delete(request, pk):
         )
         return redirect('dashboard:group-list')
     return TemplateResponse(
-        request, 'dashboard/group/modal/confirm_delete.html',
-        {'group': group}
-    )
+        request, 'dashboard/group/modal/confirm_delete.html', {'group': group})
diff --git a/saleor/dashboard/staff/emails.py b/saleor/dashboard/staff/emails.py
new file mode 100644
index 00000000000..ecb9724a43c
--- /dev/null
+++ b/saleor/dashboard/staff/emails.py
@@ -0,0 +1,23 @@
+from django.contrib.auth.tokens import default_token_generator
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
+
+from templated_email import send_templated_mail
+from celery import shared_task
+
+from ...settings import DEFAULT_FROM_EMAIL, get_host
+from ...site.utils import get_site_name
+
+
+@shared_task
+def send_set_password_email(staff):
+    ctx = {'protocol': 'http',
+           'domain': get_host(),
+           'site_name': get_site_name(),
+           'uid': urlsafe_base64_encode(force_bytes(staff.pk)),
+           'token': default_token_generator.make_token(staff)}
+    send_templated_mail(
+        template_name='dashboard/staff/set_password',
+        from_email=DEFAULT_FROM_EMAIL,
+        recipient_list=[staff.email],
+        context=ctx)
diff --git a/saleor/dashboard/staff/forms.py b/saleor/dashboard/staff/forms.py
index 4796c961b75..ea06cc9294a 100644
--- a/saleor/dashboard/staff/forms.py
+++ b/saleor/dashboard/staff/forms.py
@@ -3,7 +3,15 @@
 from ...userprofile.models import User
 
 
-class UserGroupForm(forms.ModelForm):
+class StaffForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        self.admin = kwargs.pop('admin', None)
+        kwargs.update(initial={'is_staff': True})
+        super(StaffForm, self).__init__(*args, **kwargs)
+        if self.admin == self.instance:
+            self.fields['is_superuser'].widget.attrs['disabled'] = True
+
     class Meta:
         model = User
-        fields = ['groups']
+        fields = ['email', 'groups', 'is_superuser', 'is_staff',
+                  'is_active']
diff --git a/saleor/dashboard/staff/urls.py b/saleor/dashboard/staff/urls.py
index 905f107b009..5ef027d5bcc 100644
--- a/saleor/dashboard/staff/urls.py
+++ b/saleor/dashboard/staff/urls.py
@@ -5,5 +5,7 @@
 
 urlpatterns = [
     url(r'^$', views.staff_list, name='staff-list'),
+    url(r'^staff-create/$', views.staff_create, name='staff-create'),
     url(r'^(?P<pk>[0-9]+)/$', views.staff_details, name='staff-details'),
+    url(r'^(?P<pk>[0-9]+)/delete/$', views.staff_delete, name='staff-delete'),
 ]
diff --git a/saleor/dashboard/staff/views.py b/saleor/dashboard/staff/views.py
index 562b4194eb9..cefed60fb15 100644
--- a/saleor/dashboard/staff/views.py
+++ b/saleor/dashboard/staff/views.py
@@ -1,11 +1,15 @@
 from __future__ import unicode_literals
 
-from django.shortcuts import get_object_or_404
+from django.contrib import messages
+from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
+from django.utils.translation import pgettext_lazy
 
-from .forms import UserGroupForm
+from .emails import send_set_password_email
+from .forms import StaffForm
 from ..views import superuser_required
 from ...core.utils import get_paginator_items
+from ...settings import DASHBOARD_PAGINATE_BY
 from ...userprofile.models import User
 
 
@@ -15,7 +19,7 @@ def staff_list(request):
                      .prefetch_related('default_billing_address')
                      .order_by('email'))
     staff_members = get_paginator_items(
-        staff_members, 30, request.GET.get('page'))
+        staff_members, DASHBOARD_PAGINATE_BY, request.GET.get('page'))
     ctx = {'staff': staff_members}
     return TemplateResponse(request, 'dashboard/staff/list.html', ctx)
 
@@ -24,8 +28,45 @@ def staff_list(request):
 def staff_details(request, pk):
     queryset = User.objects.filter(is_staff=True)
     staff_member = get_object_or_404(queryset, pk=pk)
-    form = UserGroupForm(request.POST or None, instance=staff_member)
+    form = StaffForm(request.POST or None, instance=staff_member,
+                     admin=request.user)
     if form.is_valid():
         form.save()
+        msg = pgettext_lazy(
+            'Dashboard message', 'Updated staff member %s') % staff_member
+        messages.success(request, msg)
+        redirect('dashboard:staff-list')
     ctx = {'staff_member': staff_member, 'form': form}
     return TemplateResponse(request, 'dashboard/staff/detail.html', ctx)
+
+
+@superuser_required
+def staff_create(request):
+    staff = User()
+    form = StaffForm(request.POST or None, instance=staff)
+    if form.is_valid():
+        form.save()
+        msg = pgettext_lazy(
+            'Dashboard message', 'Added staff member %s') % staff
+        messages.success(request, msg)
+        send_set_password_email(staff)
+        return redirect('dashboard:staff-list')
+    ctx = {'form': form}
+    return TemplateResponse(request, 'dashboard/staff/detail.html', ctx)
+
+
+@superuser_required
+def staff_delete(request, pk):
+    queryset = User.objects.prefetch_related(
+        'orders')
+    staff = get_object_or_404(queryset, pk=pk)
+    all_orders_count = staff.orders.count()
+    if request.method == 'POST':
+        staff.delete()
+        msg = pgettext_lazy(
+            'Dashboard message', 'Deleted staff member %s') % staff
+        messages.success(request, msg)
+        return redirect('dashboard:staff-list')
+    return TemplateResponse(
+        request, 'dashboard/staff/modal/confirm_delete.html',
+        {'staff': staff, 'orders': all_orders_count})
diff --git a/saleor/registration/forms.py b/saleor/registration/forms.py
index df8e01509f2..49969cb8c37 100644
--- a/saleor/registration/forms.py
+++ b/saleor/registration/forms.py
@@ -1,7 +1,11 @@
 from django import forms
 from django.contrib.auth import forms as django_forms
+from django.core.urlresolvers import reverse
+from django.http.request import HttpRequest
 from django.utils.translation import pgettext
 
+from templated_email import send_templated_mail
+
 from saleor.userprofile.models import User
 
 
@@ -45,3 +49,26 @@ class SetPasswordForm(django_forms.SetPasswordForm):
     def __init__(self, *args, **kwargs):
         super(SetPasswordForm, self).__init__(*args, **kwargs)
         del self.fields['new_password2']
+
+
+class PasswordSetUpForm(django_forms.PasswordResetForm):
+    """
+        PasswordSetUpForm that overrides sending emails to use templated email.
+        Allows setting password for new users that have no usable password.
+    """
+
+    def get_users(self, email):
+        active_users = User.objects.filter(email__iexact=email, is_active=True)
+        return active_users
+
+    def send_mail(
+            self, subject_template_name, email_template_name, context,
+            from_email, to_email, html_email_template_name=None):
+        reset_url = HttpRequest.build_absolute_uri(
+            reverse(
+                'account_reset_password_confirm',
+                kwargs={'uidb64': context['uid'], 'token': context['token']}))
+        context['reset_url'] = reset_url
+        send_templated_mail(
+            'account/password_set', from_email=from_email,
+            recipient_list=[to_email], context=context)
diff --git a/saleor/registration/views.py b/saleor/registration/views.py
index d86f1063b75..bbc840a7b3d 100644
--- a/saleor/registration/views.py
+++ b/saleor/registration/views.py
@@ -8,7 +8,7 @@
 from django.template.response import TemplateResponse
 
 from saleor.cart.utils import find_and_assign_anonymous_cart
-from .forms import LoginForm, SignupForm, SetPasswordForm
+from .forms import LoginForm, PasswordSetUpForm, SignupForm, SetPasswordForm
 
 
 @find_and_assign_anonymous_cart()
@@ -50,12 +50,20 @@ def password_reset(request):
     return django_views.PasswordResetView.as_view(**kwargs)(request, **kwargs)
 
 
+class PasswordResetConfirm(django_views.PasswordResetConfirmView):
+    template_name = 'account/password_reset_from_key.html'
+    success_url = reverse_lazy('account_reset_password_complete')
+    set_password_form = PasswordSetUpForm
+    token = None
+    uidb64 = None
+
+
 def password_reset_confirm(request, uidb64=None, token=None):
     kwargs = {
         'template_name': 'account/password_reset_from_key.html',
         'success_url': reverse_lazy('account_reset_password_complete'),
-        'set_password_form': 'SetPasswordForm',
+        'set_password_form': 'PasswordSetUpForm',
         'token': token,
         'uidb64': uidb64}
-    return django_views.PasswordResetConfirmView.as_view(**kwargs)(
+    return PasswordResetConfirm.as_view(**kwargs)(
         request, **kwargs)
diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html
index 730a676e459..d81c1d48b4f 100644
--- a/templates/account/password_reset_from_key.html
+++ b/templates/account/password_reset_from_key.html
@@ -18,21 +18,29 @@ <h2>{% trans "Bad Token" %}</h2>
       </p>
     </div>
   {% else %}
-    <form method="post" novalidate>
-      {% csrf_token %}
-      <h2>{% trans "Password Reset" context "Password reset from key form title" %}</h2>
-      <p>{% trans "Now you can set new password for you account." context "Password reset from key form title" %}</p>
-      <fieldset>
-        {% bootstrap_form form %}
-      </fieldset>
-      <div class="row login__btn-group">
-        <div class="col-sm-12">
-          <button class="btn primary narrow">
-            {% trans "Continue" context "Password reset from key form primary action" %}
-          </button>
-          <img src="{% static "images/pirate-reset.png" %}" />
+    {% if form %}
+      <form method="post" novalidate>
+        {% csrf_token %}
+        <h2>{% trans "Password Reset" context "Password reset from key form title" %}</h2>
+        <p>{% trans "Now you can set new password for you account." context "Password reset from key form title" %}</p>
+        <fieldset>
+          {% bootstrap_form form %}
+        </fieldset>
+        <div class="row login__btn-group">
+          <div class="col-sm-12">
+            <button class="btn primary narrow">
+              {% trans "Continue" context "Password reset from key form primary action" %}
+            </button>
+            <img src="{% static "images/pirate-reset.png" %}" />
+          </div>
         </div>
-      </div>
-    </form>
+      </form>
+    {% else %}
+      <h2>{% trans "Password Reset Link Expired" context "Password reset link expired from key form title" %}</h2>
+      <p>{% trans "Sorry but this link is no longer valid. If you forget your password, please use link below." context "Password reset from key form title" %}</p>
+      <a href="{% url 'account_reset_password' %}">
+        {% trans "Reset my password" context "Reset my password link" %}
+      </a>
+    {% endif %}
   {% endif %}
 {% endblock %}
diff --git a/templates/dashboard/customer/detail.html b/templates/dashboard/customer/detail.html
index 275ccb6e1ce..f8e35d4abde 100644
--- a/templates/dashboard/customer/detail.html
+++ b/templates/dashboard/customer/detail.html
@@ -1,6 +1,7 @@
 {% extends "dashboard/base.html" %}
 {% load prices_i18n %}
 {% load i18n %}
+{% load materializecss %}
 {% load status %}
 {% load staticfiles %}
 
@@ -33,6 +34,24 @@
   </ul>
 {% endblock %}
 
+
+{% block header_extra %}
+  {% if customer.pk %}
+    <a class="dropdown-button right" href="#" data-activates="context-menu" data-constrainwidth="false">
+      <svg data-src="{% static "dashboard/images/more_vert.svg" %}" height="24" width="24" />
+    </a>
+    <ul id="context-menu" class="dropdown-content">
+      <li>
+        <a href="#base-modal" data-href="{% url 'dashboard:customer-promote' pk=customer.pk %}"
+        class="modal-trigger-custom">
+          {% trans 'Promote to staff' context 'Customer promote to staff member action' %}
+        </a>
+      </li>
+    </ul>
+  {% endif %}
+{% endblock %}
+
+
 {% block content %}
   <div class="row">
     <div class="col s12 l8">
@@ -90,8 +109,9 @@ <h5 class="data-table-title">
         {% endif %}
       </div>
     </div>
+
     <div class="col s12 l4">
-      <div class="row">
+       <div class="row">
         <div class="col s12 m6 p6 l12">
           <div class="card equal-height-on-med">
             <div class="card-content">
diff --git a/templates/dashboard/customer/modal/confirm_promote.html b/templates/dashboard/customer/modal/confirm_promote.html
new file mode 100644
index 00000000000..fafd137312c
--- /dev/null
+++ b/templates/dashboard/customer/modal/confirm_promote.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+<form action="{% block action_url %}#!{% endblock %}" method="post" novalidate>
+  {% csrf_token %}
+  <div class="modal-content">
+    <div class="row">
+      <div class="col s12">
+        <h5>
+            {% block title %}
+            {% trans "Promote to staff member" context "Dashboard confirmation modal" %}
+            {% endblock %}
+        </h5>
+      </div>
+    </div>
+    {% block content %}
+    {% trans "Are you sure?" context "Dashboard confirmation modal" %}
+    {% endblock %}
+  </div>
+  <div class="modal-footer">
+  {% block footer %}
+    <button type="submit" class="modal-action btn-flat">
+      {% trans "Promote" context "Dashboard promote action" %}
+    </button>
+    <a href="#!" class="modal-action modal-close btn-flat">
+      {% trans "Cancel" context "Dashboard cancel action" %}
+    </a>
+  {% endblock footer %}
+  </div>
+</form>
diff --git a/templates/dashboard/group/detail.html b/templates/dashboard/group/detail.html
index f18264a2fa3..65670a445fb 100644
--- a/templates/dashboard/group/detail.html
+++ b/templates/dashboard/group/detail.html
@@ -1,8 +1,6 @@
 {% extends "dashboard/base.html" %}
 {% load materializecss %}
-{% load prices_i18n %}
 {% load i18n %}
-{% load status %}
 {% load staticfiles %}
 
 {% block title %}
@@ -52,7 +50,7 @@
       <li>
         <a href="#base-modal" data-href="{% url 'dashboard:group-delete' pk=group.pk %}"
         class="modal-trigger-custom">
-          {% trans "Remove group" context "Group form action" %}
+          {% trans 'Remove group' context 'Group form action' %}
         </a>
       </li>
     </ul>
@@ -84,7 +82,11 @@ <h5 class="data-table-title">
                 {% trans "Cancel" context "Dashboard cancel action" %}
               </a>
               <button type="submit" class="btn waves-effect waves-light">
-                  {% trans "Update" context "Dashboard update action" %}
+                {% if group.name %}
+                  {% trans 'Update' context 'Dashboard update action' %}
+                {% else %}
+                  {% trans 'Add' context 'Dashboard add action' %}
+                {% endif %}
               </button>
             </div>
           </div>
diff --git a/templates/dashboard/group/list.html b/templates/dashboard/group/list.html
index 3939f1b8c85..f90c90a67ca 100644
--- a/templates/dashboard/group/list.html
+++ b/templates/dashboard/group/list.html
@@ -1,8 +1,8 @@
 {% extends "dashboard/base.html" %}
 {% load i18n %}
-{% load prices_i18n %}
 {% load materializecss %}
 {% load staticfiles %}
+{% load utils %}
 
 {% block title %}
   {% trans "Groups" context "Dashboard groups list" %} - {{ block.super }}
@@ -56,6 +56,7 @@
     </div>
   {% endif %}
 
-  {% include "dashboard/includes/_pagination.html" with page_obj=customers %}
+  {% paginate groups %}
+
 {% endblock %}
 
diff --git a/templates/dashboard/group/modal/confirm_delete.html b/templates/dashboard/group/modal/confirm_delete.html
index 25556b901b9..2e1499a3fa5 100644
--- a/templates/dashboard/group/modal/confirm_delete.html
+++ b/templates/dashboard/group/modal/confirm_delete.html
@@ -2,6 +2,8 @@
 
 {% load i18n %}
 
+{% block action_url %}{% url 'dashboard:group-delete' pk=group.pk %}{% endblock %}
+
 {% block title %}
 {% blocktrans trimmed context "Modal delete group text" %}
   Delete group <strong>{{ group }}</strong>?
@@ -14,5 +16,5 @@
 <p>Users might lose permissions related to this group.</p>
 {% endblocktrans %}
 {% endblock content %}
- 
+
 
diff --git a/templates/dashboard/staff/detail.html b/templates/dashboard/staff/detail.html
index 5e58990072d..d9c91499aa8 100644
--- a/templates/dashboard/staff/detail.html
+++ b/templates/dashboard/staff/detail.html
@@ -1,12 +1,15 @@
 {% extends "dashboard/base.html" %}
 {% load materializecss %}
-{% load prices_i18n %}
 {% load i18n %}
-{% load status %}
 {% load staticfiles %}
 
 {% block title %}
-  {{ staff_member }} - {% trans "Customers" context "Dashboard customers list" %} - {{ block.super }}
+  {% if staff_member %}
+    {{ staff_member }}
+  {% else %}
+    {% trans "Add new staff member" context "Dashboard add group" %}
+  {% endif %}
+     - {% trans "Staff" context "Dashboard staff" %} - {{ block.super }}
 {% endblock %}
 
 {% block body_class %}body-customers{% endblock %}
@@ -30,10 +33,26 @@
         <svg data-src="{% static "dashboard/images/arrow_left.svg" %}" fill="#fff" width="20px" height="20px" />
       </a>
     </li>
-    <li><span class="breadcrumbs--elipsed-item">{{ staff_member }}</span></li>
+    <li><span class="breadcrumbs--elipsed-item">{% if staff_member %}{{ staff_member }}{% else %}{% trans 'Add staff member' context 'Dashboard add staff member action' %}{% endif %}</span></li>
   </ul>
 {% endblock %}
 
+{% block header_extra %}
+  {% if user != staff_member and staff_member.pk %}
+    <a class="dropdown-button right" href="#" data-activates="context-menu" data-constrainwidth="false">
+      <svg data-src="{% static "dashboard/images/more_vert.svg" %}" height="24" width="24" />
+    </a>
+    <ul id="context-menu" class="dropdown-content">
+      <li>
+        <a href="#base-modal" data-href="{% url 'dashboard:staff-delete' pk=staff_member.pk %}"
+        class="modal-trigger-custom">
+          {% trans 'Remove staff member' context 'Staff member form action' %}
+        </a>
+      </li>
+    </ul>
+  {% endif %}
+{% endblock %}
+
 {% block content %}
   <div class="row">
     <div class="col s12 l8">
@@ -41,7 +60,7 @@
         <form class="card" method="post">
           <div class="card-content">
             <span class="card-title">
-              User groups
+              {% trans 'Staff member details' context 'Staff member details header' %}
             </span>
             <div class="data">
                 {% csrf_token %}
@@ -51,8 +70,15 @@
             </div>
           </div>
           <div class="card-action right-align">
+            <a href="{% url 'dashboard:staff-list' %}" class="btn btn-flat">
+              {% trans "Cancel" context "Dashboard cancel action" %}
+            </a>
             <button type="submit" class="btn waves-effect waves-light">
-                {% trans "Update" context "Dashboard update action" %}
+              {% if staff_member %}
+                {% trans 'Update' context 'Dashboard update action' %}
+              {% else %}
+                {% trans 'Add' context 'Dashboard add action' %}
+              {% endif %}
             </button>
           </div>
         </form>
diff --git a/templates/dashboard/staff/list.html b/templates/dashboard/staff/list.html
index bb15f789356..915309308d6 100644
--- a/templates/dashboard/staff/list.html
+++ b/templates/dashboard/staff/list.html
@@ -2,6 +2,8 @@
 {% load i18n %}
 {% load prices_i18n %}
 {% load materializecss %}
+{% load staticfiles %}
+{% load utils %}
 
 {% block title %}
   {% trans "Staff" context "Dashboard staff members list" %} - {{ block.super }}
@@ -11,6 +13,16 @@
 
 {% block menu_staff_class %}active{% endblock %}
 
+{% block header_extra %}
+    <div class="fixed-action-btn">
+      <a class="btn-fab btn-floating btn-large waves-effect waves-light teal"
+        title="{% trans "Add staff member" context "Staff members list add button text" %}"
+        href="{% url 'dashboard:staff-create' %}">
+        <svg data-src="{% static "dashboard/images/add.svg" %}" />
+      </a>
+    </div>
+{% endblock %}
+
 {% block breadcrumbs %}
   <ul class="breadcrumbs">
     <li class="visible-s">
@@ -65,5 +77,7 @@
       </p>
     </div>
   {% endif %}
-  {% include "dashboard/includes/_pagination.html" with page_obj=customers %}
+
+  {% paginate staff %}
+
 {% endblock %}
diff --git a/templates/dashboard/staff/modal/confirm_delete.html b/templates/dashboard/staff/modal/confirm_delete.html
new file mode 100644
index 00000000000..be5bde9431d
--- /dev/null
+++ b/templates/dashboard/staff/modal/confirm_delete.html
@@ -0,0 +1,20 @@
+{% extends "dashboard/base_modal_confirm_delete.html" %}
+
+{% load i18n %}
+
+{% block action_url %}{% url 'dashboard:staff-delete' pk=staff.pk %}{% endblock %}
+
+{% block title %}
+{% blocktrans trimmed context "Modal delete group text" %}
+  Delete staff member <strong>{{ staff }}</strong>?
+{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+{% blocktrans trimmed context "Modal delete group text" %}
+<p>Are you sure you want to delete staff member?</p>
+<p>Deleting staff member also removes him from customer list and removes all his related data (orders, payment etc.).</p>
+<p>We strongly recommend not deleting user accounts that were used for purchases (for example customer which was promoted with staff member status).</p>
+<p>{{ staff }} has <strong>{{ orders }}</strong> orders.</p>
+{% endblocktrans %}
+{% endblock content %}
diff --git a/templates/materializecssform/field.html b/templates/materializecssform/field.html
index 8ea34328e65..66ae5ee5549 100644
--- a/templates/materializecssform/field.html
+++ b/templates/materializecssform/field.html
@@ -7,7 +7,7 @@
     <div class="{{ classes.single_value }}">
       <p>
         {% if field.auto_id %}
-          <input id="{{ field.auto_id }}" name="{{ field.html_name }}" class="filled-in" type="checkbox"{% if field.value %} checked="checked" {% endif %}>
+          <input id="{{ field.auto_id }}" name="{{ field.html_name }}" class="filled-in" type="checkbox"{% if field.value %} checked="checked" {% endif %} {% if field.field.widget.attrs.disabled %} disabled {% endif %}>
           <label for="{{ field.auto_id }}">
             {{ field.label }}
           </label>
diff --git a/templates/templated_email/dashboard/staff/set_password.email b/templates/templated_email/dashboard/staff/set_password.email
new file mode 100644
index 00000000000..4a0811daef0
--- /dev/null
+++ b/templates/templated_email/dashboard/staff/set_password.email
@@ -0,0 +1,13 @@
+{% load i18n %}
+{% block subject %}{% blocktrans %}Hello from {{ site_name }}!{% endblocktrans %}{% endblock %}
+
+{% block plain %}
+{% blocktrans %}You're receiving this e-mail because you have to set password for your staff member account at {{ domain }}.
+Click the link below to reset your password.{% endblocktrans %}
+
+{{ protocol}}://{{ domain }}{% url 'account_reset_password_confirm' uidb64=uid token=token %}
+
+{% blocktrans %}Thank you for using {{ site_name }}!
+{{ domain }}
+{% endblocktrans %}
+{% endblock %}
diff --git a/tests/dashboard/test_permissions.py b/tests/dashboard/test_permissions.py
index 942893cc152..cb80591b3cb 100644
--- a/tests/dashboard/test_permissions.py
+++ b/tests/dashboard/test_permissions.py
@@ -1,10 +1,14 @@
 from __future__ import unicode_literals
+import re
 
+from django.core import mail
 from django.core.urlresolvers import reverse
 from django.contrib.auth.models import Group
+from ..utils import get_redirect_location
+
 from saleor.userprofile.models import User
 from saleor.dashboard.group.forms import GroupPermissionsForm
-from saleor.dashboard.staff.forms import UserGroupForm
+from saleor.dashboard.staff.forms import StaffForm
 
 
 def test_admin_can_view_staff_list(admin_client):
@@ -29,6 +33,16 @@ def test_staff_cant_view_staff_detail(staff_client, admin_user):
     assert response.status_code == 302
 
 
+def test_admin_can_view_staff_create(admin_client):
+    response = admin_client.get(reverse('dashboard:staff-create'))
+    assert response.status_code == 200
+
+
+def test_staff_cant_view_staff_create(staff_client):
+    response = staff_client.get(reverse('dashboard:staff-create'))
+    assert response.status_code == 302
+
+
 def test_admin_can_view_groups_list(admin_client):
     response = admin_client.get(reverse('dashboard:group-list'))
     assert response.status_code == 200
@@ -693,22 +707,38 @@ def test_group_detail_form_valid(
     assert staff_group.permissions.get(pk=permission_view_product.pk)
 
 
-def test_user_group_form_not_valid(db):
+def test_staff_form_not_valid(db):
     data = {'groups': 1}
-    form = UserGroupForm(data=data)
+    form = StaffForm(data=data)
     assert not form.is_valid()
 
 
-def test_user_group_form_create_valid(
+def test_staff_form_create_valid(
         admin_client, staff_user, staff_group):
     url = reverse('dashboard:staff-details', args=[staff_user.pk])
-    data = {'groups': staff_group.pk}
+    data = {'email': 'staff@example.com', 'groups': staff_group.pk}
     admin_client.post(url, data)
     staff_user = User.objects.get(pk=staff_user.pk)
     assert staff_user.groups.count() == 1
 
 
-def test_user_group_form_create_not_valid(admin_client, staff_user):
+def test_staff_create_email_with_set_link_password(
+        admin_client, staff_group):
+    url = reverse('dashboard:staff-create')
+    data = {'email': 'staff3@example.com', 'groups': staff_group.pk,
+            'is_staff': True}
+    response = admin_client.post(url, data)
+    assert User.objects.count() == 2
+    assert len(mail.outbox) == 1
+    assert response['Location'] == reverse('dashboard:staff-list')
+
+    match = re.search('http.*', mail.outbox[0].body)
+    link = match.group(0)
+    response = admin_client.get(link)
+    assert response.status_code == 302
+
+
+def test_staff_form_create_not_valid(admin_client, staff_user):
     url = reverse('dashboard:staff-details', args=[staff_user.pk])
     data = {'groups': 1}
     admin_client.post(url, data)
@@ -729,3 +759,18 @@ def test_delete_group_no_POST(admin_client, staff_group):
     url = reverse('dashboard:group-delete', args=[staff_group.pk])
     admin_client.get(url)
     assert Group.objects.all().count() == 1
+
+
+def test_delete_staff(admin_client, staff_user):
+    assert User.objects.all().count() == 2
+    url = reverse('dashboard:staff-delete', kwargs={'pk': staff_user.pk})
+    data = {'pk': staff_user.pk}
+    response = admin_client.post(url, data)
+    assert User.objects.all().count() == 1
+    assert response['Location'] == '/dashboard/staff/'
+
+
+def test_delete_staff_no_POST(admin_client, staff_user):
+    url = reverse('dashboard:staff-delete', args=[staff_user.pk])
+    admin_client.get(url)
+    assert User.objects.all().count() == 2


Code review comments:
diff_hunk: @@ -0,0 +1,29 @@
+{% load i18n %}
+
+<form action="{% block action_url %}#!{% endblock %}" method="post" novalidate>
path: templates/dashboard/customer/modal/confirm_promote.html
text: @user1:
Why to use blocks in this template? It's not inherited anywhere.

@author:
I assumed that with development of our dashboard we eventualy will have more confirmation modals. So i have done to be like base_modal_confirm_delete.html.
Should i delete those blocks tags or add base_modal_confirm template?

diff_hunk: @@ -24,8 +28,45 @@ def staff_list(request):
 def staff_details(request, pk):
     queryset = User.objects.filter(is_staff=True)
     staff_member = get_object_or_404(queryset, pk=pk)
-    form = UserGroupForm(request.POST or None, instance=staff_member)
+    form = StaffForm(request.POST or None, instance=staff_member,
+                     admin=request.user)
path: saleor/dashboard/staff/views.py
text: @user1:
There are some places in code where we pass `request.user` as an argument and we used to call this argument simply `user`. Here we have `staff_member` and `admin` which is a bit confusing.

@author:
i had to differ staff_member (and his details) from user which is editing his account. And as only admin can do this i thought it would be more readable.

@user1:
That's true, but we don't use name `admin` anywhere else in code and in the context of Saleor it's not that obvious what that name would refer to. What I mean is that we should try to be consistent as much as possible.

@author:
Ok, i will change it.