def start_a_discussion(request): # This view function creates a discussion, or returns an existing one. # Validate and retreive the Task and ModuleQuestion that the discussion # is to be attached to. task = get_object_or_404(Task, id=request.POST['task']) q = get_object_or_404(ModuleQuestion, id=request.POST['question']) # The user may not have permission to create - only to get. tq_filter = { "task": task, "question": q } tq = TaskAnswer.objects.filter(**tq_filter).first() if not tq: # Validate user can create discussion. Any user who can read the task can start # a discussion. if not task.has_read_priv(request.user): return JsonResponse({ "status": "error", "message": "You do not have permission!" }) # Get the TaskAnswer for this task. It may not exist yet. tq, isnew = TaskAnswer.objects.get_or_create(**tq_filter) discussion = Discussion.get_for(request.organization, tq) if not discussion: # Validate user can create discussion. if not task.has_read_priv(request.user): return JsonResponse({ "status": "error", "message": "You do not have permission!" }) # Get the Discussion. discussion = Discussion.get_for(request.organization, tq, create=True) return JsonResponse(discussion.render_context_dict(request.user))
def read_priv(): # Yes if they have read privs on the task in general... if task.has_read_priv(request.user, allow_access_to_deleted=True): # See below for checking if the task was deleted. return True # Yes if they are a guest in a discussion on this page. This applies # to the show-question page only, where we know which question is # being asked from the URL. if taskans: d = Discussion.get_for(request.organization, taskans) if d and d.is_participant(request.user): return True return False
def get_all_participants(self): # Get all users who have read access to this project. Inverse of # has_read_priv. from guidedmodules.models import Task, TaskAnswer from discussion.models import Discussion from collections import defaultdict participants = defaultdict( lambda: { "is_member": False, "is_admin": False, "editor_of": [], "discussion_guest_in": [], }) # Fetch users with read access. for pm in ProjectMembership.objects.filter( project=self).select_related("user"): participants[pm.user]["is_member"] = True participants[pm.user]["is_admin"] = pm.is_admin for task in Task.objects.filter(project=self).select_related("editor"): participants[task.editor]["editor_of"].append(task) for ta in TaskAnswer.objects.filter(task__project=self, task__deleted_at=None): d = Discussion.get_for(self.organization, ta) if d: for user in d.guests.all(): participants[user]["discussion_guest_in"].append(d) # Add text labels to describe user and authz. for user, info in participants.items(): info["user_details"] = user.render_context_dict(self.organization) descr = [] if info["is_admin"]: descr.append("admin") elif info["is_member"]: descr.append("member") if info["editor_of"]: descr.append("editor of %d task(s)" % len(info["editor_of"])) if info["discussion_guest_in"]: descr.append("guest in %d discussion(s)" % len(info["discussion_guest_in"])) info["role"] = "; ".join(descr) # Return sorted by username. return sorted(participants.items(), key=lambda kv: kv[0].username)
def show_question(request, task, answered, context, q, EncryptionProvider, set_ephemeral_encryption_cookies): # Always hide the fill-out-your-profile blurb on all question pages - it's # distracting from answering the question at hand. See task_view. request.suppress_prompt_banner = True # If this question cannot currently be answered (i.e. dependencies are unmet), # then redirect away from this page. If the user is allowed to use the authoring # tool, then allow seeing this question so they can edit all questions. authoring_tool_enabled = task.module.is_authoring_tool_enabled(request.user) is_answerable = (((q not in answered.unanswered) or (q in answered.can_answer)) and (q.key not in answered.was_imputed)) if not is_answerable and not authoring_tool_enabled: return HttpResponseRedirect(task.get_absolute_url()) # Is there a TaskAnswer for this yet? taskq = TaskAnswer.objects.filter(task=task, question=q).first() # Display requested question. # Is there an answer already? (If this question isn't answerable, i.e. if we're # only here because the user is using the authoring tool, then there is no # real answer to load.) answer = None if taskq and is_answerable: answer = taskq.get_current_answer() if answer and answer.cleared: # If the answer is cleared, treat as if it had not been answered. answer = None # For "module"-type questions, get the Module instance of the tasks that can # be an answer to this question, and get the existing Tasks that the user can # choose as an answer. answer_module = q.answer_type_module answer_tasks = [] if answer_module: # The user can choose from any Task instances they have read permission on # and that are of the correct Module type. answer_tasks = Task.get_all_tasks_readable_by(request.user, request.organization, recursive=True)\ .filter(module=answer_module) # Annotate the instances with whether the user also has write permission. for t in answer_tasks: t.can_write = t.has_write_priv(request.user) # Sort the instances: # first: the current answer, if any # then: tasks defined in the same project as this task # later: tasks defined in projects in the same folder as this task's project # last: everything else by reverse update date now = timezone.now() current_answer = answer.answered_by_task.first() if answer else None answer_tasks = sorted(answer_tasks, key = lambda t : ( not (t == current_answer), not (t.project == task.project), not (set(t.project.contained_in_folders.all()) & set(task.project.contained_in_folders.all())), now-t.updated, )) # Add instrumentation event. # How many times has this question been shown? i_prev_view = InstrumentationEvent.objects\ .filter(user=request.user, event_type="task-question-show", task=task, question=q)\ .order_by('-event_time')\ .first() # Save. InstrumentationEvent.objects.create( user=request.user, event_type="task-question-show", event_value=(i_prev_view.event_value+1) if i_prev_view else 1, module=task.module, question=q, project=task.project, task=task, answer=taskq, ) # Indicate for the InstrumentQuestionPageLoadTimes middleware that this is # a question page load. request._instrument_page_load = { "event_type": "task-question-request-duration", "module": task.module, "question": q, "project": task.project, "task": task, "answer": taskq, } # Construct the page. def render_markdown_field(field, output_format, **kwargs): template = q.spec.get(field) if not template: return None if not isinstance(template, str): raise ValueError("%s question %s %s is not a string" % (repr(q.module), q.key, field)) return module_logic.render_content({ "template": template, "format": "markdown", }, answered, output_format, "%s question %s %s" % (repr(q.module), q.key, field), **kwargs ) # Get any existing answer for this question. existing_answer = None if answer: existing_answer = answer.get_value(decryption_provider=EncryptionProvider()) # For longtext questions, because the WYSIWYG editor is initialized with HTML, # render the value as HTML. if existing_answer and q.spec["type"] == "longtext": import CommonMark existing_answer = CommonMark.HtmlRenderer().render(CommonMark.Parser().parse(existing_answer)) # What's the title/h1 of the page and the rest of the prompt? Render the # prompt field. If it starts with a paragraph, turn that paragraph into # the title. title = q.spec["title"] prompt = render_markdown_field("prompt", "html") m = re.match(r"^<p>([\w\W]*?)</p>\s*", prompt) if m: title = m.group(1) prompt = prompt[m.end():] # Get a default answer for this question. Render Jinja2 template, but don't turn # Markdown into HTML for plain text fields. For longtext fields, turn it into # HTML because the WYSIWYG editor is initialized with HTML. default_answer = render_markdown_field("default", "text" if q.spec["type"] != "longtext" else "html", demote_headings=False) context.update({ "header_col_active": "start" if (len(answered.as_dict()) == 0 and q.spec["type"] == "interstitial") else "questions", "q": q, "title": title, "prompt": prompt, "placeholder_answer": render_markdown_field("placeholder", "text") or "", # Render Jinja2 template but don't turn Markdown into HTML. "reference_text": render_markdown_field("reference_text", "html"), "history": taskq.get_history() if taskq else None, "answer_obj": answer, "answer": existing_answer, "default_answer": default_answer, "review_choices": [(0, 'Not Reviewed'), (1, 'Reviewed'), (2, 'Approved')], "discussion": Discussion.get_for(request.organization, taskq) if taskq else None, "show_discussion_members_count": True, "answer_module": answer_module, "answer_tasks": answer_tasks, "answer_tasks_show_user": len([ t for t in answer_tasks if t.editor != request.user ]) > 0, "context": module_logic.get_question_context(answered, q), # Helpers for showing date month, day, year dropdowns, with # localized strings and integer values. Default selections # are done in the template & client-side so that we can use # the client browser's timezone to determine the current date. "date_l8n": lambda : { "months": [ (timezone.now().replace(2016,m,1).strftime("%B"), m) for m in range(1, 12+1)], "days": [ d for d in range(1, 31+1)], "years": [ y for y in reversed(range(timezone.now().year-100, timezone.now().year+101))], }, "is_answerable": is_answerable, # only false if authoring tool is enabled, otherwise this page is not renderable "authoring_tool_enabled": authoring_tool_enabled, }) return render(request, "question.html", context)