Skip to content

Mixins

hypha.apply.funds.models.mixins

UnusedFieldException

Bases: Exception

AccessFormData

Mixin for interacting with form data from streamfields

requires
  • form_data > jsonfield containing the submitted data
  • form_fields > streamfield containing the original form fields

stream_file_class class-attribute instance-attribute

stream_file_class = PrivateStreamFieldFile

storage_class class-attribute instance-attribute

storage_class = PrivateStorage

raw_data property

raw_data

question_field_ids property

question_field_ids

file_field_ids property

file_field_ids

question_text_field_ids property

question_text_field_ids

first_group_question_text_field_ids property

first_group_question_text_field_ids

raw_fields property

raw_fields

fields property

fields

named_blocks property

named_blocks

normal_blocks property

normal_blocks

group_toggle_blocks property

group_toggle_blocks

first_group_normal_text_blocks property

first_group_normal_text_blocks

stream_file classmethod

stream_file(instance, field, file)
Source code in hypha/apply/funds/models/mixins.py
@classmethod
def stream_file(cls, instance, field, file):
    if not file:
        return []
    if isinstance(file, cls.stream_file_class):
        return file
    if isinstance(file, File):
        return cls.stream_file_class(
            instance, field, file, name=file.name, storage=cls.storage_class()
        )

    if isinstance(file, PlaceholderUploadedFile):
        return cls.stream_file_class(
            instance,
            field,
            None,
            name=file.file_id,
            filename=file.name,
            storage=cls.storage_class(),
        )

    # This fixes a backwards compatibility issue with #507
    # Once every application has been re-saved it should be possible to remove it
    if "path" in file:
        file["filename"] = file["name"]
        file["name"] = file["path"]
    return cls.stream_file_class(
        instance,
        field,
        None,
        name=file["name"],
        filename=file.get("filename"),
        storage=cls.storage_class(),
    )

process_file classmethod

process_file(instance, field, file)
Source code in hypha/apply/funds/models/mixins.py
@classmethod
def process_file(cls, instance, field, file):
    if isinstance(file, list):
        return [cls.stream_file(instance, field, f) for f in file]
    else:
        return cls.stream_file(instance, field, file)

process_file_data

process_file_data(data, latest_existing_data=None)
Source code in hypha/apply/funds/models/mixins.py
def process_file_data(self, data, latest_existing_data=None):
    for field in self.form_fields:
        if isinstance(field.block, UploadableMediaBlock):
            # {field_id}-uploads contains all files data for that field
            # get all uploaded files(existing + new)
            uploaded_files = data.get(field.id + "-uploads", "")

            try:
                uploads_data = json.loads(uploaded_files)
            except json.JSONDecodeError:
                uploads_data = []

            new_file_upload = False
            for file_data in uploads_data:
                # id can be a path or a uuid, where path can exist only for existing files so uuid means a new file
                if self._is_valid_uuid(file_data.get("id", None)):
                    new_file_upload = True  # if any new file is uploaded we have to process and save the files

            # get existing files from instance
            if latest_existing_data:
                existing_file = latest_existing_data.get(field.id, [])
            else:
                existing_file = None

            # uploaded file ids to check for removed/deleted files
            uploaded_file_ids = [f["id"] for f in uploads_data if "id" in f]

            # if any existing file id is not in uploaded file ids that means it has been deleted
            # remove deleted files from existing files
            if existing_file:
                if isinstance(existing_file, list):
                    existing_file = [
                        f for f in existing_file if f.name in uploaded_file_ids
                    ]
                else:
                    if existing_file.name not in uploaded_file_ids:
                        existing_file = None

            # save only if there is any new file uploaded or no existing file
            if not existing_file or new_file_upload:
                new_file = data.get(field.id, [])
                new_stream_file = self.process_file(self, field, new_file)
                try:
                    new_stream_file.save()
                except (AttributeError, FileNotFoundError):
                    try:
                        for f in new_stream_file:
                            f.save()
                    except FileNotFoundError:
                        pass
                self.form_data[field.id] = new_stream_file
            else:
                self.form_data[field.id] = existing_file

extract_files

extract_files()
Source code in hypha/apply/funds/models/mixins.py
def extract_files(self):
    files = {}
    for field in self.form_fields:
        if isinstance(field.block, UploadableMediaBlock):
            files[field.id] = self.data(field.id) or []
    return files

from_db classmethod

from_db(db, field_names, values)
Source code in hypha/apply/funds/models/mixins.py
@classmethod
def from_db(cls, db, field_names, values):
    instance = super().from_db(db, field_names, values)
    if "form_data" in field_names:
        # When the form_data is loaded from the DB deserialise it
        instance.form_data = cls.deserialised_data(
            instance, instance.form_data, instance.form_fields
        )
    return instance

deserialised_data classmethod

deserialised_data(instance, data, form_fields)
Source code in hypha/apply/funds/models/mixins.py
@classmethod
def deserialised_data(cls, instance, data, form_fields):
    # Converts the file dicts into actual file objects
    data = data.copy()
    # PERFORMANCE NOTE:
    # Do not attempt to iterate over form_fields - that will fully instantiate the form_fields
    # including any sub queries that they do
    for i, field_data in enumerate(form_fields.raw_data):
        block = form_fields.stream_block.child_blocks[field_data["type"]]
        if isinstance(block, UploadableMediaBlock):
            field_id = field_data.get("id")
            if field_id:
                field = form_fields[i]
                file = data.get(field_id, [])
                data[field_id] = cls.process_file(instance, field, file)
    return data

get_definitive_id

get_definitive_id(id)
Source code in hypha/apply/funds/models/mixins.py
def get_definitive_id(self, id):
    if id in self.named_blocks:
        return self.named_blocks[id]
    return id

field

field(id)
Source code in hypha/apply/funds/models/mixins.py
def field(self, id):
    definitive_id = self.get_definitive_id(id)
    try:
        return self.raw_fields[definitive_id]
    except KeyError:
        raise UnusedFieldException(id) from None

data

data(id)
Source code in hypha/apply/funds/models/mixins.py
def data(self, id):
    definitive_id = self.get_definitive_id(id)
    try:
        return self.raw_data[definitive_id]
    except KeyError:
        # We have most likely progressed application forms so the data isn't in form_data
        return None

get_serialize_multi_inputs_answer

get_serialize_multi_inputs_answer(field)
Source code in hypha/apply/funds/models/mixins.py
def get_serialize_multi_inputs_answer(self, field):
    number_of_inputs = field.value.get("number_of_inputs")
    answers = [self.data(field.id + "_" + str(i)) for i in range(number_of_inputs)]
    data = ", ".join(filter(None, answers))
    return data

serialize

serialize(field_id)

Serialize a single answer.

Never redacted: unlike render_answer() this does not consult is_pii, so everything reached through it is for staff-only views and exports.

Source code in hypha/apply/funds/models/mixins.py
def serialize(self, field_id):
    """Serialize a single answer.

    Never redacted: unlike `render_answer()` this does not consult `is_pii`,
    so everything reached through it is for staff-only views and exports.
    """
    field = self.field(field_id)
    if isinstance(field.block, MultiInputCharFieldBlock):
        data = self.get_serialize_multi_inputs_answer(field)
    else:
        data = self.data(field_id)
    return field.render(
        context={
            "serialize": True,
            "data": data,
        }
    )

get_multi_inputs_answer

get_multi_inputs_answer(field, include_question=False, show_pii_marker=False)
Source code in hypha/apply/funds/models/mixins.py
def get_multi_inputs_answer(
    self, field, include_question=False, show_pii_marker=False
):
    number_of_inputs = field.value.get("number_of_inputs")
    answers = [self.data(field.id + "_" + str(i)) for i in range(number_of_inputs)]

    render_data = [
        field.render(
            context={
                "data": answer,
                "include_question": include_question if i == 0 else False,
                "show_pii_marker": show_pii_marker,
            }
        )
        for i, answer in enumerate(filter(None, answers))
    ]
    joined = "".join(render_data).replace("</section>", "")
    if include_question and render_data:
        return joined + "</section>"
    return joined

field_is_pii staticmethod

field_is_pii(field)

Has this field been marked as containing personal information?

Source code in hypha/apply/funds/models/mixins.py
@staticmethod
def field_is_pii(field) -> bool:
    """Has this field been marked as containing personal information?"""
    try:
        return bool(field.value.get("is_pii"))
    except AttributeError:
        # Blocks such as text_markup hold a scalar rather than a StructValue.
        return False

render_answer

render_answer(field_id, include_question=False, redact_pii=False, mark_pii=False)
Source code in hypha/apply/funds/models/mixins.py
def render_answer(
    self, field_id, include_question=False, redact_pii=False, mark_pii=False
):
    try:
        field = self.field(field_id)
    except UnusedFieldException:
        return "-"
    is_pii = self.field_is_pii(field)
    if redact_pii and is_pii:
        return render_to_string(
            "stream_forms/render_redacted_field.html",
            {
                "value": field.block.get_display_value(field.value),
                "include_question": include_question,
            },
        )
    show_pii_marker = mark_pii and is_pii
    if isinstance(field.block, MultiInputCharFieldBlock):
        render_data = self.get_multi_inputs_answer(
            field, include_question, show_pii_marker
        )
        return render_data
    else:
        data = self.data(field_id)
    # Some migrated content have empty address.
    if not data:
        return field.render(
            context={
                "data": "",
                "include_question": include_question,
                "show_pii_marker": show_pii_marker,
            }
        )
    return field.render(
        context={
            "data": data,
            "include_question": include_question,
            "show_pii_marker": show_pii_marker,
        }
    )

render_answers

render_answers(redact_pii=False, mark_pii=False)
Source code in hypha/apply/funds/models/mixins.py
def render_answers(self, redact_pii=False, mark_pii=False):
    # Returns a list of the rendered answers
    return [
        self.render_answer(
            field_id,
            include_question=True,
            redact_pii=redact_pii,
            mark_pii=mark_pii,
        )
        for field_id in self.normal_blocks
    ]

render_first_group_text_answers

render_first_group_text_answers()

Render the text answers of the first group, never redacted.

Answers marked as personal information are rendered in the clear, so only use this where the view is already restricted to staff.

Source code in hypha/apply/funds/models/mixins.py
def render_first_group_text_answers(self):
    """Render the text answers of the first group, never redacted.

    Answers marked as personal information are rendered in the clear, so
    only use this where the view is already restricted to staff.
    """
    return [
        self.render_answer(field_id, include_question=True)
        for field_id in self.first_group_normal_text_blocks
    ]

render_text_blocks_answers

render_text_blocks_answers()

Render the answers of type text, never redacted.

Answers marked as personal information are rendered in the clear, so only use this where the view is already restricted to staff. Its one caller is the revision comparison view, which is staff_required.

Source code in hypha/apply/funds/models/mixins.py
def render_text_blocks_answers(self):
    """Render the answers of type text, never redacted.

    Answers marked as personal information are rendered in the clear, so
    only use this where the view is already restricted to staff. Its one
    caller is the revision comparison view, which is `staff_required`.
    """
    return [
        self.render_answer(field_id, include_question=True)
        for field_id in self.question_text_field_ids
        if field_id not in self.named_blocks
    ]

output_answers

output_answers(redact_pii=False, mark_pii=False)
Source code in hypha/apply/funds/models/mixins.py
def output_answers(self, redact_pii=False, mark_pii=False):
    # Returns a safe string of the rendered answers
    return mark_safe(
        "".join(self.render_answers(redact_pii=redact_pii, mark_pii=mark_pii))
    )

output_text_answers

output_text_answers()
Source code in hypha/apply/funds/models/mixins.py
def output_text_answers(self):
    return mark_safe("".join(self.render_text_blocks_answers()))

output_first_group_text_answers

output_first_group_text_answers()
Source code in hypha/apply/funds/models/mixins.py
def output_first_group_text_answers(self):
    return mark_safe("".join(self.render_first_group_text_answers()))

get_answer_from_label

get_answer_from_label(label)
Source code in hypha/apply/funds/models/mixins.py
def get_answer_from_label(self, label):
    for field_id in self.question_text_field_ids:
        if field_id not in self.named_blocks:
            question_field = self.serialize(field_id)
            if label.lower() in question_field["question"].lower():
                if isinstance(question_field["answer"], str):
                    answer = question_field["answer"]
                else:
                    answer = ",".join(question_field["answer"])
                if answer and not answer == "N":
                    return answer
    return None

get_text_questions_answers_as_dict

get_text_questions_answers_as_dict()

Map each text question to its answer, never redacted.

Goes through serialize(), which ignores is_pii, so answers marked as personal information are included in the clear. Its one caller is the submission PDF download, which is staff_required.

Source code in hypha/apply/funds/models/mixins.py
def get_text_questions_answers_as_dict(self):
    """Map each text question to its answer, never redacted.

    Goes through `serialize()`, which ignores `is_pii`, so answers marked as
    personal information are included in the clear. Its one caller is the
    submission PDF download, which is `staff_required`.
    """
    data_dict = {}
    for field_id in self.question_text_field_ids:
        if field_id not in self.named_blocks:
            question_field = self.serialize(field_id)
            if isinstance(question_field["answer"], str):
                answer = question_field["answer"]
            else:
                answer = ",".join(question_field["answer"])
            if answer and not answer == "None":
                data_dict[question_field["question"]] = answer
            elif question_field["type"] == "checkbox":
                data_dict[question_field["question"]] = False
            else:
                data_dict[question_field["question"]] = "-"
    return data_dict