Skip to content

Models

hypha.apply.review.models

ReviewFormFieldsMixin

Bases: Model

wagtail_reference_index_ignore class-attribute instance-attribute

wagtail_reference_index_ignore = True

form_fields class-attribute instance-attribute

form_fields = StreamField(ReviewCustomFormFieldsBlock())

score_fields property

score_fields

score_fields_without_text property

score_fields_without_text

recommendation_field property

recommendation_field

visibility_field property

visibility_field

comment_field property

comment_field

Meta

abstract class-attribute instance-attribute
abstract = True

ReviewForm

Bases: ReviewFormFieldsMixin, Model

name class-attribute instance-attribute

name = models.CharField(max_length=255)

panels class-attribute instance-attribute

panels = [FieldPanel('name'), FieldPanel('form_fields')]

wagtail_reference_index_ignore class-attribute instance-attribute

wagtail_reference_index_ignore = True

form_fields class-attribute instance-attribute

form_fields = StreamField(ReviewCustomFormFieldsBlock())

score_fields property

score_fields

score_fields_without_text property

score_fields_without_text

recommendation_field property

recommendation_field

visibility_field property

visibility_field

comment_field property

comment_field

Meta

verbose_name class-attribute instance-attribute
verbose_name = _('review form')
verbose_name_plural class-attribute instance-attribute
verbose_name_plural = _('review forms')

ReviewQuerySet

Bases: QuerySet

submitted

submitted()
Source code in hypha/apply/review/models.py
def submitted(self):
    return self.filter(is_draft=False)

by_staff

by_staff()
Source code in hypha/apply/review/models.py
def by_staff(self):
    return self.submitted()._by_group(STAFF_GROUP_NAME)

by_reviewers

by_reviewers()
Source code in hypha/apply/review/models.py
def by_reviewers(self):
    return self.submitted()._by_group(REVIEWER_GROUP_NAME)

by_user

by_user(user)
Source code in hypha/apply/review/models.py
def by_user(self, user):
    return self.submitted().filter(author__reviewer=user).order_by("-created_at")

staff_score

staff_score()
Source code in hypha/apply/review/models.py
def staff_score(self):
    return self.by_staff().score()

staff_recommendation

staff_recommendation()
Source code in hypha/apply/review/models.py
def staff_recommendation(self):
    return self.by_staff().recommendation()

reviewers_score

reviewers_score()
Source code in hypha/apply/review/models.py
def reviewers_score(self):
    return self.by_reviewers().score()

reviewers_recommendation

reviewers_recommendation()
Source code in hypha/apply/review/models.py
def reviewers_recommendation(self):
    return self.by_reviewers().recommendation()

score

score()
Source code in hypha/apply/review/models.py
def score(self):
    return self.exclude(score=NA).aggregate(models.Avg("score"))["score__avg"]

recommendation

recommendation()
Source code in hypha/apply/review/models.py
def recommendation(self):
    opinions = self.values_list("opinions__opinion", flat=True)

    if any(opinion == DISAGREE for opinion in opinions):
        return MAYBE

    recommendations = self.values_list("recommendation", flat=True)
    try:
        recommendation = sum(recommendations) / len(recommendations)
    except ZeroDivisionError:
        return -1

    if recommendation == YES or recommendation == NO:
        # If everyone in agreement return Yes/No
        return recommendation
    else:
        return MAYBE

opinions

opinions()
Source code in hypha/apply/review/models.py
def opinions(self):
    return ReviewOpinion.objects.filter(review__id__in=self.values_list("id"))

Review

Bases: ReviewFormFieldsMixin, BaseStreamForm, AccessFormData, Model

submission class-attribute instance-attribute

submission = models.ForeignKey('funds.ApplicationSubmission', on_delete=models.CASCADE, related_name='reviews')

revision class-attribute instance-attribute

revision = models.ForeignKey('funds.ApplicationRevision', on_delete=models.SET_NULL, related_name='reviews', null=True)

author class-attribute instance-attribute

author = models.OneToOneField('funds.AssignedReviewers', related_name='review', on_delete=models.CASCADE)

form_data class-attribute instance-attribute

form_data = models.JSONField(default=dict, encoder=DjangoJSONEncoder)

recommendation class-attribute instance-attribute

recommendation = models.IntegerField(verbose_name=_('recommendation'), choices=RECOMMENDATION_CHOICES, default=0)

score class-attribute instance-attribute

score = models.DecimalField(max_digits=10, decimal_places=1, default=0)

is_draft class-attribute instance-attribute

is_draft = models.BooleanField(default=False, verbose_name=_('draft'))

created_at class-attribute instance-attribute

created_at = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True)

updated_at class-attribute instance-attribute

updated_at = models.DateTimeField(verbose_name=_('update time'), auto_now=True)

visibility class-attribute instance-attribute

visibility = models.CharField(verbose_name=_('visibility'), choices=VISIBILITY.items(), default=PRIVATE, max_length=10)

objects class-attribute instance-attribute

objects = ReviewQuerySet.as_manager()

outcome property

outcome

get_score_display property

get_score_display

for_latest property

for_latest

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

submission_form_class class-attribute instance-attribute

submission_form_class = PageStreamBaseForm

wagtail_reference_index_ignore class-attribute instance-attribute

wagtail_reference_index_ignore = True

form_fields class-attribute instance-attribute

form_fields = StreamField(ReviewCustomFormFieldsBlock())

score_fields property

score_fields

score_fields_without_text property

score_fields_without_text

recommendation_field property

recommendation_field

visibility_field property

visibility_field

comment_field property

comment_field

Meta

unique_together class-attribute instance-attribute
unique_together = ('author', 'submission')
verbose_name class-attribute instance-attribute
verbose_name = _('review')
verbose_name_plural class-attribute instance-attribute
verbose_name_plural = _('reviews')

get_comments_display

get_comments_display(include_question=True)
Source code in hypha/apply/review/models.py
def get_comments_display(self, include_question=True):
    return self.render_answer(
        self.comment_field.id, include_question=include_question
    )

get_absolute_url

get_absolute_url()
Source code in hypha/apply/review/models.py
def get_absolute_url(self):
    return reverse(
        "apply:submissions:reviews:review",
        args=(
            self.submission_id,
            self.id,
        ),
    )

get_compare_url

get_compare_url()
Source code in hypha/apply/review/models.py
def get_compare_url(self):
    return self.revision.get_compare_url_to_latest()

reviewer_visibility

reviewer_visibility()
Source code in hypha/apply/review/models.py
@cached_property
def reviewer_visibility(self):
    return self.visibility == REVIEWER

is_updated

is_updated()
Source code in hypha/apply/review/models.py
@cached_property
def is_updated(self):
    # Only compare dates, not time.
    return self.created_at.date() < self.updated_at.date()

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)

Deserialize form data when loading from database.

Parameters:

  • db –

    Database connection

  • field_names –

    List of field names being loaded

  • values –

    Values for the fields

Returns:

  • –

    Instance with deserialized form data

Source code in hypha/apply/stream_forms/models.py
@classmethod
def from_db(cls, db, field_names, values):
    """Deserialize form data when loading from database.

    Args:
        db: Database connection
        field_names: List of field names being loaded
        values: Values for the fields

    Returns:
        Instance with deserialized form data
    """
    instance = super().from_db(db, field_names, values)
    if "form_data" in field_names:
        instance.form_data = cls.deserialize_form_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

deserialize_form_data classmethod

deserialize_form_data(instance, form_data, form_fields)

Convert stored form data back into Python objects.

Parameters:

  • instance –

    Form instance

  • form_data –

    Raw form data from database

  • form_fields –

    Form field definitions

Returns:

  • –

    Deserialized form data

Source code in hypha/apply/stream_forms/models.py
@classmethod
def deserialize_form_data(cls, instance, form_data, form_fields):
    """Convert stored form data back into Python objects.

    Args:
        instance: Form instance
        form_data: Raw form data from database
        form_fields: Form field definitions

    Returns:
        Deserialized form data
    """
    data = form_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"]]
        field_id = field_data.get("id")
        try:
            value = data[field_id]
        except KeyError:
            pass
        else:
            data[field_id] = block.decode(value)
    return data

get_defined_fields

get_defined_fields()

Get the form field definitions.

Returns:

  • –

    StreamField containing form field blocks

Raises:

  • AttributeError –

    If form_fields attribute is not defined on instance

Source code in hypha/apply/stream_forms/models.py
def get_defined_fields(self):
    """Get the form field definitions.

    Returns:
        StreamField containing form field blocks

    Raises:
        AttributeError: If form_fields attribute is not defined on instance
    """
    try:
        return self.form_fields  # type: ignore
    except AttributeError as err:
        raise AttributeError(
            "form_fields attribute not found. "
            "Make sure form_fields is defined on the implementing class."
        ) from err

get_form_fields

get_form_fields(draft=False, form_data=None, user=None)

Generate form fields with applied logic and grouping.

Parameters:

  • draft –

    Whether this is a draft form. When True, fields that are not marked as ApplicationMustIncludeFieldBlock will have their required flag set to False, allowing incomplete form submissions to be saved as drafts.

  • form_data –

    Existing form data

  • user –

    User completing the form

Returns:

  • –

    OrderedDict of form fields

Source code in hypha/apply/stream_forms/models.py
def get_form_fields(self, draft=False, form_data=None, user=None):
    """Generate form fields with applied logic and grouping.

    Args:
        draft: Whether this is a draft form. When True, fields that are not
              marked as ApplicationMustIncludeFieldBlock will have their
              required flag set to False, allowing incomplete form submissions
              to be saved as drafts.
        form_data: Existing form data
        user: User completing the form

    Returns:
        OrderedDict of form fields
    """
    if form_data is None:
        form_data = {}

    form_fields = OrderedDict()
    field_blocks = self.get_defined_fields()
    group_counter = 1
    is_in_group = False

    # If true option 1 is selected
    grouped_fields_visible = False
    for struct_child in field_blocks:
        block = struct_child.block
        struct_value = struct_child.value
        if isinstance(block, FormFieldBlock):
            field_from_block = block.get_field(struct_value)
            disabled_help_text = _(
                "You are logged in so this information is fetched from your user account."
            )
            if isinstance(block, FullNameBlock) and user and user.is_authenticated:
                if user.full_name:
                    field_from_block.disabled = True
                    field_from_block.initial = user.full_name
                    field_from_block.help_text = disabled_help_text
                else:
                    field_from_block.help_text = _(
                        "You are logged in but your user account does not have a "
                        "full name. We'll update your user account with the name you provide here."
                    )
            if isinstance(block, EmailBlock) and user and user.is_authenticated:
                field_from_block.disabled = True
                field_from_block.initial = user.email
                field_from_block.help_text = disabled_help_text
            if draft and not issubclass(
                block.__class__, ApplicationMustIncludeFieldBlock
            ):
                field_from_block.required = False
            field_from_block.group_number = group_counter if is_in_group else 1
            if isinstance(block, GroupToggleBlock) and not is_in_group:
                field_from_block.group_number = 1
                field_from_block.grouper_for = group_counter + 1
                group_counter += 1
                is_in_group = True
                grouped_fields_visible = (
                    form_data.get(struct_child.id) == field_from_block.choices[0][0]
                )
            if isinstance(block, TextFieldBlock):
                field_from_block.word_limit = struct_value.get("word_limit")
            if isinstance(block, MultiInputCharFieldBlock):
                number_of_inputs = struct_value.get("number_of_inputs")
                for index in range(number_of_inputs):
                    form_fields[struct_child.id + "_" + str(index)] = (
                        field_from_block
                    )
                    field_from_block.multi_input_id = struct_child.id
                    field_from_block.add_button_text = struct_value.get(
                        "add_button_text"
                    )
                    if (
                        index == number_of_inputs - 1
                    ):  # Add button after last input field
                        field_from_block.multi_input_add_button = True
                        # Index for field until which fields will be visible to applicant.
                        # Initially only the first field with id UUID_0 will be visible.
                        field_from_block.visibility_index = 0
                        field_from_block.max_index = index
                    if index != 0:
                        field_from_block.multi_input_field = True
                        field_from_block.required = False
                        field_from_block.initial = None
                    field_from_block = copy.copy(field_from_block)
            else:
                if is_in_group and not isinstance(block, GroupToggleBlock):
                    field_from_block.required_when_visible = (
                        field_from_block.required
                    )
                    field_from_block.required = (
                        field_from_block.required & grouped_fields_visible
                    )
                    field_from_block.visible = grouped_fields_visible
                form_fields[struct_child.id] = field_from_block
        elif isinstance(block, GroupToggleEndBlock):
            # Group toggle end block is used only to group fields and not used in actual form.
            # Todo: Use streamblock to create nested form field blocks, a more elegant method to group form fields.
            is_in_group = False
        else:
            field_wrapper = BlockFieldWrapper(struct_child)
            field_wrapper.group_number = group_counter if is_in_group else 1
            form_fields[struct_child.id] = field_wrapper

    return form_fields

get_form_class

get_form_class(draft=False, form_data=None, user=None)

Dynamically creates and returns a form class based on the field configuration.

Creates a new form class that inherits from submission_form_class (PageStreamBaseForm) and includes all the dynamically generated form fields.

Parameters:

  • draft –

    Whether this is a draft form

  • form_data –

    Existing form data for pre-populating form fields

  • user –

    User completing the form, used for auto-populating user fields.

Returns:

  • –

    A dynamically generated form class

Source code in hypha/apply/stream_forms/models.py
def get_form_class(self, draft=False, form_data=None, user=None):
    """Dynamically creates and returns a form class based on the field configuration.

    Creates a new form class that inherits from submission_form_class (PageStreamBaseForm)
    and includes all the dynamically generated form fields.

    Args:
        draft: Whether this is a draft form
        form_data: Existing form data for pre-populating form fields
        user: User completing the form, used for auto-populating user fields.

    Returns:
        A dynamically generated form class
    """
    return type(
        "WagtailStreamForm",
        (self.submission_form_class,),
        self.get_form_fields(draft=draft, form_data=form_data, user=user),
    )

ReviewOpinion

Bases: Model

review class-attribute instance-attribute

review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name='opinions')

author class-attribute instance-attribute

author = models.ForeignKey('funds.AssignedReviewers', related_name='opinions', on_delete=models.CASCADE)

opinion class-attribute instance-attribute

opinion = models.IntegerField(choices=OPINION_CHOICES)

opinion_display property

opinion_display

Meta

unique_together class-attribute instance-attribute
unique_together = ('author', 'review')
verbose_name class-attribute instance-attribute
verbose_name = _('review opinion')
verbose_name_plural class-attribute instance-attribute
verbose_name_plural = _('review opinions')