@@ -563,6 +563,8 @@ def save(self, *args, **kwargs):
563563 output = super ().save (* args , ** kwargs )
564564 if adding :
565565 self ._schedule_command ()
566+ if self .batch_command_id and self .status != "in-progress" :
567+ self .batch_command .calculate_and_update_status ()
566568 return output
567569
568570 def _save_without_resurrecting (self ):
@@ -738,6 +740,8 @@ class AbstractBatchCommand(TimeStampedEditableModel):
738740 organization = models .ForeignKey (
739741 get_model_name ("openwisp_users" , "Organization" ),
740742 on_delete = models .CASCADE ,
743+ blank = True ,
744+ null = True ,
741745 )
742746 status = models .CharField (
743747 max_length = 12 , choices = STATUS_CHOICES , default = STATUS_CHOICES [0 ][0 ]
@@ -761,37 +765,55 @@ class AbstractBatchCommand(TimeStampedEditableModel):
761765 null = True ,
762766 verbose_name = _ ("location" ),
763767 )
764- include_all_devices = models .BooleanField (default = False )
768+ devices = models .ManyToManyField (
769+ get_model_name ("config" , "Device" ),
770+ blank = True ,
771+ verbose_name = _ ("devices" ),
772+ )
765773 total_devices = models .PositiveIntegerField (default = 0 )
766774 successful = models .PositiveIntegerField (default = 0 )
767775 failed = models .PositiveIntegerField (default = 0 )
768776 cancelled = models .PositiveIntegerField (default = 0 )
769777
770778 class Meta :
771779 abstract = True
772- verbose_name = _ ("Batch command operation " )
773- verbose_name_plural = _ ("Batch command operations " )
780+ verbose_name = _ ("Batch command" )
781+ verbose_name_plural = _ ("Batch commands " )
774782
775783 def clean (self ):
776784 super ().clean ()
777- if self .group and self .group .organization != self .organization :
778- raise ValidationError (
779- {
780- "group" : _ (
781- "The organization of the group doesn't match "
782- "the organization of the batch command operation"
783- )
784- }
785- )
786- if self .location and self .location .organization != self .organization :
787- raise ValidationError (
788- {
789- "location" : _ (
790- "The organization of the location doesn't match "
791- "the organization of the batch command operation"
785+ if self .organization_id :
786+ if self .group and self .group .organization != self .organization :
787+ raise ValidationError (
788+ {
789+ "group" : _ (
790+ "The organization of the group doesn't match "
791+ "the organization of the batch command operation"
792+ )
793+ }
794+ )
795+ if self .location and self .location .organization != self .organization :
796+ raise ValidationError (
797+ {
798+ "location" : _ (
799+ "The organization of the location doesn't match "
800+ "the organization of the batch command operation"
801+ )
802+ }
803+ )
804+ if self .pk and self .devices .exists ():
805+ org_mismatch = self .devices .exclude (
806+ organization = self .organization
807+ ).exists ()
808+ if org_mismatch :
809+ raise ValidationError (
810+ {
811+ "devices" : _ (
812+ "All devices must belong to the same "
813+ "organization as the batch command."
814+ )
815+ }
792816 )
793- }
794- )
795817 allowed = dict (
796818 AbstractCommand .get_org_allowed_commands (
797819 organization_id = self .organization_id
@@ -813,14 +835,16 @@ def clean(self):
813835 raise ValidationError ({"command_input" : e .message })
814836
815837 def resolve_devices (self ):
838+ if self .pk and self .devices .exists ():
839+ return self .devices .all ()
816840 Device = load_model ("config" , "Device" )
817- qs = Device .objects .filter (organization = self .organization )
841+ qs = Device .objects .all ()
842+ if self .organization_id :
843+ qs = qs .filter (organization = self .organization )
818844 if self .group :
819845 qs = qs .filter (group = self .group )
820846 if self .location :
821847 qs = qs .filter (location = self .location )
822- if not self .include_all_devices and not self .group and not self .location :
823- qs = qs .none ()
824848 return qs
825849
826850 def launch (self ):
0 commit comments