Skip to content

[feature] csv import add notes and user_groups field #396 #590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/user/importing_users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ many features included in it such as:
Usernames are generated from the email address whereas passwords are
generated randomly and their lengths can be customized.
- Passwords are accepted in both clear-text and hash formats from the CSV.
- Set the RADIUS user groups that the user will belong to.
- Send mails to users whose passwords have been generated automatically.

This operation can be performed via the admin interface, with a management
Expand All @@ -26,10 +27,21 @@ CSV Format

The CSV shall be of the format:

::

username,password,email,firstname,lastname,notes,user_groups

`user_groups` consists of one or more radius group names separated by a semicolon.
Inserting groups that don't exist will silently fail.

The previous format is also supported for backwards compatibility:

::

username,password,email,firstname,lastname

OpenWISP will recognize the correct format automatically.

Imported users with hashed passwords
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
32 changes: 28 additions & 4 deletions openwisp_radius/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,18 +929,36 @@ def clean(self):
super().clean()

def add(self, reader, password_length=BATCH_DEFAULT_PASSWORD_LENGTH):
RadiusUserGroup = swapper.load_model('openwisp_radius', 'RadiusUserGroup')
RadiusGroup = swapper.load_model('openwisp_radius', 'RadiusGroup')
users_list = []
generated_passwords = []
user_group_associations = []

for row in reader:
if len(row) == 5:
# Both formats
if len(row) in [5, 7]:
user, password = self.get_or_create_user(
row, users_list, password_length
)
users_list.append(user)
if password:
generated_passwords.append(password)
# New format
if len(row) == 7:
radius_ugroups = row[6]
groupnames = radius_ugroups.split(';')
for groupname in groupnames:
user_group_associations.append((user, groupname.strip()))
for user in users_list:
self.save_user(user)
for user, groupname in user_group_associations:
if RadiusGroup.objects.filter(name=groupname).exists():
RadiusUserGroup.objects.get_or_create(
user=user,
groupname=groupname,
defaults={'priority': 1}
)
for element in generated_passwords:
username, password, user_email = element
send_mail(
Expand Down Expand Up @@ -973,17 +991,17 @@ def prefix_add(self, prefix, n, password_length=BATCH_DEFAULT_PASSWORD_LENGTH):

def get_or_create_user(self, row, users_list, password_length):
User = get_user_model()
username, password, email, first_name, last_name = row
username, password, email, first_name, last_name, notes, radius_ugroups_string = self._batch_csv_read_row(row)
if email and User.objects.filter(email=email).exists():
user = User.objects.get(email=email)
return user, None
generated_password = None
username, password, email, first_name, last_name = row
username, password, email, first_name, last_name, notes, radius_ugroups_string = self._batch_csv_read_row(row)
if not username and email:
username = email.split('@')[0]
username = find_available_username(username, users_list)
user = User(
username=username, email=email, first_name=first_name, last_name=last_name
username=username, email=email, first_name=first_name, last_name=last_name, notes=notes
)
cleartext_delimiter = 'cleartext$'
if not password:
Expand Down Expand Up @@ -1028,6 +1046,12 @@ def expire(self):
u.is_active = False
u.save()

def _batch_csv_read_row(self, row):
# 5 or 7 fields, for backwards compatibility with previous CSV format.
read_row = lambda row: (*row[:5], *row[5:7], '', '')[:7]
username, password, email, first_name, last_name, notes, radius_ugroups_string = readrow(row)
return username, password, email, first_name, last_name, notes, radius_ugroups_string

def _remove_files(self):
if self.csvfile:
self.csvfile.storage.delete(self.csvfile.name)
Expand Down
18 changes: 18 additions & 0 deletions openwisp_radius/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ def decode_byte_data(data):
return data


def validate_csv_batch_field_radusergroups(rugs_string):
if not rugs_string.strip():
return []
reader = csv.reader([rugs_string], delimiter=' ')
rugs = next(reader)
return rugs


def validate_csvfile(csvfile):
csv_data = csvfile.read()

Expand Down Expand Up @@ -177,6 +185,16 @@ def validate_csvfile(csvfile):
_(error_message.format(str(row_count), error.message))
)
row_count += 1
elif len(row) == 7:
username, password, email, firstname, lastname, notes, link_radius_usergroups = row
try:
validate_csv_batch_field_radusergroups(link_radius_usergroups)
validate_email(email)
except ValidationError as error:
raise ValidationError(
_(error_message.format(str(row_count), error.message))
)
row_count += 1
elif len(row) > 0:
raise ValidationError(
_(error_message.format(str(row_count), 'Improper CSV format.'))
Expand Down
Loading