|
10 | 10 | import django |
11 | 11 | import phonenumbers |
12 | 12 | import swapper |
| 13 | +from asgiref.sync import async_to_sync |
| 14 | +from channels.layers import get_channel_layer |
13 | 15 | from django.conf import settings |
14 | 16 | from django.contrib.auth import get_user_model |
15 | 17 | from django.core.cache import cache |
|
23 | 25 | from django.utils.translation import gettext_lazy as _ |
24 | 26 | from jsonfield import JSONField |
25 | 27 | from model_utils.fields import AutoLastModifiedField |
| 28 | +from openwisp_notifications.signals import notify |
26 | 29 | from phonenumber_field.modelfields import PhoneNumberField |
27 | 30 | from private_storage.fields import PrivateFileField |
28 | 31 |
|
29 | 32 | from openwisp_radius.registration import ( |
30 | 33 | REGISTRATION_METHOD_CHOICES, |
31 | 34 | get_registration_choices, |
32 | 35 | ) |
| 36 | +from openwisp_radius.tasks import process_radius_batch |
33 | 37 | from openwisp_users.mixins import OrgMixin |
34 | 38 | from openwisp_utils.base import KeyField, TimeStampedEditableModel, UUIDModel |
35 | 39 | from openwisp_utils.fields import ( |
@@ -865,13 +869,31 @@ def _get_csv_file_location(instance, filename): |
865 | 869 |
|
866 | 870 |
|
867 | 871 | class AbstractRadiusBatch(OrgMixin, TimeStampedEditableModel): |
| 872 | + PENDING = "pending" |
| 873 | + PROCESSING = "processing" |
| 874 | + COMPLETED = "completed" |
| 875 | + FAILED = "failed" |
| 876 | + |
| 877 | + BATCH_STATUS_CHOICES = ( |
| 878 | + (PENDING, _("Pending")), |
| 879 | + (PROCESSING, _("Processing")), |
| 880 | + (COMPLETED, _("Completed")), |
| 881 | + (FAILED, _("Failed")), |
| 882 | + ) |
| 883 | + |
868 | 884 | strategy = models.CharField( |
869 | 885 | _("strategy"), |
870 | 886 | max_length=16, |
871 | 887 | choices=_STRATEGIES, |
872 | 888 | db_index=True, |
873 | 889 | help_text=_("Import users from a CSV or generate using a prefix"), |
874 | 890 | ) |
| 891 | + status = models.CharField( |
| 892 | + max_length=16, |
| 893 | + choices=BATCH_STATUS_CHOICES, |
| 894 | + default=PENDING, |
| 895 | + db_index=True, |
| 896 | + ) |
875 | 897 | name = models.CharField( |
876 | 898 | verbose_name=_("name"), |
877 | 899 | max_length=128, |
@@ -1064,6 +1086,71 @@ def _remove_files(self): |
1064 | 1086 | if self.csvfile: |
1065 | 1087 | self.csvfile.storage.delete(self.csvfile.name) |
1066 | 1088 |
|
| 1089 | + def schedule_processing(self, number_of_users=0): |
| 1090 | + items_to_process = 0 |
| 1091 | + if self.strategy == "prefix": |
| 1092 | + items_to_process = number_of_users |
| 1093 | + elif self.strategy == "csv" and self.csvfile: |
| 1094 | + try: |
| 1095 | + csv_data = self.csvfile.read() |
| 1096 | + decoded_data = decode_byte_data(csv_data) |
| 1097 | + items_to_process = sum(1 for row in csv.reader(StringIO(decoded_data))) |
| 1098 | + self.csvfile.seek(0) |
| 1099 | + except Exception as e: |
| 1100 | + logger.error(f"Could not count rows in CSV for batch {self.pk}: {e}") |
| 1101 | + items_to_process = app_settings.BATCH_ASYNC_THRESHOLD |
| 1102 | + is_async = items_to_process >= app_settings.BATCH_ASYNC_THRESHOLD |
| 1103 | + if is_async: |
| 1104 | + process_radius_batch.delay(self.pk, number_of_users=number_of_users) |
| 1105 | + else: |
| 1106 | + self.process(number_of_users=number_of_users) |
| 1107 | + return is_async |
| 1108 | + |
| 1109 | + def process(self, number_of_users=0, is_async=False): |
| 1110 | + channel_layer = get_channel_layer() |
| 1111 | + group_name = f"radius_batch_{self.pk}" |
| 1112 | + try: |
| 1113 | + self.status = self.PROCESSING |
| 1114 | + self.save(update_fields=["status"]) |
| 1115 | + if self.strategy == "prefix": |
| 1116 | + self.prefix_add(self.prefix, number_of_users) |
| 1117 | + elif self.strategy == "csv": |
| 1118 | + self.csvfile_upload() |
| 1119 | + self.status = self.COMPLETED |
| 1120 | + self.save(update_fields=["status"]) |
| 1121 | + if is_async: |
| 1122 | + notify.send( |
| 1123 | + type="generic_message", |
| 1124 | + level="success", |
| 1125 | + message=_( |
| 1126 | + f'The batch creation operation for "{self.name}" ' |
| 1127 | + "has completed successfully." |
| 1128 | + ), |
| 1129 | + sender=self.organization, |
| 1130 | + target=self, |
| 1131 | + description=_(f"Number of users processed: {self.users.count()}."), |
| 1132 | + ) |
| 1133 | + except Exception as e: |
| 1134 | + logger.error( |
| 1135 | + "RadiusBatch %s failed during processing:", self.pk, exc_info=True |
| 1136 | + ) |
| 1137 | + self.status = self.FAILED |
| 1138 | + self.save(update_fields=["status"]) |
| 1139 | + notify.send( |
| 1140 | + type="generic_message", |
| 1141 | + level="error", |
| 1142 | + message=_(f'The batch creation operation for "{self.name}" failed.'), |
| 1143 | + sender=self.organization, |
| 1144 | + target=self, |
| 1145 | + description=_( |
| 1146 | + f"An error occurred while processing the batch.\n\n" f"Error: {e}" |
| 1147 | + ), |
| 1148 | + ) |
| 1149 | + finally: |
| 1150 | + async_to_sync(channel_layer.group_send)( |
| 1151 | + group_name, {"type": "batch_status_update", "status": self.status} |
| 1152 | + ) |
| 1153 | + |
1067 | 1154 |
|
1068 | 1155 | class AbstractRadiusToken(OrgMixin, TimeStampedEditableModel, models.Model): |
1069 | 1156 | # key field is a primary key so additional id field will be redundant |
|
0 commit comments