Add import users
This commit is contained in:
parent
850076b444
commit
6faf7a10bd
8 changed files with 307 additions and 59 deletions
|
@ -382,7 +382,14 @@ urlpatterns = [
|
||||||
|
|
||||||
url(r'^notifications/',
|
url(r'^notifications/',
|
||||||
login_required(notification.NotificationList.as_view()),
|
login_required(notification.NotificationList.as_view()),
|
||||||
name='notification')
|
name='notification'),
|
||||||
|
|
||||||
|
url(r'^import_users/', include([
|
||||||
|
url(r'^$', user.ImportUsersView.as_view(), name='import_users'),
|
||||||
|
url(r'post_file/$', user.import_users_post_file, name='import_users_post_file'),
|
||||||
|
url(r'submit/$', user.import_users_submit, name='import_users_submit'),
|
||||||
|
url(r'sample/$', user.sample_import_users, name='import_users_sample')
|
||||||
|
])),
|
||||||
]
|
]
|
||||||
|
|
||||||
favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png',
|
favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png',
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
from django.contrib.auth.models import User
|
from judge.models import SubmissionTestCase, Problem
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from judge.models import SubmissionTestCase, Problem, Profile, Language, Organization
|
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import csv
|
|
||||||
|
|
||||||
def generate_report(problem):
|
def generate_report(problem):
|
||||||
testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all()
|
testcases = SubmissionTestCase.objects.filter(submission__problem=problem).all()
|
||||||
|
@ -21,52 +17,4 @@ def generate_report(problem):
|
||||||
rate[i] = score[i] / total[i]
|
rate[i] = score[i] / total[i]
|
||||||
|
|
||||||
for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True):
|
for i, _ in sorted(rate.items(), key=lambda x: x[1], reverse=True):
|
||||||
print(i, score[i], total[i], rate[i])
|
print(i, score[i], total[i], rate[i])
|
||||||
|
|
||||||
|
|
||||||
def import_users(csv_file):
|
|
||||||
# 1st row: username, password, name, organization
|
|
||||||
# ... row: a_username, passhere, my_name, organ
|
|
||||||
try:
|
|
||||||
f = open(csv_file, 'r')
|
|
||||||
except OSError:
|
|
||||||
print("Could not open csv file", csv_file)
|
|
||||||
return
|
|
||||||
|
|
||||||
with f:
|
|
||||||
reader = csv.DictReader(f)
|
|
||||||
|
|
||||||
for row in reader:
|
|
||||||
try:
|
|
||||||
username = row['username']
|
|
||||||
pwd = row['password']
|
|
||||||
except Exception:
|
|
||||||
print('username and/or password column missing')
|
|
||||||
print('Make sure your columns are: username, password, name, organization')
|
|
||||||
|
|
||||||
user, created = User.objects.get_or_create(username=username, defaults={
|
|
||||||
'is_active': True,
|
|
||||||
})
|
|
||||||
|
|
||||||
profile, _ = Profile.objects.get_or_create(user=user, defaults={
|
|
||||||
'language': Language.get_python3(),
|
|
||||||
'timezone': settings.DEFAULT_USER_TIME_ZONE,
|
|
||||||
})
|
|
||||||
if created:
|
|
||||||
print('Created user', username)
|
|
||||||
|
|
||||||
if pwd:
|
|
||||||
user.set_password(pwd)
|
|
||||||
elif created:
|
|
||||||
user.set_password('lqdoj')
|
|
||||||
print('User', username, 'missing password, default=lqdoj')
|
|
||||||
|
|
||||||
if 'name' in row.keys() and row['name']:
|
|
||||||
user.first_name = row['name']
|
|
||||||
|
|
||||||
if 'organization' in row.keys() and row['organization']:
|
|
||||||
org = Organization.objects.get(name=row['organization'])
|
|
||||||
profile.organizations.add(org)
|
|
||||||
user.email = row['email']
|
|
||||||
user.save()
|
|
||||||
profile.save()
|
|
98
judge/tasks/import_users.py
Normal file
98
judge/tasks/import_users.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import csv
|
||||||
|
from tempfile import mktemp
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from judge.models import Profile, Language, Organization
|
||||||
|
|
||||||
|
|
||||||
|
fields = ['username', 'password', 'name', 'school', 'email', 'organizations']
|
||||||
|
descriptions = ['my_username(edit old one if exist)',
|
||||||
|
'123456 (must have)',
|
||||||
|
'Le Van A (can be empty)',
|
||||||
|
'Le Quy Don (can be empty)',
|
||||||
|
'email@email.com (can be empty)',
|
||||||
|
'org1&org2&org3&... (can be empty - org slug in URL)']
|
||||||
|
|
||||||
|
def csv_to_dict(csv_file):
|
||||||
|
rows = csv.reader(csv_file.read().decode().split('\n'))
|
||||||
|
header = next(rows)
|
||||||
|
header = [i.lower() for i in header]
|
||||||
|
|
||||||
|
if 'username' not in header:
|
||||||
|
return []
|
||||||
|
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
if len(row) != len(header):
|
||||||
|
continue
|
||||||
|
cur_dict = {i: '' for i in fields}
|
||||||
|
for i in range(len(header)):
|
||||||
|
if header[i] not in fields:
|
||||||
|
continue
|
||||||
|
cur_dict[header[i]] = row[i]
|
||||||
|
if cur_dict['username']:
|
||||||
|
res.append(cur_dict)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# return result log
|
||||||
|
def import_users(users):
|
||||||
|
log = ''
|
||||||
|
for i, row in enumerate(users):
|
||||||
|
cur_log = str(i + 1) + '. '
|
||||||
|
|
||||||
|
username = row['username']
|
||||||
|
cur_log += username + ': '
|
||||||
|
|
||||||
|
pwd = row['password']
|
||||||
|
|
||||||
|
user, created = User.objects.get_or_create(username=username, defaults={
|
||||||
|
'is_active': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
profile, _ = Profile.objects.get_or_create(user=user, defaults={
|
||||||
|
'language': Language.get_python3(),
|
||||||
|
'timezone': settings.DEFAULT_USER_TIME_ZONE,
|
||||||
|
})
|
||||||
|
|
||||||
|
if created:
|
||||||
|
cur_log += 'Create new - '
|
||||||
|
else:
|
||||||
|
cur_log += 'Edit - '
|
||||||
|
|
||||||
|
if pwd:
|
||||||
|
user.set_password(pwd)
|
||||||
|
elif created:
|
||||||
|
user.set_password('lqdoj')
|
||||||
|
cur_log += 'Missing password, set password = lqdoj - '
|
||||||
|
|
||||||
|
if 'name' in row.keys() and row['name']:
|
||||||
|
user.first_name = row['name']
|
||||||
|
|
||||||
|
if 'school' in row.keys() and row['school']:
|
||||||
|
user.last_name = row['school']
|
||||||
|
|
||||||
|
if row['organizations']:
|
||||||
|
orgs = row['organizations'].split('&')
|
||||||
|
added_orgs = []
|
||||||
|
for o in orgs:
|
||||||
|
try:
|
||||||
|
org = Organization.objects.get(slug=o)
|
||||||
|
profile.organizations.add(org)
|
||||||
|
added_orgs.append(org.name)
|
||||||
|
except Organization.DoesNotExist:
|
||||||
|
continue
|
||||||
|
if added_orgs:
|
||||||
|
cur_log += 'Added to ' + ', '.join(added_orgs) + ' - '
|
||||||
|
|
||||||
|
user.email = row['email']
|
||||||
|
user.save()
|
||||||
|
profile.save()
|
||||||
|
cur_log += 'Saved\n'
|
||||||
|
log += cur_log
|
||||||
|
log += 'FINISH'
|
||||||
|
|
||||||
|
return log
|
|
@ -13,7 +13,8 @@ from django.db import transaction
|
||||||
from django.db.models import Count, Max, Min
|
from django.db.models import Count, Max, Min
|
||||||
from django.db.models.fields import DateField
|
from django.db.models.fields import DateField
|
||||||
from django.db.models.functions import Cast, ExtractYear
|
from django.db.models.functions import Cast, ExtractYear
|
||||||
from django.http import Http404, HttpResponseRedirect, JsonResponse
|
from django.forms import Form
|
||||||
|
from django.http import Http404, HttpResponseRedirect, JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -21,18 +22,21 @@ from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
|
from django.views import View
|
||||||
from django.views.generic import DetailView, ListView, TemplateView
|
from django.views.generic import DetailView, ListView, TemplateView
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from reversion import revisions
|
from reversion import revisions
|
||||||
|
|
||||||
from judge.forms import ProfileForm, newsletter_id
|
from judge.forms import ProfileForm, newsletter_id
|
||||||
from judge.models import Profile, Rating, Submission, Friend
|
from judge.models import Profile, Rating, Submission, Friend
|
||||||
from judge.performance_points import get_pp_breakdown
|
from judge.performance_points import get_pp_breakdown
|
||||||
from judge.ratings import rating_class, rating_progress
|
from judge.ratings import rating_class, rating_progress
|
||||||
|
from judge.tasks import import_users
|
||||||
from judge.utils.problems import contest_completed_ids, user_completed_ids
|
from judge.utils.problems import contest_completed_ids, user_completed_ids
|
||||||
from judge.utils.ranker import ranker
|
from judge.utils.ranker import ranker
|
||||||
from judge.utils.subscription import Subscription
|
from judge.utils.subscription import Subscription
|
||||||
from judge.utils.unicode import utf8text
|
from judge.utils.unicode import utf8text
|
||||||
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message
|
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message, SingleObjectFormView
|
||||||
from .contests import ContestRanking
|
from .contests import ContestRanking
|
||||||
|
|
||||||
__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile']
|
__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile']
|
||||||
|
@ -396,3 +400,56 @@ class UserLogoutView(TitleMixin, TemplateView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
auth_logout(request)
|
auth_logout(request)
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
return HttpResponseRedirect(request.get_full_path())
|
||||||
|
|
||||||
|
|
||||||
|
class ImportUsersView(TitleMixin, TemplateView):
|
||||||
|
template_name = 'user/import/index.html'
|
||||||
|
title = _('Import Users')
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
if self.request.user.is_superuser:
|
||||||
|
return super().get(self, *args, **kwargs)
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
|
||||||
|
def import_users_post_file(request):
|
||||||
|
if not request.user.is_superuser or request.method != 'POST':
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
users = import_users.csv_to_dict(request.FILES['csv_file'])
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
return JsonResponse({
|
||||||
|
'done': False,
|
||||||
|
'msg': 'No valid row found. Make sure row containing username.'
|
||||||
|
})
|
||||||
|
|
||||||
|
table_html = render_to_string('user/import/table_csv.html', {
|
||||||
|
'data': users
|
||||||
|
})
|
||||||
|
return JsonResponse({
|
||||||
|
'done': True,
|
||||||
|
'html': table_html,
|
||||||
|
'data': users
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def import_users_submit(request):
|
||||||
|
import json
|
||||||
|
if not request.user.is_superuser or request.method != 'POST':
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
users = json.loads(request.body)['users']
|
||||||
|
log = import_users.import_users(users)
|
||||||
|
return JsonResponse({
|
||||||
|
'msg': log
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def sample_import_users(request):
|
||||||
|
if not request.user.is_superuser or request.method != 'GET':
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
filename = 'import_sample.csv'
|
||||||
|
content = ','.join(import_users.fields) + '\n' + ','.join(import_users.descriptions)
|
||||||
|
response = HttpResponse(content, content_type='text/plain')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
|
||||||
|
return response
|
111
templates/user/import/index.html
Normal file
111
templates/user/import/index.html
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
{% extends "user/user-base.html" %}
|
||||||
|
{% block js_media %}
|
||||||
|
<script>
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('#load_button').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var files = $('#csv_file').prop('files');
|
||||||
|
if (files.length == 1) {
|
||||||
|
$('#load_button').addClass('disabled');
|
||||||
|
|
||||||
|
var file = files[0];
|
||||||
|
if (file.type != 'text/csv') {
|
||||||
|
alert("{{_('Upload CSV only')}}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var form_data = new FormData();
|
||||||
|
form_data.append('csv_file', file, file.name);
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', "{{url('import_users_post_file')}}", true);
|
||||||
|
xhr.setRequestHeader('X-CSRFToken', csrftoken);
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var json = JSON.parse(xhr.responseText);
|
||||||
|
$('#load_button').removeClass('disabled');
|
||||||
|
|
||||||
|
if (json.done) {
|
||||||
|
window.import_users = json.data
|
||||||
|
$('#table_csv').html(json.html);
|
||||||
|
$('#confirm_button').removeClass('disabled');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.import_users = []
|
||||||
|
$('#table_csv').html(json.msg);
|
||||||
|
$('#confirm_button').addClass('disabled');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Fail to read file.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(form_data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#confirm_button').on('click', function() {
|
||||||
|
$(this).addClass('disabled');
|
||||||
|
var data = {
|
||||||
|
'users': window.import_users
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.users || data.users.length == 0) {
|
||||||
|
alert('No valid users');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#table_csv').html('');
|
||||||
|
$('#log').html('Working...');
|
||||||
|
$.post({
|
||||||
|
url: "{{url('import_users_submit')}}",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
contentType:"application/json; charset=utf-8",
|
||||||
|
dataType:"text",
|
||||||
|
fail: function() {alert('Fail to import')},
|
||||||
|
success: function(data) {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
var msg = data.msg.split('\n');
|
||||||
|
$('#log').html('')
|
||||||
|
for (var i of msg) {
|
||||||
|
$('#log').append(`<p>${i}</p>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<center>
|
||||||
|
<label for="csv_file">{{_('User File')}}:</label>
|
||||||
|
<input type="file" accept=".csv" id="csv_file">
|
||||||
|
<a href="{{url('import_users_sample')}}">{{_('Sample')}}</a>
|
||||||
|
<div style="display: inline-flex">
|
||||||
|
<button id="load_button" style="margin-left: 1em">{{_('Load')}}</button>
|
||||||
|
<button id="confirm_button" style="margin-left: 1em" class="disabled">{{_('Import')}}</button>
|
||||||
|
</div>
|
||||||
|
</center>
|
||||||
|
<br>
|
||||||
|
<table id="table_csv" class="table"></table>
|
||||||
|
<p style="margin-left: 2em" id="log"></p>
|
||||||
|
{% endblock %}
|
24
templates/user/import/table_csv.html
Normal file
24
templates/user/import/table_csv.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{_('ID')}}</th>
|
||||||
|
<th>{{_('Username')}}</th>
|
||||||
|
<th>{{_('Password')}}</th>
|
||||||
|
<th>{{_('Name')}}</th>
|
||||||
|
<th>{{_('School')}}</th>
|
||||||
|
<th>{{_('Email')}}</th>
|
||||||
|
<th>{{_('Organizations')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in data %}
|
||||||
|
<tr>
|
||||||
|
<td>{{loop.index}}</td>
|
||||||
|
<td>{{i.username}}</td>
|
||||||
|
<td>{{i.password}}</td>
|
||||||
|
<td>{{i.name}}</td>
|
||||||
|
<td>{{i.school}}</td>
|
||||||
|
<td>{{i.email}}</td>
|
||||||
|
<td>{{i.organizations}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
|
@ -13,7 +13,7 @@
|
||||||
<div class="user-info-card">
|
<div class="user-info-card">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-info-header"><i class="fa fa-star {{user.css_class}}"></i> {{_('Rating')}}</div>
|
<div class="user-info-header"><i class="fa fa-star {{user.css_class}}"></i> {{_('Rating')}}</div>
|
||||||
<div class="user-info-body {{user.css_class}}">{{user.rating}}</div>
|
<div class="user-info-body {{user.css_class}}">{{user.rating if user.rating else '-'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-card">
|
<div class="user-info-card">
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<div class="user-info-card">
|
<div class="user-info-card">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-info-header" title="{{_('Rank by rating')}}"><i style="color: peru" class="fa fa-globe" ></i> {{_('Rating')}}</div>
|
<div class="user-info-header" title="{{_('Rank by rating')}}"><i style="color: peru" class="fa fa-globe" ></i> {{_('Rating')}}</div>
|
||||||
<div class="user-info-body">{{rating_rank}}</div>
|
<div class="user-info-body">{{rating_rank if rating_rank else '-'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -4,4 +4,7 @@
|
||||||
{{ make_tab('list', 'fa-trophy', url('user_list'), _('Leaderboard')) }}
|
{{ make_tab('list', 'fa-trophy', url('user_list'), _('Leaderboard')) }}
|
||||||
{{ make_tab('friends', 'fa-users', url('user_list') + '?friend=true', _('Friends')) }}
|
{{ make_tab('friends', 'fa-users', url('user_list') + '?friend=true', _('Friends')) }}
|
||||||
{{ make_tab('organizations', 'fa-university', url('organization_list'), _('Organizations')) }}
|
{{ make_tab('organizations', 'fa-university', url('organization_list'), _('Organizations')) }}
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
{{ make_tab('import', 'fa-table', url('import_users'), _('Import')) }}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue