Projet

Général

Profil

0003-manager-add-user-import-views-32833.patch

Benjamin Dauvergne, 17 juin 2019 09:50

Télécharger (35,3 ko)

Voir les différences:

Subject: [PATCH 3/3] manager: add user import views (#32833)

 debian/control                                |   3 +-
 setup.py                                      |   1 +
 src/authentic2/manager/forms.py               |  74 ++++++
 .../authentic2/manager/css/user_import.css    |  51 ++++
 .../authentic2/manager/user_import.html       |  67 ++++++
 .../manager/user_import_report.html           | 126 ++++++++++
 .../authentic2/manager/user_imports.html      |  47 ++++
 .../templates/authentic2/manager/users.html   |   5 +
 src/authentic2/manager/urls.py                |   8 +
 src/authentic2/manager/user_import.py         | 227 ++++++++++++++++++
 src/authentic2/manager/user_views.py          | 132 +++++++++-
 tests/test_manager_user_import.py             |  93 +++++++
 12 files changed, 829 insertions(+), 5 deletions(-)
 create mode 100644 src/authentic2/manager/static/authentic2/manager/css/user_import.css
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/user_import.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/user_import_report.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/user_imports.html
 create mode 100644 src/authentic2/manager/user_import.py
 create mode 100644 tests/test_manager_user_import.py
debian/control
31 31
    python-pil,
32 32
    python-tablib,
33 33
    python-chardet,
34
    python-attr
34
    python-attr,
35
    python-atomicwrites
35 36
Breaks: python-authentic2-auth-fc (<< 0.26)
36 37
Replaces: python-authentic2-auth-fc (<< 0.26)
37 38
Provides: ${python:Provides}, python-authentic2-auth-fc
setup.py
142 142
          'tablib',
143 143
          'chardet',
144 144
          'attrs',
145
          'atomicwrites',
145 146
      ],
146 147
      zip_safe=False,
147 148
      classifiers=[
src/authentic2/manager/forms.py
45 45
from . import fields, app_settings, utils
46 46

  
47 47
User = get_user_model()
48
OU = get_ou_model()
48 49

  
49 50
logger = logging.getLogger(__name__)
50 51

  
......
715 716
class SiteImportForm(forms.Form):
716 717
    site_json = forms.FileField(
717 718
        label=_('Site Export File'))
719

  
720

  
721
ENCODINGS = [
722
    ('utf-8', _('UTF-8')),
723
    ('cp1252', _('CP-1252 - Windows France')),
724
    ('iso-8859-1', _('Latin 1')),
725
    ('iso-8859-15', _('Latin 15')),
726
    ('detect', _('Detect encoding')),
727
]
728

  
729

  
730
class UserImportForm(forms.Form):
731
    import_file = forms.FileField(
732
        label=_('Import file'),
733
        help_text=_('A CSV file'))
734
    encoding = forms.ChoiceField(
735
        label=_('Encoding'),
736
        choices=ENCODINGS)
737
    ou = forms.ModelChoiceField(
738
        label=_('Organizational Unit'),
739
        queryset=OU.objects.all())
740

  
741

  
742
class UserNewImportForm(UserImportForm):
743
    def clean(self):
744
        from authentic2.csv_import import CsvImporter
745

  
746
        import_file = self.cleaned_data['import_file']
747
        encoding = self.cleaned_data['encoding']
748
        # force seek(0)
749
        import_file.open()
750
        importer = CsvImporter()
751
        if not importer.run(import_file, encoding):
752
            raise forms.ValidationError(importer.error.description or importer.error.code)
753
        self.cleaned_data['rows_count'] = len(importer.rows)
754

  
755
    def save(self):
756
        from . import user_import
757

  
758
        import_file = self.cleaned_data['import_file']
759
        import_file.open()
760
        new_import = user_import.UserImport.new(
761
            import_file=import_file,
762
            encoding=self.cleaned_data['encoding'])
763
        with new_import.meta_update as meta:
764
            meta['filename'] = import_file.name
765
            meta['ou'] = self.cleaned_data['ou']
766
            meta['rows_count'] = self.cleaned_data['rows_count']
767
        return new_import
768

  
769

  
770
class UserEditImportForm(UserImportForm):
771
    def __init__(self, *args, **kwargs):
772
        self.user_import = kwargs.pop('user_import')
773
        initial = kwargs.setdefault('initial', {})
774
        initial['encoding'] = self.user_import.meta['encoding']
775
        initial['ou'] = self.user_import.meta['ou']
776
        super(UserEditImportForm, self).__init__(*args, **kwargs)
777
        del self.fields['import_file']
778

  
779
    def clean(self):
780
        from authentic2.csv_import import CsvImporter
781
        encoding = self.cleaned_data['encoding']
782
        with self.user_import.import_file as fd:
783
            importer = CsvImporter(fd, encoding)
784
            if not importer.run():
785
                raise forms.ValidationError(importer.error.description or importer.error.code)
786
            self.cleaned_data['rows_count'] = len(importer.rows)
787

  
788
    def save(self):
789
        with self.user_import.meta_update as meta:
790
            meta['ou'] = self.cleaned_data['ou']
791
            meta['encoding'] = self.cleaned_data['encoding']
src/authentic2/manager/static/authentic2/manager/css/user_import.css
1
#import-report-table tr.row-valid td, .legend-row-valid {
2
    background-color: #d5f5e3 ;
3
}
4

  
5
#import-report-table tr.row-invalid td, .legend-row-invalid {
6
    background-color: #ff4408;
7
}
8

  
9
#import-report-table tr td.cell-action-updated, .legend-cell-action-updated {
10
    background-color: #abebc6;
11
}
12

  
13
#import-report-table tr td.cell-errors, .legend-cell-errors {
14
    background-color: #cd6155;
15
}
16
.header-flag-key::after {
17
        content: "\f084"; /* fa-key */
18
        font-family: FontAwesome;
19
        padding-left: 1ex;
20
}
21

  
22
.header-flag-unique::after,
23
.header-flag-globally-unique::after
24
{
25
        content: "\f0cd"; /* fa-underline */
26
        font-family: FontAwesome;
27
        padding-left: 1ex;
28
}
29

  
30
.header-flag-create::after {
31
        content: "\f0fe"; /* fa-plus-square */
32
        font-family: FontAwesome;
33
        padding-left: 1ex;
34
}
35

  
36
.header-flag-update::after {
37
        content: "\f040"; /* fa-pencil */
38
        font-family: FontAwesome;
39
        padding-left: 1ex;
40
}
41

  
42
.header-flag-verified::after {
43
        content: "\f023"; /* fa-lock */
44
        font-family: FontAwesome;
45
        padding-left: 1ex;
46
}
47

  
48
span.icon-check::after {
49
    content: "\f00c";
50
    font-family: FontAwesome;
51
}
src/authentic2/manager/templates/authentic2/manager/user_import.html
1
{% extends "authentic2/manager/base.html" %}
2
{% load i18n gadjo staticfiles %}
3

  
4
{% block page-title %}{{ block.super }} - {% trans "Import Users" %}{% endblock %}
5

  
6
{% block css %}
7
{{ block.super }}
8
<link rel="stylesheet" href="{% static "authentic2/manager/css/user_import.css" %}">
9
{% endblock %}
10

  
11
{% block breadcrumb %}
12
  {{ block.super }}
13
  <a href="{% url 'a2-manager-users' %}">{% trans 'Users' %}</a>
14
  <a href="{% url 'a2-manager-users-imports' %}">{% trans 'Import' %}</a>
15
  <a href="{% url 'a2-manager-users-import' uuid=user_import.uuid %}">{% trans "User Import" %} {{ user_import.created }}</a>
16
{% endblock %}
17

  
18
{% block sidebar %}
19
  <aside id="sidebar">
20
    <div>
21
      <h3>{% trans "Modify import" %}</h3>
22
      <form method="post">
23
        {% csrf_token %}
24
        {{ form|with_template }}
25
        <div class="buttons">
26
          <button name="create">{% trans "Modify" %}</button>
27
          <button name="simulate">{% trans "Simulate" %}</button>
28
          <button name="execute">{% trans "Execute" %}</button>
29
        </div>
30
      </form>
31
    </div>
32
    <div>
33
      <h3>{% trans "Download" %}</h3>
34
      <form action="download/{{ user_import.filename }}">
35
        <div class="buttons">
36
          <button>{% trans "Download" %}</button>
37
        </div>
38
      </form>
39
    </div>
40
  </aside>
41
{% endblock %}
42

  
43
{% block main %}
44
  <h2>{% trans "User Import" %} - {{ user_import.created }} - {{ user_import.user }}</h2>
45
  <p>{% trans "Rows count:" %} {{ user_import.rows_count }}</p>
46
  <h2>{% trans "Reports" %}</h2>
47
  <table class="main">
48
    <thead>
49
      <tr>
50
        <td>{% trans "Creation date" %}</td>
51
        <td>{% trans "State" %}</td>
52
        <td>{% trans "Imported" %}</td>
53
        <td></td>
54
      </tr>
55
    </thead>
56
    <tbody>
57
      {% for report in reports %}
58
        <tr>
59
          <td>{% if report.state != 'running' %}<a href="{% url "a2-manager-users-import-report" import_uuid=user_import.uuid report_uuid=report.uuid %}">{{ report.created }}</a>{% else %}{{ report.created }}{% endif %}</td>
60
          <td>{{ report.state }} {% if report.state == 'error' %}"{{ report.exception }}"{% endif %}</td>
61
          <td>{% if not report.simulate %}<span class="icon-check"></span>{% endif %}</td>
62
          <td>{% if report.simulate %}<form method="post">{% csrf_token %}<button name="delete" value="{{ report.uuid }}">{% trans "Delete" %}</button></form>{% endif %}</td>
63
        </tr>
64
      {% endfor %}
65
    </tbody>
66
  </table>
67
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/user_import_report.html
1
{% extends "authentic2/manager/base.html" %}
2
{% load i18n gadjo staticfiles %}
3

  
4
{% block page-title %}{{ block.super }} - {% trans "Import Users" %}{% endblock %}
5

  
6
{% block css %}
7
{{ block.super }}
8
<link rel="stylesheet" href="{% static "authentic2/manager/css/user_import.css" %}">
9
{% endblock %}
10

  
11
{% block breadcrumb %}
12
  {{ block.super }}
13
  <a href="{% url 'a2-manager-users' %}">{% trans 'Users' %}</a>
14
  <a href="{% url 'a2-manager-users-imports' %}">{% trans 'Import' %}</a>
15
  <a href="{% url 'a2-manager-users-import' uuid=user_import.uuid %}">{% trans "User Import" %} {{ user_import.created }}</a>
16
  <a href="{% url 'a2-manager-users-import-report' import_uuid=user_import.uuid report_uuid=report.uuid%}">{{ report_title }} {{ report.created }}</a>
17
{% endblock %}
18

  
19
{% block sidebar %}
20
  <aside id="sidebar">
21
    <h3>{% trans "Legend" %}</td>
22
    <table>
23
      <tr>
24
        <td><span class="header-flag-key"></span></td>
25
        <td>{% trans "value is a key" %}</td>
26
      </tr>
27
      <tr>
28
        <td><span class="header-flag-unique"></span></td>
29
        <td>{% trans "value must be unique" %}</td>
30
      </tr>
31
      <tr>
32
        <td><span class="header-flag-create"></span></td>
33
        <td>{% trans "used on creation" %}</td>
34
      </tr>
35
      <tr>
36
        <td><span class="header-flag-update"></span></td>
37
        <td>{% trans "used on update" %}</td>
38
      </tr>
39
      <tr>
40
        <td><span class="header-flag-verified"></span></td>
41
        <td>{% trans "value is verified" %}</td>
42
      </tr>
43
      <tr>
44
        <td class="legend-row-valid"></td>
45
        <td>{% trans "row is valid" %}</td>
46
      </tr>
47
      <tr>
48
        <td class="legend-cell-action-updated"></td>
49
        <td>{% trans "value will be written" %}</td>
50
      </tr>
51
      <tr>
52
        <td class="legend-row-invalid"></td>
53
        <td>{% trans "row is invalid" %}</td>
54
      </tr>
55
      <tr>
56
        <td class="legend-cell-errors"></td>
57
        <td>{% trans "value has errors" %}</td>
58
      </tr>
59
    </table>
60
  </aside>
61
{% endblock %}
62

  
63
{% block main %}
64
  <h2>{{ report_title }} - {{ report.created }} - {{ report.state }}</h2>
65
  {% if report.exception %}
66
  <p>{% trans "Exception:" %} {{ report.exception}}</p>
67
  {% endif %}
68
  {% if report.importer %}
69
    {% with importer=report.importer %}
70
      {% if importer.errors %}
71
        <h4>{% trans "Errors" %}</h4>
72
        <ul class="errors">
73
          {% for error in importer.errors %}
74
          <li data-code="{{ error.code }}">{% firstof error.description error.code %}</li>
75
          {% endfor %}
76
        </ul>
77
      {% endif %}
78
      {% if importer.rows %}
79
        <table id="import-report-table" class="main">
80
          <thead>
81
              <tr>
82
                <td>{% trans "Line" %}</td>
83
                {% for header in importer.headers %}
84
                  <td
85
                    {% if header.flags %}
86
                    title="flags: {% for flag in header.flags %}{{ flag }} {% endfor %}"
87
                    {% endif %}
88
                    >
89
                    {{ header.name }}
90
                    {% for flag in header.flags %}
91
                      <span class="header-flag-{{ flag }}"></span>
92
                    {% endfor %}
93
                  </td>
94
                {% endfor %}
95
                <td>{% trans "Action" %}</td>
96
              </tr>
97
          </thead>
98
          <tbody>
99
            {% for row in importer.rows %}
100
              <tr
101
                class="{% if row.is_valid %}row-valid{% else %}row-invalid{% endif %} row-action-{{ row.action }}"
102
                {% if not row.is_valid %}title="{% for error in row.errors %}{% firstof error.description error.code %}
103
                {% endfor %}{% for cell in row %}{% for error in cell.errors %}
104
{{ cell.header.name }}: {% firstof error.description error.code %}{% endfor %}{% endfor %}"{% endif %}
105
              >
106
                <td class="row-line">{{ row.line }}</td>
107
                {% for cell in row %}
108
                  <td
109
                    class="{% if cell.errors %}cell-errors{% endif %} {% if cell.action %}cell-action-{{ cell.action }}{% endif %}"
110
                    {% if cell.errors %}title="{% for error in cell.errors %}{% firstof error.description error.code %}
111
{% endfor %}"{% endif %}
112
                  >
113
                    {{ cell.value }}
114
                  </td>
115
                {% endfor %}
116
                <td class="row-action">{% firstof row.action "-" %}</td>
117
              </tr>
118
            {% endfor %}
119
          </tbody>
120
        </table>
121
      {% else %}
122
        <p>{% trans "No row analysed." %}</p>
123
      {% endif %}
124
    {% endwith %}
125
  {% endif %}
126
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/user_imports.html
1
{% extends "authentic2/manager/base.html" %}
2
{% load i18n gadjo staticfiles %}
3

  
4
{% block page-title %}{{ block.super }} - {% trans "Import Users" %}{% endblock %}
5

  
6
{% block breadcrumb %}
7
  {{ block.super }}
8
  <a href="{% url 'a2-manager-users' %}">{% trans 'Users' %}</a>
9
  <a href="{% url 'a2-manager-users-imports' %}">{% trans 'Import Users' %}</a>
10
{% endblock %}
11

  
12
{% block sidebar %}
13
  <aside id="sidebar">
14
    <h3>{% trans "Create new import" %}</h3>
15
    <form method="post" enctype="multipart/form-data">
16
      {% csrf_token %}
17
      {{ form|with_template }}
18
      <button name="create">{% trans "Create" %}</button>
19
    </form>
20
  </aside>
21
{% endblock %}
22

  
23
{% block content %}
24
  <h3>{% trans "Imports" %}</h3>
25
  <table class="main">
26
    <thead>
27
      <tr>
28
        <td>{% trans "Filename" %}</td>
29
        <td>{% trans "Creation date" %}</td>
30
        <td>{% trans "By" %}</td>
31
        <td>{% trans "Rows" %}</td>
32
        <td></td>
33
      </tr>
34
    </thead>
35
    <tbody>
36
      {% for import in imports %}
37
        <tr>
38
          <td><a href="{% url "a2-manager-users-import" uuid=import.uuid %}">{{ import.filename }}</a></td>
39
          <td>{{ import.created }}</td>
40
          <td>{{ import.user }}</td>
41
          <td>{{ import.rows_count }}</td>
42
          <td><form method="post">{% csrf_token %}<button name="delete" value="{{ import.uuid }}">{% trans "Delete" %}</button></form></td>
43
        </tr>
44
      {% endfor %}
45
    </tbody>
46
  </table>
47
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/users.html
6 6
{% block appbar %}
7 7
  {{ block.super }}
8 8
  <span class="actions">
9
    <a class="extra-actions-menu-opener"></a>
9 10
    {% if add_ou %}
10 11
     <a
11 12
        href="{% url "a2-manager-user-add" ou_pk=add_ou.pk %}"
......
20 21
         {% trans "Add user" %}
21 22
     </a>
22 23
   {% endif %}
24
     <ul class="extra-actions-menu">
25
       <li><a href="{% url "a2-manager-users-imports" %}">{% trans 'Import Users' %}</a></li>
26
     </ul>
23 27
  </span>
28

  
24 29
{% endblock %}
25 30

  
26 31
{% block breadcrumb %}
src/authentic2/manager/urls.py
39 39
            user_views.users_export, name='a2-manager-users-export'),
40 40
        url(r'^users/add/$', user_views.user_add_default_ou,
41 41
            name='a2-manager-user-add-default-ou'),
42
        url(r'^users/import/$',
43
            user_views.user_imports, name='a2-manager-users-imports'),
44
        url(r'^users/import/(?P<uuid>[a-z0-9]+)/download/(?P<filename>.*)$',
45
            user_views.user_import, name='a2-manager-users-import-download'),
46
        url(r'^users/import/(?P<uuid>[a-z0-9]+)/$',
47
            user_views.user_import, name='a2-manager-users-import'),
48
        url(r'^users/import/(?P<import_uuid>[a-z0-9]+)/(?P<report_uuid>[a-z0-9]+)/$',
49
            user_views.user_import_report, name='a2-manager-users-import-report'),
42 50
        url(r'^users/(?P<ou_pk>\d+)/add/$', user_views.user_add,
43 51
            name='a2-manager-user-add'),
44 52
        url(r'^users/(?P<pk>\d+)/$', user_views.user_detail,
src/authentic2/manager/user_import.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from __future__ import unicode_literals
18

  
19
import base64
20
import contextlib
21
import datetime
22
import logging
23
import os
24
import pickle
25
import shutil
26
import uuid
27
import threading
28

  
29
from atomicwrites import atomic_write
30

  
31
from django.core.files.storage import default_storage
32
from django.utils import six
33
from django.utils.functional import cached_property
34
from django.utils.timezone import utc
35

  
36
logger = logging.getLogger(__name__)
37

  
38

  
39
def new_id():
40
    return (base64.b32encode(uuid.uuid4().get_bytes())
41
            .strip('=')
42
            .lower()
43
            .decode('ascii'))
44

  
45

  
46
class UserImport(object):
47
    def __init__(self, uuid):
48
        self.uuid = uuid
49
        self.path = os.path.join(self.base_path(), self.uuid)
50
        self.import_path = os.path.join(self.path, 'content')
51
        self.meta_path = os.path.join(self.path, 'meta.pck')
52

  
53
    def exists(self):
54
        return os.path.exists(self.import_path) and os.path.exists(self.meta_path)
55

  
56
    @cached_property
57
    def created(self):
58
        return datetime.datetime.fromtimestamp(os.path.getctime(self.path), utc)
59

  
60
    @property
61
    def import_file(self):
62
        return open(self.import_path, 'rb')
63

  
64
    @cached_property
65
    def meta(self):
66
        meta = {}
67
        if os.path.exists(self.meta_path):
68
            with open(self.meta_path) as fd:
69
                meta = pickle.load(fd)
70
        return meta
71

  
72
    @property
73
    @contextlib.contextmanager
74
    def meta_update(self):
75
        try:
76
            yield self.meta
77
        finally:
78
            with atomic_write(self.meta_path, overwrite=True) as fd:
79
                pickle.dump(self.meta, fd)
80

  
81
    @classmethod
82
    def base_path(self):
83
        path = default_storage.path('user_imports')
84
        if not os.path.exists(path):
85
            os.makedirs(path)
86
        return path
87

  
88
    @classmethod
89
    def new(cls, import_file, encoding):
90
        o = cls(new_id())
91
        os.makedirs(o.path)
92
        with open(o.import_path, 'wb') as fd:
93
            import_file.seek(0)
94
            fd.write(import_file.read())
95
        with o.meta_update as meta:
96
            meta['encoding'] = encoding
97
        return o
98

  
99
    @classmethod
100
    def all(cls):
101
        for subpath in os.listdir(cls.base_path()):
102
            user_import = UserImport(subpath)
103
            if user_import.exists():
104
                yield user_import
105

  
106
    @property
107
    def reports(self):
108
        return Reports(self)
109

  
110
    def __getattr__(self, name):
111
        try:
112
            return self.meta[name]
113
        except KeyError:
114
            raise AttributeError(name)
115

  
116
    def delete(self):
117
        if self.exists():
118
            shutil.rmtree(self.path)
119

  
120

  
121
class Report(object):
122
    def __init__(self, user_import, uuid):
123
        self.user_import = user_import
124
        self.uuid = uuid
125
        self.path = os.path.join(self.user_import.path, '%s%s' % (Reports.PREFIX, uuid))
126

  
127
    @cached_property
128
    def created(self):
129
        return datetime.datetime.fromtimestamp(os.path.getctime(self.path), utc)
130

  
131
    @cached_property
132
    def data(self):
133
        data = {}
134
        if os.path.exists(self.path):
135
            with open(self.path) as fd:
136
                data = pickle.load(fd)
137
        return data
138

  
139
    @property
140
    @contextlib.contextmanager
141
    def data_update(self):
142
        try:
143
            yield self.data
144
        finally:
145
            with atomic_write(self.path, overwrite=True) as fd:
146
                pickle.dump(self.data, fd)
147

  
148
    @classmethod
149
    def new(cls, user_import):
150
        report = cls(user_import, new_id())
151
        with report.data_update as data:
152
            data['encoding'] = user_import.meta['encoding']
153
            data['ou'] = user_import.meta.get('ou')
154
            data['state'] = 'waiting'
155
        return report
156

  
157
    def run(self, start=True, simulate=False):
158
        assert self.data.get('state') == 'waiting'
159

  
160
        with self.data_update as data:
161
            data['simulate'] = simulate
162

  
163
        def target():
164
            from authentic2.csv_import import UserCsvImporter
165

  
166
            with self.user_import.import_file as fd:
167
                importer = UserCsvImporter()
168
                try:
169
                    importer.run(fd,
170
                                 encoding=self.data['encoding'],
171
                                 ou=self.data['ou'],
172
                                 simulate=simulate)
173
                except Exception as e:
174
                    logger.exception('error during report %s:%s run', self.user_import.uuid, self.uuid)
175
                    state = 'error'
176
                    try:
177
                        exception = six.text_type(e)
178
                    except Exception:
179
                        exception = repr(repr(e))
180
                else:
181
                    exception = None
182
                    state = 'finished'
183

  
184
                with self.data_update as data:
185
                    data['state'] = state
186
                    data['exception'] = exception
187
                    data['importer'] = importer
188
        t = threading.Thread(target=target)
189
        with self.data_update as data:
190
            data['state'] = 'running'
191
        if start:
192
            t.start()
193
        return t
194

  
195
    def __getattr__(self, name):
196
        try:
197
            return self.data[name]
198
        except KeyError:
199
            raise AttributeError(name)
200

  
201
    def exists(self):
202
        return os.path.exists(self.path)
203

  
204
    def delete(self):
205
        if self.simulate and self.exists():
206
            os.unlink(self.path)
207

  
208

  
209
class Reports(object):
210
    PREFIX = 'report-'
211

  
212
    def __init__(self, user_import):
213
        self.user_import = user_import
214

  
215
    def __getitem__(self, uuid):
216
        report = Report(self.user_import, uuid)
217
        if not report.exists():
218
            raise KeyError
219
        return report
220

  
221
    def __iter__(self):
222
        for name in os.listdir(self.user_import.path):
223
            if name.startswith(self.PREFIX):
224
                try:
225
                    yield self[name[len(self.PREFIX):]]
226
                except KeyError:
227
                    pass
src/authentic2/manager/user_views.py
16 16

  
17 17
import datetime
18 18
import collections
19
import operator
19 20

  
20 21
from django.db import models
21 22
from django.utils.translation import ugettext_lazy as _, ugettext
......
26 27
from django.contrib.auth import get_user_model
27 28
from django.contrib.contenttypes.models import ContentType
28 29
from django.contrib import messages
30
from django.views.generic import FormView, TemplateView
31
from django.http import Http404, FileResponse
29 32

  
30 33
import tablib
31 34

  
......
36 39
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
37 40

  
38 41

  
39
from .views import BaseTableView, BaseAddView, \
40
    BaseEditView, ActionMixin, OtherActionsMixin, Action, ExportMixin, \
41
    BaseSubTableView, HideOUColumnMixin, BaseDeleteView, BaseDetailView
42
from .views import (BaseTableView, BaseAddView, BaseEditView, ActionMixin,
43
                    OtherActionsMixin, Action, ExportMixin, BaseSubTableView,
44
                    HideOUColumnMixin, BaseDeleteView, BaseDetailView,
45
                    PermissionMixin)
42 46
from .tables import UserTable, UserRolesTable, OuUserRolesTable
43 47
from .forms import (UserSearchForm, UserAddForm, UserEditForm,
44
    UserChangePasswordForm, ChooseUserRoleForm, UserRoleSearchForm, UserChangeEmailForm)
48
                    UserChangePasswordForm, ChooseUserRoleForm,
49
                    UserRoleSearchForm, UserChangeEmailForm, UserNewImportForm,
50
                    UserEditImportForm)
45 51
from .resources import UserResource
46 52
from .utils import get_ou_count, has_show_username
47 53
from . import app_settings
......
618 624

  
619 625

  
620 626
user_delete = UserDeleteView.as_view()
627

  
628

  
629
class UserImportsView(PermissionMixin, FormView):
630
    form_class = UserNewImportForm
631
    template_name = 'authentic2/manager/user_imports.html'
632

  
633
    def post(self, request, *args, **kwargs):
634
        from . import user_import
635

  
636
        if 'delete' in request.POST:
637
            uuid = request.POST['delete']
638
            user_import.UserImport(uuid).delete()
639
            return redirect(self.request, 'a2-manager-users-imports')
640
        return super(UserImportsView, self).post(request, *args, **kwargs)
641

  
642
    def form_valid(self, form):
643
        user_import = form.save()
644
        with user_import.meta_update as meta:
645
            meta['user'] = self.request.user.get_full_name()
646
            meta['user_pk'] = self.request.user.pk
647
        return redirect(self.request, 'a2-manager-users-import', kwargs={'uuid': user_import.uuid})
648

  
649
    def get_context_data(self, **kwargs):
650
        from . import user_import
651

  
652
        ctx = super(UserImportsView, self).get_context_data()
653
        ctx['imports'] = sorted(user_import.UserImport.all(), key=operator.attrgetter('created'), reverse=True)
654
        return ctx
655

  
656
user_imports = UserImportsView.as_view()
657

  
658

  
659
class UserImportView(PermissionMixin, FormView):
660
    form_class = UserEditImportForm
661
    permissions = ['custom_user.admin_user']
662
    template_name = 'authentic2/manager/user_import.html'
663

  
664
    def dispatch(self, request, uuid, **kwargs):
665
        from user_import import UserImport
666
        self.user_import = UserImport(uuid)
667
        if not self.user_import.exists():
668
            raise Http404
669
        return super(UserImportView, self).dispatch(request, uuid, **kwargs)
670

  
671
    def get(self, request, uuid, filename=None):
672
        if filename:
673
            return FileResponse(self.user_import.import_file, content_type='text/csv')
674
        return super(UserImportView, self).get(request, uuid=uuid, filename=filename)
675

  
676
    def get_form_kwargs(self):
677
        kwargs = super(UserImportView, self).get_form_kwargs()
678
        kwargs['user_import'] = self.user_import
679
        return kwargs
680

  
681
    def post(self, request, *args, **kwargs):
682
        from . import user_import
683

  
684
        if 'delete' in request.POST:
685
            uuid = request.POST['delete']
686
            try:
687
                report = self.user_import.reports[uuid]
688
            except KeyError:
689
                pass
690
            else:
691
                report.delete()
692
            return redirect(request, 'a2-manager-users-import', kwargs={'uuid': self.user_import.uuid})
693

  
694
        simulate = 'simulate' in request.POST
695
        execute = 'execute' in request.POST
696
        if simulate or execute:
697
            report = user_import.Report.new(self.user_import)
698
            report.run(simulate=simulate)
699
            return redirect(request, 'a2-manager-users-import', kwargs={'uuid': self.user_import.uuid})
700
        return super(UserImportView, self).post(request, *args, **kwargs)
701

  
702
    def form_valid(self, form):
703
        form.save()
704
        return super(UserImportView, self).form_valid(form)
705

  
706
    def get_success_url(self):
707
        return reverse('a2-manager-users-import', kwargs={'uuid': self.user_import.uuid})
708

  
709
    def get_context_data(self, **kwargs):
710
        ctx = super(UserImportView, self).get_context_data()
711
        ctx['user_import'] = self.user_import
712
        ctx['reports'] = sorted(self.user_import.reports, key=operator.attrgetter('created'), reverse=True)
713
        return ctx
714

  
715
user_import = UserImportView.as_view()
716

  
717

  
718
class UserImportReportView(PermissionMixin, TemplateView):
719
    form_class = UserEditImportForm
720
    permissions = ['custom_user.admin_user']
721
    template_name = 'authentic2/manager/user_import_report.html'
722

  
723
    def dispatch(self, request, import_uuid, report_uuid):
724
        from user_import import UserImport
725
        self.user_import = UserImport(import_uuid)
726
        if not self.user_import.exists():
727
            raise Http404
728
        try:
729
            self.report = self.user_import.reports[report_uuid]
730
        except KeyError:
731
            raise Http404
732
        return super(UserImportReportView, self).dispatch(request, import_uuid, report_uuid)
733

  
734
    def get_context_data(self, **kwargs):
735
        ctx = super(UserImportReportView, self).get_context_data()
736
        ctx['user_import'] = self.user_import
737
        ctx['report'] = self.report
738
        if self.report.simulate:
739
            ctx['report_title'] = _('Simulation')
740
        else:
741
            ctx['report_title'] = _('Execution')
742
        return ctx
743

  
744
user_import_report = UserImportReportView.as_view()
tests/test_manager_user_import.py
1
# -*- coding: utf-8 -*-
2
# authentic2 - versatile identity manager
3
# Copyright (C) 2010-2019 Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
from __future__ import unicode_literals
19

  
20
import io
21
import operator
22

  
23
import pytest
24

  
25
from authentic2.manager.user_import import UserImport, Report
26
from authentic2.models import Attribute
27

  
28
from utils import skipif_sqlite
29

  
30

  
31
@pytest.fixture
32
def profile(transactional_db):
33
    Attribute.objects.create(name='phone', kind='phone_number', label='Numéro de téléphone')
34

  
35

  
36
@skipif_sqlite
37
def test_user_import(media, transactional_db, profile):
38
    content = '''email key verified,first_name,last_name,phone no-create
39
tnoel@entrouvert.com,Thomas,Noël,1234
40
fpeters@entrouvert.com,Frédéric,Péters,5678
41
x,x,x,x'''
42
    fd = io.BytesIO(content.encode('utf-8'))
43

  
44
    assert len(list(UserImport.all())) == 0
45

  
46
    UserImport.new(fd, encoding='utf-8')
47
    UserImport.new(fd, encoding='utf-8')
48

  
49
    assert len(list(UserImport.all())) == 2
50
    for user_import in UserImport.all():
51
        with user_import.import_file as fd:
52
            assert fd.read() == content.encode('utf-8')
53

  
54
    for user_import in UserImport.all():
55
        report = Report.new(user_import)
56
        assert user_import.reports[report.uuid].exists()
57
        assert user_import.reports[report.uuid].data['encoding'] == 'utf-8'
58
        assert user_import.reports[report.uuid].data['state'] == 'waiting'
59

  
60
        t = report.run(start=False)
61

  
62
        assert user_import.reports[report.uuid].data['state'] == 'running'
63

  
64
        t.start()
65
        t.join()
66

  
67
        assert user_import.reports[report.uuid].data['state'] == 'finished'
68
        assert user_import.reports[report.uuid].data['importer']
69
        assert not user_import.reports[report.uuid].data['importer'].errors
70

  
71
    for user_import in UserImport.all():
72
        reports = list(user_import.reports)
73
        assert len(reports) == 1
74
        assert reports[0].created
75
        importer = reports[0].data['importer']
76
        assert importer.rows[0].is_valid
77
        assert importer.rows[1].is_valid
78
        assert not importer.rows[2].is_valid
79

  
80
    user_imports = sorted(UserImport.all(), key=operator.attrgetter('created'))
81
    user_import1 = user_imports[0]
82
    report1 = list(user_import1.reports)[0]
83
    importer = report1.data['importer']
84
    assert all(row.action == 'create' for row in importer.rows[:2])
85
    assert all(cell.action == 'updated' for row in importer.rows[:2] for cell in row.cells[:3])
86
    assert all(cell.action == 'nothing' for row in importer.rows[:2] for cell in row.cells[3:])
87

  
88
    user_import2 = user_imports[1]
89
    report2 = list(user_import2.reports)[0]
90
    importer = report2.data['importer']
91
    assert all(row.action == 'update' for row in importer.rows[:2])
92
    assert all(cell.action == 'nothing' for row in importer.rows[:2] for cell in row.cells[:3])
93
    assert all(cell.action == 'updated' for row in importer.rows[:2] for cell in row.cells[3:])
0
-