This is an automated email from the ASF dual-hosted git repository.

ephraimanierobi pushed a commit to branch v2-7-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 2a0106e4edf67c5905ebfcb82a6008662ae0f7ad
Author: Augusto Hidalgo <augus...@google.com>
AuthorDate: Wed Aug 16 14:33:56 2023 +0200

    Add read only validation to read only fields (#33413)
    
    * Add read only validation to read only fields
    
    Add read only validation to DagRunEditForm and TaskInstanceEditForm
    read only fields.
    
    * Improve docstring
    
    ---------
    
    Co-authored-by: Hussein Awala <huss...@awala.fr>
    (cherry picked from commit b7a46c970d638028a4a7643ad000dcee951fb9ef)
---
 airflow/www/forms.py         | 54 +++++++++++++++++++++++++++++++-------------
 airflow/www/validators.py    | 11 +++++++++
 tests/www/test_validators.py | 11 +++++++++
 3 files changed, 60 insertions(+), 16 deletions(-)

diff --git a/airflow/www/forms.py b/airflow/www/forms.py
index 61b203804a..d36a189828 100644
--- a/airflow/www/forms.py
+++ b/airflow/www/forms.py
@@ -41,7 +41,7 @@ from airflow.configuration import conf
 from airflow.providers_manager import ProvidersManager
 from airflow.utils import timezone
 from airflow.utils.types import DagRunType
-from airflow.www.validators import ValidKey
+from airflow.www.validators import ReadOnly, ValidKey
 from airflow.www.widgets import (
     AirflowDateTimePickerROWidget,
     AirflowDateTimePickerWidget,
@@ -121,38 +121,54 @@ class DateTimeWithNumRunsForm(FlaskForm):
 class DagRunEditForm(DynamicForm):
     """Form for editing DAG Run.
 
-    We don't actually want to allow editing, so everything is read-only here.
+    Only note field is editable, so everything else is read-only here.
     """
 
-    dag_id = StringField(lazy_gettext("Dag Id"), widget=BS3TextFieldROWidget())
-    start_date = DateTimeWithTimezoneField(lazy_gettext("Start Date"), 
widget=AirflowDateTimePickerROWidget())
-    end_date = DateTimeWithTimezoneField(lazy_gettext("End Date"), 
widget=AirflowDateTimePickerROWidget())
-    run_id = StringField(lazy_gettext("Run Id"), widget=BS3TextFieldROWidget())
-    state = StringField(lazy_gettext("State"), widget=BS3TextFieldROWidget())
+    dag_id = StringField(lazy_gettext("Dag Id"), validators=[ReadOnly()], 
widget=BS3TextFieldROWidget())
+    start_date = DateTimeWithTimezoneField(
+        lazy_gettext("Start Date"), validators=[ReadOnly()], 
widget=AirflowDateTimePickerROWidget()
+    )
+    end_date = DateTimeWithTimezoneField(
+        lazy_gettext("End Date"), validators=[ReadOnly()], 
widget=AirflowDateTimePickerROWidget()
+    )
+    run_id = StringField(lazy_gettext("Run Id"), validators=[ReadOnly()], 
widget=BS3TextFieldROWidget())
+    state = StringField(lazy_gettext("State"), validators=[ReadOnly()], 
widget=BS3TextFieldROWidget())
     execution_date = DateTimeWithTimezoneField(
         lazy_gettext("Logical Date"),
+        validators=[ReadOnly()],
         widget=AirflowDateTimePickerROWidget(),
     )
-    conf = TextAreaField(lazy_gettext("Conf"), widget=BS3TextAreaROWidget())
+    conf = TextAreaField(lazy_gettext("Conf"), validators=[ReadOnly()], 
widget=BS3TextAreaROWidget())
     note = TextAreaField(lazy_gettext("User Note"), 
widget=BS3TextAreaFieldWidget())
 
     def populate_obj(self, item):
-        """Populates the attributes of the passed obj with data from the 
form's fields."""
-        super().populate_obj(item)
+        """Populates the attributes of the passed obj with data from the 
form's not-read-only fields."""
+        for name, field in self._fields.items():
+            if not field.flags.readonly:
+                field.populate_obj(item, name)
         item.run_type = DagRunType.from_run_id(item.run_id)
         if item.conf:
             item.conf = json.loads(item.conf)
 
 
 class TaskInstanceEditForm(DynamicForm):
-    """Form for editing TaskInstance."""
+    """Form for editing TaskInstance.
 
-    dag_id = StringField(lazy_gettext("Dag Id"), validators=[InputRequired()], 
widget=BS3TextFieldROWidget())
+    Only note and state fields are editable, so everything else is read-only 
here.
+    """
+
+    dag_id = StringField(
+        lazy_gettext("Dag Id"), validators=[InputRequired(), ReadOnly()], 
widget=BS3TextFieldROWidget()
+    )
     task_id = StringField(
-        lazy_gettext("Task Id"), validators=[InputRequired()], 
widget=BS3TextFieldROWidget()
+        lazy_gettext("Task Id"), validators=[InputRequired(), ReadOnly()], 
widget=BS3TextFieldROWidget()
+    )
+    start_date = DateTimeWithTimezoneField(
+        lazy_gettext("Start Date"), validators=[ReadOnly()], 
widget=AirflowDateTimePickerROWidget()
+    )
+    end_date = DateTimeWithTimezoneField(
+        lazy_gettext("End Date"), validators=[ReadOnly()], 
widget=AirflowDateTimePickerROWidget()
     )
-    start_date = DateTimeWithTimezoneField(lazy_gettext("Start Date"), 
widget=AirflowDateTimePickerROWidget())
-    end_date = DateTimeWithTimezoneField(lazy_gettext("End Date"), 
widget=AirflowDateTimePickerROWidget())
     state = SelectField(
         lazy_gettext("State"),
         choices=(
@@ -167,10 +183,16 @@ class TaskInstanceEditForm(DynamicForm):
     execution_date = DateTimeWithTimezoneField(
         lazy_gettext("Logical Date"),
         widget=AirflowDateTimePickerROWidget(),
-        validators=[InputRequired()],
+        validators=[InputRequired(), ReadOnly()],
     )
     note = TextAreaField(lazy_gettext("User Note"), 
widget=BS3TextAreaFieldWidget())
 
+    def populate_obj(self, item):
+        """Populates the attributes of the passed obj with data from the 
form's not-read-only fields."""
+        for name, field in self._fields.items():
+            if not field.flags.readonly:
+                field.populate_obj(item, name)
+
 
 @cache
 def create_connection_form_class() -> type[DynamicForm]:
diff --git a/airflow/www/validators.py b/airflow/www/validators.py
index 8dbe640e8c..ce273308df 100644
--- a/airflow/www/validators.py
+++ b/airflow/www/validators.py
@@ -97,3 +97,14 @@ class ValidKey:
                 helpers.validate_key(field.data, self.max_length)
             except Exception as e:
                 raise ValidationError(str(e))
+
+
+class ReadOnly:
+    """Adds readonly flag to a field.
+
+    When using this you normally will need to override the form's populate_obj 
method,
+    so field.populate_obj is not called for read-only fields.
+    """
+
+    def __call__(self, form, field):
+        field.flags.readonly = True
diff --git a/tests/www/test_validators.py b/tests/www/test_validators.py
index 718091214c..e77459f1d8 100644
--- a/tests/www/test_validators.py
+++ b/tests/www/test_validators.py
@@ -155,3 +155,14 @@ class TestValidKey:
             match=r"The key has to be less than [0-9]+ characters",
         ):
             self._validate()
+
+
+class TestReadOnly:
+    def setup_method(self):
+        self.form_read_only_field_mock = mock.MagicMock(data="readOnlyField")
+        self.form_mock = mock.MagicMock(spec_set=dict)
+
+    def test_read_only_validator(self):
+        validator = validators.ReadOnly()
+        assert validator(self.form_mock, self.form_read_only_field_mock) is 
None
+        assert self.form_read_only_field_mock.flags.readonly is True

Reply via email to