"""
TIM plugin: a checkbox field
"""
from dataclasses import dataclass, asdict
from typing import Union
from flask import render_template_string
from marshmallow.utils import missing
from timApp.document.docentry import DocEntry
from timApp.document.viewcontext import default_view_ctx
from timApp.modules.fields.textfield import TextfieldMarkupModel
from timApp.plugin.taskid import TaskId
from timApp.tim_app import csrf
from timApp.user.user import User
from timApp.util.get_fields import (
get_fields_and_users,
RequestedGroups,
GetFieldsAccess,
FieldValue,
UserFieldObj,
)
from tim_common.common_schemas import TextfieldStateModel
from tim_common.pluginserver_flask import (
create_blueprint,
GenericAnswerModel,
PluginAnswerWeb,
PluginAnswerResp,
PluginReqs,
GenericHtmlModelWithContext,
)
from tim_common.utils import Missing
[docs]@dataclass
class CbcountfieldMarkupModel(TextfieldMarkupModel):
groups: list[str] | Missing = missing
limit: int | Missing = missing
[docs]@dataclass
class TextfieldInputModel:
"""Model for the information that is sent from browser (plugin AngularJS component)."""
c: str
nosave: bool | Missing = missing
[docs]@dataclass
class CbcountfieldContext:
checked_count: int
[docs]@dataclass
class CbcountfieldHtmlModel(
GenericHtmlModelWithContext[
TextfieldInputModel,
CbcountfieldMarkupModel,
TextfieldStateModel,
CbcountfieldContext,
]
):
[docs] def get_component_html_name(self) -> str:
return "cbcountfield-runner"
[docs] def get_static_html(self) -> str:
return render_static_cdfield(self)
[docs] def get_browser_json(self) -> dict:
r = super().get_browser_json()
if self.ctx:
count = self.ctx.checked_count
else:
count, _ = get_checked_count(self.markup, self.taskID, self.current_user_id)
r["count"] = count
return r
[docs]@dataclass
class CbcountfieldAnswerModel(
GenericAnswerModel[
TextfieldInputModel, CbcountfieldMarkupModel, TextfieldStateModel
]
):
pass
[docs]def preprocess_multihtml(plugin_list: list[CbcountfieldHtmlModel]) -> None:
# Right now, count fetching is batched for the following common case:
# * Fields don't have group limitation (i.e. uses "*" group)
# * The values are requested all for the same user
# * Fields are in the same document
# TODO: Evaluate if other cases need similar speedup
can_batch = [
m for m in plugin_list if m.user_id != "Anonymous" and not m.markup.groups
]
if not can_batch:
return
users = {m.user_id for m in can_batch}
if len(users) > 1:
return
docs = {TaskId.parse_doc_id(m.taskID) for m in can_batch}
if len(docs) > 1:
return
doc_id = next(iter(docs))
user_id = next(iter(users))
curr_user = User.get_by_name(user_id)
if curr_user is None:
return
d = DocEntry.find_by_id(doc_id)
if not d:
return
user_fields, _, _, _ = get_fields_and_users(
[m.taskID for m in can_batch],
RequestedGroups(groups=[], include_all_answered=True),
d,
curr_user,
default_view_ctx,
access_option=GetFieldsAccess.AllowAlwaysNonTeacher,
)
for p in can_batch:
count, _ = count_field(user_fields, p.taskID, curr_user)
p.ctx = CbcountfieldContext(checked_count=count)
[docs]def render_static_cdfield(m: CbcountfieldHtmlModel) -> str:
return render_template_string(
"""
<div>
<h4>{{ header or '' }}</h4>
<p class="stem">{{ stem or '' }}</p>
<div><label>{{ inputstem or '' }} <span>
<input type="checkbox"
class="xform-control"
placeholder="{{ inputplaceholder or '' }}"
size="{{ cols or '' }}"></span></label>
</div>
<a>{{ resetText or '' }}</a>
<p class="plgfooter">{{ '' }}</p>
</div>""".strip(),
**asdict(m.markup),
)
[docs]class CbAnswerWeb(PluginAnswerWeb, total=False):
count: int
new: int
[docs]class CbAnswerResp(PluginAnswerResp, total=False):
pass
[docs]def cb_answer(args: CbcountfieldAnswerModel) -> CbAnswerResp:
web: CbAnswerWeb = {}
result = CbAnswerResp(web=web)
c = args.input.c
count, previous = get_checked_count(
args.markup, args.taskID, args.info.primary_user
)
# Take the current answer into account.
if previous is None:
previous = "0"
if previous != c:
if c == "1":
count += 1
else:
count -= 1
nosave = args.input.nosave
limit = args.markup.limit
if isinstance(limit, int):
limit = int(limit) # not needed?
if count > limit:
web["result"] = "error"
web["count"] = limit
web["new"] = 0
return result
if not nosave:
save = {"c": c}
result["save"] = save
web["result"] = "saved"
web["count"] = count
web["new"] = 1 if c == "1" else 0
return result
[docs]def count_field(
user_fields: list[UserFieldObj], task_id: str, curr_user: User
) -> tuple[int, FieldValue]:
count = 0
previous = None
for u in user_fields:
fs = u["fields"]
if task_id not in fs:
continue
val = fs[task_id]
if val == "1":
count += 1
if curr_user == u["user"]:
previous = val
return count, previous
[docs]def get_checked_count(
markup: CbcountfieldMarkupModel, task_id: str, user_id: str
) -> tuple[int, FieldValue]:
groups = ["*"]
if isinstance(markup.groups, list):
groups = markup.groups
doc_id = TaskId.parse_doc_id(task_id)
curr_user = User.get_by_name(user_id)
assert curr_user is not None, f"Could not find user {user_id}"
d = DocEntry.find_by_id(doc_id)
if not d:
return 0, None # TODO handle error properly
user_fields, _, _, _ = get_fields_and_users(
[task_id],
RequestedGroups.from_name_list(groups),
d,
curr_user,
default_view_ctx,
access_option=GetFieldsAccess.AllowAlwaysNonTeacher,
)
return count_field(user_fields, task_id, curr_user)
[docs]def cb_reqs() -> PluginReqs:
return {
"js": ["cbcountfield"],
"css": ["/field/css/field.css"],
"multihtml": True,
}
cbcountfield_route = create_blueprint(
__name__,
"cbcountfield",
CbcountfieldHtmlModel,
CbcountfieldAnswerModel,
cb_answer,
cb_reqs,
csrf,
preprocess_multihtml,
)