Choosing and Counting Objects

A Draft holds sets of objects: its Forms are draft.als_forms_set, its Fields are draft.als_fields_set, and so on through the Object Model Reference. You do not have to read a whole set and pick through it yourself. You can query it: filter it to just the objects you want, order_by the attribute you want, or count them. A Draft can hold over ten thousand Fields, so asking for less is much faster than reading them all.

The same methods work in a Document Template and in an Import Script, and they are written the same way in both. In a template:

{% for field in draft.als_fields_set.filter(ControlType="CheckBox") %}
{{ field.FieldOID }}
{% endfor %}

and in a script:

draft = importer.context.draft
checkboxes = draft.als_fields_set.filter(ControlType="CheckBox")
for field in checkboxes:
    importer.log(field.FieldOID)

The examples on the rest of this page are written for a script. In a template, put the same call inside {{ }} or {% %}.

The methods you can use

These are all the methods a set of objects offers. Anything else is refused, and the message lists these names.

Method

What it gives you

all()

Every object in the set

filter(...)

The objects that match all the conditions you give

exclude(...)

The objects that do not match

get(...)

The one object that matches. It stops with an error if none match or more than one does

first()

The first object, or nothing if the set is empty

last()

The last object, or nothing if the set is empty

count()

How many objects there are, without reading them

exists()

Whether there are any objects at all

order_by(...)

The same objects in the order you name. Put - in front of a name to reverse it

reverse()

The same objects in the opposite order

distinct()

The set with duplicates removed. See Removing duplicates below

values(...)

For each object, a dictionary of the attributes you name

values_list(...)

For each object, the attributes you name. With flat=True and one name, just the values

select_related(...)

The same objects, with the related object you name read at the same time

prefetch_related(...)

The same objects, with the related set you name read at the same time

only(...)

The same objects, reading only the attributes you name until you ask for others

defer(...)

The same objects, leaving out the attributes you name until you ask for them

none()

An empty set

union(...)

The objects in this set or in another set of the same kind

intersection(...)

The objects in both this set and another set of the same kind

difference(...)

The objects in this set but not in another set of the same kind

Most of these give you back another set, so you can call one after another: draft.als_fields_set.filter(ControlType="CheckBox").order_by("FieldOID").count().

Choosing objects

filter keeps the objects that match. exclude keeps the ones that do not. count and exists answer without reading the objects at all:

fields = importer.context.draft.als_fields_set.filter(DraftFieldActive=True)
not_text = fields.exclude(ControlType="Text")
importer.log(f"{fields.count()} active fields, {not_text.count()} of them not Text")
if not fields.filter(IsLog=True).exists():
    importer.log("No log fields")

Give several conditions and an object has to match all of them.

Inactive Forms and Fields

A Form or Field that is not Active is switched off in Medidata Rave. A study behaves as though it does not exist, and nothing in the study can switch it back on. Changing a Field's visibility shows or hides it, but does not make an inactive Field active.

A Draft's sets still hold its inactive Forms and Fields. Most reports leave them out, as the example above does, with filter(DraftFormActive=True) on a set of Forms or filter(DraftFieldActive=True) on a set of Fields.

Conditions other than "equals"

Add two underscores and a word to the attribute name to compare a different way. To go through a related object, name it with two underscores too: Form__OID is the OID of the Field's Form. These work with any attribute the Object Model Reference lists for that kind of object, not only the ones in the examples: OID__startswith on a set of Forms works as FieldOID__startswith does on a set of Fields.

fields = importer.context.draft.als_fields_set
on_dm = fields.filter(Form__OID="DM")
dates = fields.filter(FieldOID__startswith="DT", ControlType="DateTime")
choices = fields.filter(ControlType__in=["RadioButton", "DropDownList"])
importer.log(f"{on_dm.count()} on DM, {dates.count()} date fields, {choices.count()} choice fields")

The ones you are most likely to want:

Written as

Matches when the value

FieldOID="AGE"

is exactly AGE

FieldOID__iexact="age"

is age, ignoring capitals

FieldOID__contains="DT"

contains DT (icontains ignores capitals)

FieldOID__startswith="DT"

starts with DT (endswith for the end)

ControlType__in=["Text", "LongText"]

is one of the values in the list

Ordinal__gt=5

is greater than 5 (gte, lt and lte for the others)

DataDictionary__isnull=True

is not set at all, here a Field with no Data Dictionary

last_updated__year=2026

is a date in 2026

You can only go through related objects that the Object Model Reference lists. A name it does not list, or one that leads to an object it does not list, is refused, and the message names the part it refused.

Putting objects in order

order_by takes the attributes to sort by, first one first. Put - in front for largest or latest first:

for form in importer.context.draft.als_forms_set.order_by("-Ordinal"):
    importer.log(f"{form.OID} has {form.als_fields_set.count()} fields")

Here is a full example: the CheckBox Fields of a Draft, grouped under their Form. Asking for select_related("Form") reads each Field's Form along with it, rather than once per Field:

draft = importer.context.draft
checkboxes = (
    draft.als_fields_set.filter(ControlType="CheckBox").select_related("Form").order_by("Form__Ordinal", "Ordinal")
)

current_form = None
for field in checkboxes:
    if field.Form.OID != current_form:
        current_form = field.Form.OID
        importer.log(f"Form {current_form}")
    importer.log(f"  {field.FieldOID}")

Getting one object

first gives you nothing, rather than an error, when the set is empty, so check for it. get is for an object you know is there exactly once:

forms = importer.context.draft.als_forms_set
first_form = forms.order_by("Ordinal").first()
if first_form is not None:
    importer.log(f"The first form is {first_form.OID}")

demographics = forms.get(OID="DM")
importer.log(demographics.DraftFormName)

Getting just the values

values_list gives you the values of the attributes you name, without the objects around them. With one name and flat=True you get a plain list of values:

fields = importer.context.draft.als_fields_set
field_oids = list(fields.values_list("FieldOID", flat=True))
control_types = fields.order_by("ControlType").values_list("ControlType", flat=True).distinct()
importer.log(f"{len(field_oids)} fields using {', '.join(control_types)}")

Removing duplicates

Each kind of object has an order it is listed in by default, and distinct only treats two objects as the same when they also agree on the attributes used for that order. So fields.values_list("ControlType", flat=True).distinct() can still list CheckBox once for every CheckBox Field. Put the values in an order of your own first, as the example above does with order_by("ControlType"), and each one is listed once.

What is not offered

A set of objects in TrialGrid is only for reading.

  • There is no update, delete or create. A Document Template only reads, and an Import Script asks for changes with importer.execute so that you can see them before they are made.

  • There is no annotate or aggregate. To count the Fields on each Form, ask each Form: form.als_fields_set.count().

  • latest, earliest, iterator, in_bulk, contains and get_or_create are not offered either. Use order_by with first, or a loop, instead.

Asking for a method that is not offered stops with a message listing the ones that are.

Sorting by something you work out

order_by sorts by attributes the objects already have. To sort by something you work out, like how many Fields a Form has, read the objects and sort them yourself. In an Import Script you can write your own functions, and use lambda and list comprehensions:

def describe(form):
    return f"{form.OID} ({form.als_fields_set.count()} fields)"


forms = list(importer.context.draft.als_forms_set.all())
largest_first = sorted(forms, key=lambda form: form.als_fields_set.count(), reverse=True)
importer.log(", ".join([describe(form) for form in largest_first]))

In a Document Template, collect what you worked out and sort it with Jinja's sort filter. Collect into a namespace: a list started with {% set %} outside a loop is not changed by a {% set %} inside it.

{% set found = namespace(forms=[]) %}
{% for form in draft.als_forms_set.all() %}
{% set found.forms = found.forms + [(form.als_fields_set.count(), form.OID)] %}
{% endfor %}
{% for count, oid in found.forms|sort(reverse=True) %}
{{ oid }}: {{ count }} fields
{% endfor %}

Further reading

These methods come from Django, the framework TrialGrid is built on, and the Django QuerySet reference explains the condition syntax in more depth. That reference describes many methods that are not offered here. The list on this page is the one that applies.