manager: display progress while importing users (#50163)

This commit is contained in:
Valentin Deniaud 2021-04-21 12:05:28 +02:00
parent 5ca05f327e
commit 30ee547427
6 changed files with 70 additions and 19 deletions

View File

@ -363,7 +363,7 @@ class UserCsvImporter(object):
line_error = LineError.from_error(line_error)
self.errors.append(line_error)
def run(self, fd_or_str, encoding, ou=None, simulate=False):
def run(self, fd_or_str, encoding, ou=None, simulate=False, progress_callback=None):
self.ou = ou or get_default_ou()
self.errors = []
self._missing_roles = set()
@ -379,7 +379,9 @@ class UserCsvImporter(object):
try:
with atomic():
for row in self.rows:
for i, row in enumerate(self.rows):
if progress_callback:
progress_callback(_('importing'), i, len(self.rows))
try:
if not self.do_import_row(row, unique_map):
self.rows_with_errors += 1
@ -393,7 +395,10 @@ class UserCsvImporter(object):
except Simulate:
pass
for action in [parse_csv, self.parse_header_row, self.parse_rows, do_import]:
def parse_rows():
self.parse_rows(progress_callback)
for action in [parse_csv, self.parse_header_row, parse_rows, do_import]:
action()
if self.errors:
break
@ -514,13 +519,15 @@ class UserCsvImporter(object):
except (AttributeError, TypeError, KeyError):
self.add_error(LineError('unknown-flag', _('unknown flag "%s"'), line=1, column=column))
def parse_rows(self):
def parse_rows(self, progress_callback=None):
base_form_class = ImportUserForm
if SOURCE_NAME in self.headers_by_name:
base_form_class = ImportUserFormWithExternalId
form_class = modelform_factory(User, fields=self.headers_by_name.keys(), form=base_form_class)
rows = self.rows = []
for i, row in enumerate(self.csv_importer.rows[1:]):
if progress_callback:
progress_callback(_('parsing'), i, len(self.csv_importer.rows))
csv_row = self.parse_row(form_class, row, line=i + 2)
self.has_errors = self.has_errors or not (csv_row.is_valid)
rows.append(csv_row)

View File

@ -56,19 +56,29 @@
</thead>
<tbody>
{% for report in reports %}
<tr data-uuid="{{ report.uuid }}">
<td class="created">
{% if report.state != 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>
<td class="state">{{ report.state_display }}</td>
<td class="applied">{% if not report.simulate %}<span class="icon-check"></span>{% endif %}</td>
<td class="delete-action">{% if report.simulate %}<form method="post" id="delete-form-{{ report.uuid }}">{% csrf_token %}<button name="delete" value="{{ report.uuid }}">{% trans "Delete" %}</button></form>{% endif %}</td>
</tr>
<tr data-uuid="{{ report.uuid }}" data-url="{% url "a2-manager-users-import-report" import_uuid=user_import.uuid report_uuid=report.uuid %}">
{% include "authentic2/manager/user_import_report_row.html" %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
function autorefresh(el) {
$.ajax({
url: $(el).data('url'),
success: function(html) {
$(el).html(html);
},
});
}
function scheduleRefreshes() {
$('tbody tr').each(function() {
if($(this).children('td.state.running').length > 0)
setInterval(autorefresh, 2000, this);
});
}
$(document).ready(scheduleRefreshes);
</script>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% load i18n %}
<td class="created">
{% if report.state != 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>
<td class="state{% if report.state == report.STATE_RUNNING %} running{% endif %}">{{ report.state_display }}</td>
<td class="applied">{% if not report.simulate %}<span class="icon-check"></span>{% endif %}</td>
<td class="delete-action">{% if report.simulate %}<form method="post" id="delete-form-{{ report.uuid }}">{% csrf_token %}<button name="delete" value="{{ report.uuid }}">{% trans "Delete" %}</button></form>{% endif %}</td>

View File

@ -166,7 +166,10 @@ class Report(object):
@property
def state_display(self):
state = self.data['state']
return self.STATES.get(state, state)
state_display = self.STATES.get(state, state)
if state == self.STATE_RUNNING and 'progress' in self.data:
state_display = '%s (%s)' % (state_display, self.data['progress'])
return state_display
@property
@contextlib.contextmanager
@ -205,6 +208,12 @@ class Report(object):
else:
yield None
def callback(status, line, total):
if total < 1 or not self.exists():
return
with self.data_update as data:
data['progress'] = '%s, %d%%' % (status, round((line / total) * 100))
def thread_worker():
from authentic2.csv_import import UserCsvImporter
@ -218,7 +227,11 @@ class Report(object):
try:
with publik_provisionning():
importer.run(
fd, encoding=self.data['encoding'], ou=self.data['ou'], simulate=simulate
fd,
encoding=self.data['encoding'],
ou=self.data['ou'],
simulate=simulate,
progress_callback=callback,
)
except Exception as e:
logger.exception('error during report %s:%s run', self.user_import.uuid, self.uuid)

View File

@ -902,7 +902,6 @@ class UserImportReportView(MediaMixin, PermissionMixin, TemplateView):
form_class = UserEditImportForm
permissions = ['custom_user.admin_user']
permissions_global = True
template_name = 'authentic2/manager/user_import_report.html'
def dispatch(self, request, import_uuid, report_uuid):
from authentic2.manager.user_import import UserImport
@ -926,6 +925,11 @@ class UserImportReportView(MediaMixin, PermissionMixin, TemplateView):
ctx['report_title'] = _('Execution')
return ctx
def get_template_names(self):
if self.request.is_ajax():
return ['authentic2/manager/user_import_report_row.html']
return ['authentic2/manager/user_import_report.html']
user_import_report = UserImportReportView.as_view()

View File

@ -500,6 +500,11 @@ x,x,x,x'''.encode(
response = response.follow()
report_url = response.pyquery('tr[data-uuid="%s"]' % uuid).attr('data-url')
ajax_resp = app.get(report_url, xhr=True)
assert len(ajax_resp.pyquery('td')) == 4
assert 'body' not in ajax_resp
def assert_timeout(duration, wait_function):
start = time.time()
while True: