commit 64438aee024316f29861c1515b0eb6eaa9021da7
Author: Serghei Mihai <smihai@entrouvert.com>
Date:   Tue Apr 28 15:43:46 2015 +0200

    import users view(#7065)

diff --git a/uauth/organization/forms.py b/uauth/organization/forms.py
index 90c13e2..6bfb5b2 100644
--- a/uauth/organization/forms.py
+++ b/uauth/organization/forms.py
@@ -27,3 +27,7 @@ class LocalAccountForm(forms.ModelForm):
 class LocalAccountCreateForm(LocalAccountForm):
     accounts_number = forms.IntegerField(_('Number of accounts to create'), required=False)
     accounts_number_start = forms.IntegerField(_('First number of multiple accounts'), required=False)
+
+
+class UsersImportForm(forms.Form):
+    users_file = forms.FileField(_('Users file'))
diff --git a/uauth/organization/templates/organization/import_users.html b/uauth/organization/templates/organization/import_users.html
new file mode 100644
index 0000000..3e82fdd
--- /dev/null
+++ b/uauth/organization/templates/organization/import_users.html
@@ -0,0 +1,28 @@
+{% extends 'uauth/base.html' %}
+{% load i18n %}
+
+{% block content %}
+<form method="post" action="{% url "import-users" organization.slug %}" enctype="multipart/form-data">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <h3>{% trans "Expected file format" %}: CSV</h3>
+  <ul>
+    <li>{% trans "5 columns: login, first name, last name, expiration date, password" %}</li>
+    <li>{% trans "date format: YYYY-MM-DD" %}</li>
+    <li>{% trans "if empty password, it will be generated" %}</li>
+    <li>{% trans "if user exists, it will be updated" %}</li>
+    <li>{% trans "separator: comma" %}</li>
+    <li>{% trans "encoding: utf-8" %}</li>
+    <li>{% trans "first line is ignored" %}</li>
+  </ul>
+  <p>Example:
+    <div class="example">
+      "login", "first name", "last name", "expiration", "password"<br />
+      "foo", "Foo", "User", "2015-12-31", "secret" <br />
+      "bar", "User", "Bar", "2015-04-30", ""<br />
+      "test", "", "", "", ""<br />
+    </div>
+  </p>
+  <p><button name="upload">{% trans "Import" %}</button>
+</form>
+{% endblock %}
diff --git a/uauth/organization/templates/organization/users.html b/uauth/organization/templates/organization/users.html
index cd0e065..f88a23d 100644
--- a/uauth/organization/templates/organization/users.html
+++ b/uauth/organization/templates/organization/users.html
@@ -7,6 +7,7 @@
 
 {% block appbar %}
 <h2>{% trans "Local users" %}</h2>
+<a href="{% url "import-users" organization.slug %}" rel="popup">{% trans "Import users" %}</a>
 <a href="{% url "create-users" organization.slug %}" rel="popup">{% trans "Create users" %}</a>
 {% endblock %}
 
diff --git a/uauth/organization/urls.py b/uauth/organization/urls.py
index 9697a1f..a169b26 100644
--- a/uauth/organization/urls.py
+++ b/uauth/organization/urls.py
@@ -6,6 +6,7 @@ urlpatterns = patterns('',
     url(r'^$', manage, name='manage'),
     url(r'^users/?$', users, name='manage-users'),
     url(r'^users/create$', create_users, name='create-users'),
+    url(r'^users/import$', import_users, name='import-users'),
     url(r'^users/(?P<pk>[\w]+)/$', view_user, name='view-user'),
     url(r'^users/(?P<pk>[\w]+)/edit$', edit_user, name='edit-user'),
 )
diff --git a/uauth/organization/utils.py b/uauth/organization/utils.py
index e59425c..f348c8d 100644
--- a/uauth/organization/utils.py
+++ b/uauth/organization/utils.py
@@ -18,3 +18,19 @@ def create_user(data):
         return user
     except:
         return False
+
+def create_or_update_users(data):
+    created = 0
+    updated = 0
+    for user in data:
+        try:
+            account = LocalAccount.objects.get(username=user['username'])
+            if not user['password']:
+                del user['password']
+            account.__dict__.update(user)
+            account.save()
+            updated += 1
+        except LocalAccount.DoesNotExist:
+            if create_user(user):
+                created += 1
+    return created, updated
diff --git a/uauth/organization/views.py b/uauth/organization/views.py
index adae5e7..e125a4e 100644
--- a/uauth/organization/views.py
+++ b/uauth/organization/views.py
@@ -1,18 +1,21 @@
+import csv
+import datetime
+
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse_lazy
-from django.http import HttpResponseRedirect
+from django.shortcuts import render, redirect
 
 from django.views.generic.base import TemplateView
 from django.views.generic.list import ListView
 from django.views.generic.edit import FormView, UpdateView
-from django.views.generic import DetailView
+from django.views.generic import DetailView, View
 from django.contrib import messages
 
 from django_tables2 import RequestConfig
 
-from .utils import create_user
+from .utils import create_user, create_or_update_users
 from .models import LocalAccount, Organization
-from .forms import LocalAccountCreateForm, LocalAccountForm
+from .forms import LocalAccountCreateForm, LocalAccountForm, UsersImportForm
 from .tables import AccountTable
 
 
@@ -96,9 +99,57 @@ class UserEditView(OrganizationMixin, UpdateView):
         if 'delete' in self.request.POST:
             self.object.delete()
             messages.info(self.request, _('Account "%s" successfully deleted' % username))
-            return HttpResponseRedirect(self.get_success_url())
+            return redirect(self.get_success_url())
         else:
             messages.info(self.request, _('Account "%s" successfully updated' % username))
             return super(UserEditView, self).form_valid(form)
 
 edit_user = UserEditView.as_view()
+
+
+class ImportUsersView(OrganizationMixin, TemplateView):
+    form_class = UsersImportForm
+    template_name = 'organization/import_users.html'
+
+    def get_context_data(self, **kwargs):
+        ctx = super(ImportUsersView, self).get_context_data(**kwargs)
+        ctx['form'] = self.form_class()
+        return ctx
+
+    def post(self, request, *args, **kwargs):
+        form = self.form_class(request.POST, request.FILES)
+        context = self.get_context_data(**kwargs)
+        context['form'] = form
+        if form.is_valid():
+            data = form.cleaned_data['users_file']
+            dialect = csv.Sniffer().sniff(data.read(1024))
+            data.seek(0)
+            reader = csv.reader(data, dialect)
+            reader.next()
+            users = []
+            for row in reader:
+                try:
+                    user = {'username': row[0].strip(),
+                            'first_name': row[1].strip(),
+                            'last_name': row[2].strip(),
+                            'password': row[4].strip(),
+                            'organization': context['organization']
+                        }
+                except IndexError:
+                    # ignore wrong lines
+                    continue
+                try:
+                    user['expiration_date'] = datetime.datetime.strptime(row[3], '%Y-%m-%d')
+                except ValueError:
+                    pass
+                users.append(user)
+            created, updated = create_or_update_users(users)
+            if created:
+                messages.info(request, _('%s accounts added' % created))
+            if updated:
+                messages.info(request, _('%s accounts updated' % updated))
+            return redirect(self.get_success_url())
+        else:
+            return self.render_to_response(context)
+
+import_users = ImportUsersView.as_view()
diff --git a/uauth/static/css/style.css b/uauth/static/css/style.css
index de28ea0..48a304f 100644
--- a/uauth/static/css/style.css
+++ b/uauth/static/css/style.css
@@ -61,4 +61,10 @@ ul.login li, #guest-login ul li, #voucher-login ul li, .loginbox li {
 .icon-delete:before {
     content: '\f1f8';
     margin: 0 3px;
+}
+
+div.example {
+    background: #eee;
+    border: 1px solid #bbb;
+    font-family: Monospace;
 }
\ No newline at end of file
