Question
pretalx.submission.models.question.Question source
Questions can be asked per Submission, per pretalx.person.models.SpeakerProfile, or of reviewers per Review.
Questions can have many types, which offers a flexible framework to give organisers the opportunity to get all the information they need.
Fields 29
idAutoFieldpkeventForeignKeysource
event = models.ForeignKey(
to="event.Event", on_delete=models.PROTECT, related_name="questions"
)
variantCharFieldsource
variant = models.CharField(
max_length=QuestionVariant.get_max_length(),
choices=QuestionVariant.choices,
default=QuestionVariant.STRING,
verbose_name=_("Field type"),
)
targetCharFieldsource
target = models.CharField(
max_length=QuestionTarget.get_max_length(),
choices=QuestionTarget.choices,
default=QuestionTarget.SUBMISSION,
verbose_name="Target", # Only used in API
)
deadlineDateTimeFieldnullsourceSet a deadline to make this field required after the given date.
deadline = DateTimeField(
null=True,
blank=True,
verbose_name=_("Deadline"),
help_text=_("Set a deadline to make this field required after the given date."),
)
freeze_afterDateTimeFieldnullsourceSet a deadline to stop changes to responses after the given date.
freeze_after = DateTimeField(
null=True,
blank=True,
verbose_name=_("freeze after"),
help_text=_(
"Set a deadline to stop changes to responses after the given date."
),
)
question_requiredCharFieldsource
question_required = models.CharField(
max_length=QuestionRequired.get_max_length(),
choices=QuestionRequired.choices,
default=QuestionRequired.OPTIONAL,
verbose_name=_("Field required"),
)
questionI18nCharFieldsource
question = I18nCharField(
max_length=800,
verbose_name=pgettext_lazy("display label for custom field", "Label"),
)
help_textI18nCharFieldnullsourceWill appear just like this text below the custom input field. You can use <a href="https://docs.pretalx.org/user/markdown/" target="_blank" rel="noopener">Markdown</a> here.
help_text = I18nCharField(
null=True,
blank=True,
max_length=800,
verbose_name=_("Help text"),
help_text=format_lazy(
"{} {}",
_("Will appear just like this text below the custom input field."),
phrases.base.use_markdown,
),
)
default_answerTextFieldnullsource
default_answer = models.TextField(
null=True, blank=True, verbose_name=_("default answer")
)
positionIntegerFieldsource
position = models.IntegerField(default=0)
identifierCharFieldsourceYou can enter any value here to make it easier to match the data with other sources. If you do not input one, we will generate one automatically.
identifier = models.CharField(
max_length=190,
verbose_name=_("Internal identifier"),
help_text=_(
"You can enter any value here to make it easier to match the data "
"with other sources. If you do not input one, we will generate one "
"automatically."
),
validators=[
RegexValidator(
regex=r"^[a-zA-Z0-9.\-_]+$",
message=_(
"The identifier may only contain letters, numbers, dots, "
"dashes, and underscores."
),
)
],
)
activeBooleanFieldsourceInactive fields will no longer be shown.
active = models.BooleanField(
default=True,
verbose_name=_("active"),
help_text=_("Inactive fields will no longer be shown."),
)
contains_personal_dataBooleanFieldsourceIf a user deletes their account, responses containing personal data will be removed, too.
contains_personal_data = models.BooleanField(
default=True,
verbose_name=_("Responses contain personal data"),
help_text=_(
"If a user deletes their account, responses containing personal data will be removed, too."
),
)
min_lengthPositiveIntegerFieldnullsourceMinimum text length in characters or words (set in CfP settings).
min_length = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_("Minimum length"),
help_text=_(
"Minimum text length in characters or words (set in CfP settings)."
),
)
max_lengthPositiveIntegerFieldnullsourceMaximum text length in characters or words (set in CfP settings).
max_length = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_("Maximum length"),
help_text=_(
"Maximum text length in characters or words (set in CfP settings)."
),
)
min_numberDecimalFieldnullsource
min_number = models.DecimalField(
decimal_places=6,
max_digits=16,
null=True,
blank=True,
verbose_name=_("Minimum value"),
)
max_numberDecimalFieldnullsource
max_number = models.DecimalField(
decimal_places=6,
max_digits=16,
null=True,
blank=True,
verbose_name=_("Maximum value"),
)
min_optionsPositiveIntegerFieldnullsourceMinimum number of options that have to be selected.
min_options = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_("Minimum number of options"),
help_text=_("Minimum number of options that have to be selected."),
validators=[MinValueValidator(1)],
)
max_optionsPositiveIntegerFieldnullsourceMaximum number of options that can be selected.
max_options = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_("Maximum number of options"),
help_text=_("Maximum number of options that can be selected."),
validators=[MinValueValidator(1)],
)
min_dateDateFieldnullsource
min_date = DateField(null=True, blank=True, verbose_name=_("Minimum value"))
max_dateDateFieldnullsource
max_date = DateField(null=True, blank=True, verbose_name=_("Maximum value"))
min_datetimeDateTimeFieldnullsource
min_datetime = DateTimeField(null=True, blank=True, verbose_name=_("Minimum value"))
max_datetimeDateTimeFieldnullsource
max_datetime = DateTimeField(null=True, blank=True, verbose_name=_("Maximum value"))
is_publicBooleanFieldsourceResponses will be shown on session or speaker pages as appropriate.
is_public = models.BooleanField(
default=False,
verbose_name=_("Publish answers"),
help_text=_(
"Responses will be shown on session or speaker pages as appropriate."
),
)
is_visible_to_reviewersBooleanFieldsourceShould responses to this field be shown to reviewers? This is helpful if you want to collect personal information, but use anonymous reviews.
is_visible_to_reviewers = models.BooleanField(
default=True,
verbose_name=_("Show answers to reviewers"),
help_text=_(
"Should responses to this field be shown to reviewers? This is helpful if you want to collect personal information, but use anonymous reviews."
),
)
iconCharFieldnullsourceCustom URL fields that are shown publicly can use an icon when displaying the link.
icon = models.CharField(
max_length=QuestionIcon.get_max_length(),
choices=QuestionIcon.choices,
default=None,
null=True,
blank=False,
verbose_name=_("Icon"),
help_text=_(
"Custom URL fields that are shown publicly can use an icon when displaying the link."
),
)
createdDateTimeFieldnullTimestampedModelsource
on pretalx.common.models.mixins.TimestampedModel source
created = models.DateTimeField(
verbose_name=_("Created"), auto_now_add=True, blank=True, null=True
)
updatedDateTimeFieldnullTimestampedModelsource
on pretalx.common.models.mixins.TimestampedModel source
updated = models.DateTimeField(
verbose_name=_("Updated"), auto_now=True, blank=True, null=True
)
Reverse relations 5
Attributes 6
all_objects<django_scopes.manager.ScopedManager.<locals>.Manager>code_length8code_property'identifier'code_scope('event',)log_prefix'pretalx.question'objects<django_scopes.manager.ScopedManager.<locals>.Manager>Inherited attributes 3
code_charset['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '3', '7', '8', '9']GenerateCodelog_parentNoneLogMixinorder_field'position'OrderedModelProperties 6
Defined here
icon_urlsource
@cached_property
def icon_url(self):
if self.show_icon:
return reverse(
"api:question-icon", kwargs={"event": self.event.slug, "pk": self.pk}
)
log_parentsource
@property
def log_parent(self):
return self.event
read_onlysource
@property
def read_only(self):
return self.freeze_after and (self.freeze_after <= now())
requiredsource
@cached_property
def required(self):
_now = now()
# Question should become optional in order to be frozen
if self.read_only:
return False
if self.question_required == QuestionRequired.REQUIRED:
return True
if self.question_required == QuestionRequired.AFTER_DEADLINE:
return self.deadline <= _now
return False
show_iconsource
@property
def show_icon(self):
return self.variant == QuestionVariant.URL and self.icon not in ("", "-", None)
From OrderedModel pretalx.common.models.mixins
order_querysetsource
@property
def order_queryset(self):
return self.get_order_queryset(event=self.event)
Methods 38
Defined here
clean()sourceHook for doing any extra model-wide validation after clean() has been called on every field by self.clean_fields.
on pretalx.submission.models.question.Question source
Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field defined by NON_FIELD_ERRORS.
def clean(self):
super().clean()
validate_question_deadline(self)
validate_question_option_limits(self)
if not (self.event_id and self.identifier):
return
validate_question_identifier_unique(
event=self.event, identifier=self.identifier, instance=self
)
Hook for doing any extra model-wide validation after clean() has been called on every field by self.clean_fields. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field defined by NON_FIELD_ERRORS.
def clean(self):
"""
Hook for doing any extra model-wide validation after clean() has been
called on every field by self.clean_fields. Any ValidationError raised
by this method will not be associated with a particular field; it will
have a special-case association with the field defined by
NON_FIELD_ERRORS.
"""
pass
get_instance_data()sourceGet a dictionary of field values for this instance.
on pretalx.submission.models.question.Question source
Used for change tracking in log_action. Excludes auto-updated fields like timestamps and sensitive data. Does not handle many-to-many fields.
def get_instance_data(self):
data = super().get_instance_data()
if not self._state.adding and self.variant in (
QuestionVariant.CHOICES,
QuestionVariant.MULTIPLE,
):
options = list(self.options.values_list("answer", flat=True))
if options:
with override(self.event.locale):
data["options"] = "\n".join(f"- {option}" for option in options)
return data
on pretalx.common.models.mixins.LogMixin source
Get a dictionary of field values for this instance.
Used for change tracking in log_action. Excludes auto-updated fields like timestamps and sensitive data. Does not handle many-to-many fields.
def get_instance_data(self):
"""Get a dictionary of field values for this instance.
Used for change tracking in log_action. Excludes auto-updated
fields like timestamps and sensitive data.
Does not handle many-to-many fields.
"""
excluded_fields = {
"created",
"updated",
"is_active",
"last_login",
"user",
"event",
"code",
}
data = {}
for field in self._meta.fields:
if (
field.name in excluded_fields
or field.name in SENSITIVE_KEYS
or "thumbnail" in field.name
or getattr(field, "auto_now", False)
or getattr(field, "auto_now_add", False)
):
continue
if isinstance(field, models.ForeignKey):
data[field.name] = getattr(self, field.attname, None)
continue
value = getattr(self, field.name, None)
if isinstance(field, models.FileField):
data[field.name] = value.name if value else None
elif isinstance(field, models.UUIDField):
data[field.name] = str(value) if value else None
elif isinstance(value, LazyI18nString):
if isinstance(getattr(value, "data", None), dict):
data[field.name] = {k: v for k, v in value.data.items() if v}
else:
data[field.name] = str(value)
else:
data[field.name] = json_roundtrip(value)
return data
get_order_queryset(event)source
on pretalx.submission.models.question.Question source
@staticmethod
def get_order_queryset(event):
return event.questions(manager="all_objects").all()
on pretalx.common.models.mixins.OrderedModel source
@staticmethod
def get_order_queryset(**kwargs):
raise NotImplementedError
__str__()sourceReturn str(self).
on pretalx.submission.models.question.Question source
def __str__(self):
return str(self.question)
Return str(self).
def __str__(self):
return "%s object (%s)" % (self.__class__.__name__, self.pk)
From GenerateCode pretalx.common.models.mixins
assign_code(length=None)source
def assign_code(self, length=None):
length = length or self.code_length
while True:
code = self.generate_code(length=length)
filter_kwargs = {f"{self.code_property}__iexact": code}
for field in self.code_scope:
filter_kwargs[field] = getattr(self, field)
with scopes_disabled():
if not self.__class__.objects.filter(**filter_kwargs).exists():
setattr(self, self.code_property, code)
return
save(*args, **kwargs)source
on pretalx.common.models.mixins.GenerateCode source
def save(self, *args, **kwargs):
if getattr(self, self.code_property, None):
return super().save(*args, **kwargs)
# Auto-generate code with retry loop to handle unlikely race conditions
if "update_fields" in kwargs:
kwargs["update_fields"] = {self.code_property}.union(
kwargs["update_fields"]
)
for attempt in range(
3
): # pragma: no branch -- loop always exits via return or raise
self.assign_code()
try:
with transaction.atomic():
return super().save(*args, **kwargs)
except IntegrityError:
if attempt == 2:
raise
setattr(self, self.code_property, None)
on pretalx.common.models.mixins.FileCleanupMixin source
def save(self, *args, **kwargs):
update_fields = kwargs.get("update_fields")
if self._state.adding or (
update_fields and not set(self._file_fields) & set(update_fields)
):
return super().save(*args, **kwargs)
try:
pre_save_instance = self.__class__.objects.get(pk=self.pk)
except ObjectDoesNotExist:
return super().save(*args, **kwargs)
old_files = {}
for field in self._file_fields:
if old_value := getattr(pre_save_instance, field):
new_value = getattr(self, field)
if new_value and old_value.path != new_value.path:
old_files[field] = old_value.path
result = super().save(*args, **kwargs)
for field, path in old_files.items():
self._schedule_file_cleanup(field=field, path=path)
return result
Save the current instance. Override this in a subclass if you want to control the saving process.
The 'force_insert' and 'force_update' parameters can be used to insist that the "save" must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.
def save(
self,
*,
force_insert=False,
force_update=False,
using=None,
update_fields=None,
):
"""
Save the current instance. Override this in a subclass if you want to
control the saving process.
The 'force_insert' and 'force_update' parameters can be used to insist
that the "save" must be an SQL insert or update (or equivalent for
non-SQL backends), respectively. Normally, they should not be set.
"""
self._prepare_related_fields_for_save(operation_name="save")
using = using or router.db_for_write(self.__class__, instance=self)
if force_insert and (force_update or update_fields):
raise ValueError("Cannot force both insert and updating in model saving.")
deferred_non_generated_fields = {
f.attname
for f in self._meta.concrete_fields
if f.attname not in self.__dict__ and f.generated is False
}
if update_fields is not None:
# If update_fields is empty, skip the save. We do also check for
# no-op saves later on for inheritance cases. This bailout is
# still needed for skipping signal sending.
if not update_fields:
return
update_fields = frozenset(update_fields)
field_names = self._meta._non_pk_concrete_field_names
not_updatable_fields = update_fields.difference(field_names)
if not_updatable_fields:
raise ValueError(
"The following fields do not exist in this model, are m2m "
"fields, primary keys, or are non-concrete fields: %s"
% ", ".join(not_updatable_fields)
)
# If saving to the same database, and this model is deferred, then
# automatically do an "update_fields" save on the loaded fields.
elif (
not force_insert
and deferred_non_generated_fields
and using == self._state.db
and self._is_pk_set()
):
field_names = set()
pk_fields = self._meta.pk_fields
for field in self._meta.concrete_fields:
if field not in pk_fields and not hasattr(field, "through"):
field_names.add(field.attname)
loaded_fields = field_names.difference(deferred_non_generated_fields)
if loaded_fields:
update_fields = frozenset(loaded_fields)
self.save_base(
using=using,
force_insert=force_insert,
force_update=force_update,
update_fields=update_fields,
)
From OrderedModel pretalx.common.models.mixins
move(up=True)source
def move(self, up=True):
queryset = list(self.order_queryset.order_by(self.order_field))
index = queryset.index(self)
if index != 0 and up:
queryset[index - 1], queryset[index] = queryset[index], queryset[index - 1]
elif index != len(queryset) - 1 and not up:
queryset[index + 1], queryset[index] = queryset[index], queryset[index + 1]
for index, element in enumerate(queryset):
if element.position != index:
element.position = index
element.save()
From LogMixin pretalx.common.models.mixins
delete(*args, log_kwargs=None, skip_log=False, **kwargs)source
on pretalx.common.models.mixins.LogMixin source
def delete(self, *args, log_kwargs=None, skip_log=False, **kwargs):
parent = self.log_parent
result = super().delete(*args, **kwargs)
if (
not skip_log
and parent
and getattr(parent, "log_action", None)
and self.log_prefix
):
log_kwargs = log_kwargs or {}
parent.log_action(f"{self.log_prefix}.delete", **log_kwargs)
return result
on pretalx.common.models.mixins.FileCleanupMixin source
def delete(self, *args, **kwargs):
self.delete_files()
return super().delete(*args, **kwargs)
def delete(self, using=None, keep_parents=False):
if not self._is_pk_set():
raise ValueError(
"%s object can't be deleted because its %s attribute is set "
"to None." % (self._meta.object_name, self._meta.pk.attname)
)
using = using or router.db_for_write(self.__class__, instance=self)
collector = Collector(using=using, origin=self)
collector.collect([self], keep_parents=keep_parents)
return collector.delete()
log_action(action, data=None, person=None, orga=False, content_object=None, old_data=None, new_data=None)source
def log_action(
self,
action,
data=None,
person=None,
orga=False,
content_object=None,
old_data=None,
new_data=None,
):
if self._state.adding or not isinstance(self.pk, int):
return
if action.startswith("."):
if self.log_prefix:
action = f"{self.log_prefix}{action}"
else:
return
if old_data is not None or new_data is not None:
from pretalx.common.log import ( # noqa: PLC0415 -- thin method
compute_log_changes,
)
changes = compute_log_changes(old_data, new_data)
if not changes and not data:
return
if changes:
if data is None:
data = {}
data["changes"] = changes
if data:
if not isinstance(data, dict):
raise TypeError(
f"Logged data should always be a dictionary, not {type(data)}."
)
for key in data:
if any(sensitive_key in key for sensitive_key in SENSITIVE_KEYS):
data[key] = "********" if data[key] else data[key]
data = json_roundtrip(data)
return ActivityLog.objects.create(
person=person,
content_object=content_object or self,
action_type=action,
data=data,
is_orga_action=orga,
**self._log_event_kwargs(),
)
logged_actions()source
def logged_actions(self):
return (
ActivityLog.objects.filter(
content_type=ContentType.objects.get_for_model(type(self)),
object_id=self.pk,
)
.select_related("event", "person")
.prefetch_related("content_object")
)
From FileCleanupMixin pretalx.common.models.mixins
delete_files()sourceSchedule cleanup of every uploaded file attached to this object.
Public hook: called by delete() and by anonymisation paths (e.g. person.domain.user.deactivate_user) that want to drop files without removing the row. Overridable by subclasses that need to recurse into related objects.
def delete_files(self):
"""Schedule cleanup of every uploaded file attached to this object.
Public hook: called by ``delete()`` and by anonymisation paths
(e.g. ``person.domain.user.deactivate_user``) that want to drop
files without removing the row. Overridable by subclasses that
need to recurse into related objects."""
for field in self._file_fields:
value = getattr(self, field, None)
if not value:
continue
with suppress(Exception):
self._schedule_file_cleanup(field=field, path=value.path)
process_image(field, generate_thumbnail=False)source
def process_image(self, field, generate_thumbnail=False):
task_process_image.apply_async(
kwargs={
"field": field,
"model": self._meta.model_name.capitalize(),
"pk": self.pk,
"generate_thumbnail": generate_thumbnail,
},
countdown=10,
)