diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9f8df957..3ae8cc761 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,9 @@ v32.5.3 (unreleased) processed, if all the resources in their extracted directory is mapped/processed. https://github.com/nexB/scancode.io/issues/827 +- Add the ability to clone a project. + https://github.com/nexB/scancode.io/issues/874 + v32.5.2 (2023-08-14) -------------------- diff --git a/scanpipe/forms.py b/scanpipe/forms.py index 813eaea45..ac5b88d92 100644 --- a/scanpipe/forms.py +++ b/scanpipe/forms.py @@ -130,8 +130,7 @@ def __init__(self, *args, **kwargs): name_field.help_text = "The unique name of your project." def clean_name(self): - name = self.cleaned_data["name"] - return " ".join(name.split()) + return " ".join(self.cleaned_data["name"].split()) def save(self, *args, **kwargs): project = super().save(*args, **kwargs) @@ -282,3 +281,45 @@ def update_project_settings(self, project): } project.settings.update(config) project.save(update_fields=["settings"]) + + +class ProjectCloneForm(forms.Form): + clone_name = forms.CharField(widget=forms.TextInput(attrs={"class": "input"})) + copy_inputs = forms.BooleanField( + initial=True, + required=False, + help_text="Input files located in the input/ work directory will be copied.", + widget=forms.CheckboxInput(attrs={"class": "checkbox mr-1"}), + ) + copy_pipelines = forms.BooleanField( + initial=True, + required=False, + help_text="All pipelines assigned to the original project will be copied over.", + widget=forms.CheckboxInput(attrs={"class": "checkbox mr-1"}), + ) + copy_settings = forms.BooleanField( + initial=True, + required=False, + help_text="All project settings will be copied.", + widget=forms.CheckboxInput(attrs={"class": "checkbox mr-1"}), + ) + execute_now = forms.BooleanField( + label="Execute copied pipeline(s) now", + initial=False, + required=False, + help_text="Copied pipelines will be directly executed.", + ) + + def __init__(self, instance, *args, **kwargs): + self.project = instance + super().__init__(*args, **kwargs) + self.fields["clone_name"].initial = f"{self.project.name} clone" + + def clean_clone_name(self): + clone_name = self.cleaned_data.get("clone_name") + if Project.objects.filter(name=clone_name).exists(): + raise ValidationError("Project with this name already exists.") + return clone_name + + def save(self, *args, **kwargs): + return self.project.clone(**self.cleaned_data) diff --git a/scanpipe/models.py b/scanpipe/models.py index db5efb82d..d736b24bc 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -642,6 +642,31 @@ def reset(self, keep_input=True): self.setup_work_directory() + def clone( + self, + clone_name, + copy_inputs=False, + copy_pipelines=False, + copy_settings=False, + execute_now=False, + ): + """Clone this project using the provided ``clone_name`` as new project name.""" + cloned_project = Project.objects.create( + name=clone_name, + input_sources=self.input_sources if copy_inputs else {}, + settings=self.settings if copy_settings else {}, + ) + + if copy_inputs: + for input_location in self.inputs(): + cloned_project.copy_input_from(input_location) + + if copy_pipelines: + for run in self.runs.all(): + cloned_project.add_pipeline(run.pipeline_name, execute_now) + + return cloned_project + def _raise_if_run_in_progress(self): """ Raise a `RunInProgressError` exception if one of the project related run is diff --git a/scanpipe/templates/scanpipe/includes/clone_modal.html b/scanpipe/templates/scanpipe/includes/clone_modal.html new file mode 100644 index 000000000..7e24603f7 --- /dev/null +++ b/scanpipe/templates/scanpipe/includes/clone_modal.html @@ -0,0 +1,18 @@ +
Clone this project
+ +{{ form.clone_name.help_text }}
+{{ form.copy_inputs.help_text }}
+{{ form.copy_pipelines.help_text }}
+{{ form.copy_settings.help_text }}
+{{ form.execute_now.help_text }}
+