Skip to content

Commit 5aac685

Browse files
committed
[feature] csv import add notes and user_groups field #396
Batch csv import now accepts notes and setting RADIUS groups for each user. The previous format is still accepted for backwards compatibility. Fixes #396
1 parent 2f6a7cb commit 5aac685

File tree

3 files changed

+58
-4
lines changed

3 files changed

+58
-4
lines changed

docs/user/importing_users.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ many features included in it such as:
1414
Usernames are generated from the email address whereas passwords are
1515
generated randomly and their lengths can be customized.
1616
- Passwords are accepted in both clear-text and hash formats from the CSV.
17+
- Set the RADIUS user groups that the user will belong to.
1718
- Send mails to users whose passwords have been generated automatically.
1819

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

2728
The CSV shall be of the format:
2829

30+
::
31+
32+
username,password,email,firstname,lastname,notes,user_groups
33+
34+
`user_groups` consists of one or more radius group names separated by a semicolon.
35+
Inserting groups that don't exist will silently fail.
36+
37+
The previous format is also supported for backwards compatibility:
38+
2939
::
3040

3141
username,password,email,firstname,lastname
3242

43+
OpenWISP will recognize the correct format automatically.
44+
3345
Imported users with hashed passwords
3446
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3547

openwisp_radius/base/models.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -925,18 +925,36 @@ def clean(self):
925925
super().clean()
926926

927927
def add(self, reader, password_length=BATCH_DEFAULT_PASSWORD_LENGTH):
928+
RadiusUserGroup = swapper.load_model('openwisp_radius', 'RadiusUserGroup')
929+
RadiusGroup = swapper.load_model('openwisp_radius', 'RadiusGroup')
928930
users_list = []
929931
generated_passwords = []
932+
user_group_associations = []
933+
930934
for row in reader:
931-
if len(row) == 5:
935+
# Both formats
936+
if len(row) in [5, 7]:
932937
user, password = self.get_or_create_user(
933938
row, users_list, password_length
934939
)
935940
users_list.append(user)
936941
if password:
937942
generated_passwords.append(password)
943+
# New format
944+
if len(row) == 7:
945+
radius_ugroups = row[6]
946+
groupnames = radius_ugroups.split(';')
947+
for groupname in groupnames:
948+
user_group_associations.append((user, groupname.strip()))
938949
for user in users_list:
939950
self.save_user(user)
951+
for user, groupname in user_group_associations:
952+
if RadiusGroup.objects.filter(name=groupname).exists():
953+
RadiusUserGroup.objects.get_or_create(
954+
user=user,
955+
groupname=groupname,
956+
defaults={'priority': 1}
957+
)
940958
for element in generated_passwords:
941959
username, password, user_email = element
942960
send_mail(
@@ -969,17 +987,17 @@ def prefix_add(self, prefix, n, password_length=BATCH_DEFAULT_PASSWORD_LENGTH):
969987

970988
def get_or_create_user(self, row, users_list, password_length):
971989
User = get_user_model()
972-
username, password, email, first_name, last_name = row
990+
username, password, email, first_name, last_name, notes, radius_ugroups_string = self._batch_csv_read_row(row)
973991
if email and User.objects.filter(email=email).exists():
974992
user = User.objects.get(email=email)
975993
return user, None
976994
generated_password = None
977-
username, password, email, first_name, last_name = row
995+
username, password, email, first_name, last_name, notes, radius_ugroups_string = self._batch_csv_read_row(row)
978996
if not username and email:
979997
username = email.split('@')[0]
980998
username = find_available_username(username, users_list)
981999
user = User(
982-
username=username, email=email, first_name=first_name, last_name=last_name
1000+
username=username, email=email, first_name=first_name, last_name=last_name, notes=notes
9831001
)
9841002
cleartext_delimiter = 'cleartext$'
9851003
if not password:
@@ -1024,6 +1042,12 @@ def expire(self):
10241042
u.is_active = False
10251043
u.save()
10261044

1045+
def _batch_csv_read_row(self, row):
1046+
# 5 or 7 fields, for backwards compatibility with previous CSV format.
1047+
read_row = lambda row: (*row[:5], *row[5:7], '', '')[:7]
1048+
username, password, email, first_name, last_name, notes, radius_ugroups_string = readrow(row)
1049+
return username, password, email, first_name, last_name, notes, radius_ugroups_string
1050+
10271051
def _remove_files(self):
10281052
if self.csvfile:
10291053
self.csvfile.storage.delete(self.csvfile.name)

openwisp_radius/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ def decode_byte_data(data):
150150
return data
151151

152152

153+
def validate_csv_batch_field_radusergroups(rugs_string):
154+
if not rugs_string.strip():
155+
return []
156+
reader = csv.reader([rugs_string], delimiter=' ')
157+
rugs = next(reader)
158+
return rugs
159+
160+
153161
def validate_csvfile(csvfile):
154162
csv_data = csvfile.read()
155163

@@ -177,6 +185,16 @@ def validate_csvfile(csvfile):
177185
_(error_message.format(str(row_count), error.message))
178186
)
179187
row_count += 1
188+
elif len(row) == 7:
189+
username, password, email, firstname, lastname, notes, link_radius_usergroups = row
190+
try:
191+
validate_csv_batch_field_radusergroups(link_radius_usergroups)
192+
validate_email(email)
193+
except ValidationError as error:
194+
raise ValidationError(
195+
_(error_message.format(str(row_count), error.message))
196+
)
197+
row_count += 1
180198
elif len(row) > 0:
181199
raise ValidationError(
182200
_(error_message.format(str(row_count), 'Improper CSV format.'))

0 commit comments

Comments
 (0)