Django-CMS 3.0.3 Publishing page duplicates data from django-cms-saq plugin

Django-cms-saq is tested against 2.4.x. I am trying to update a program to work with 3.0.X.

So far I have updated all imports but am encountering an unusual error. When I add a question (plugin) to the page and hit post, it creates two copies of the question in the database (viewed through the admin site). Deleting any copy removes both from the published page, but leaves the question in edit mode.

How do I solve the problem with this?

I will introduce some of the files here. Please let me know if you need other files.

Note that I am trying to add a multiple choice question.

From models.py:

from django.db import models
from django.db.models import Max, Sum

from cms.models import CMSPlugin, Page, Placeholder
from cms.models.fields import PageField
from taggit.managers import TaggableManager

from djangocms_text_ckeditor.models import AbstractText

...

class Question(CMSPlugin):
    QUESTION_TYPES = [
        ('S', 'Single-choice question'),
        ('M', 'Multi-choice question'),
        ('F', 'Free-text question'),
    ]

    slug = models.SlugField(
        help_text="A slug for identifying answers to this specific question "
        "(allows multiple only for multiple languages)")
    tags = TaggableManager(blank=True)
    label = models.CharField(max_length=512, blank=True)
    help_text = models.CharField(max_length=512, blank=True)
    question_type = models.CharField(max_length=1, choices=QUESTION_TYPES)
    optional = models.BooleanField(
        default=False,
        help_text="Only applies to free text questions",
    )

    depends_on_answer = models.ForeignKey(
        Answer, null=True, blank=True, related_name='trigger_questions')

    def copy_relations(self, oldinstance):
        for answer in oldinstance.answers.all():
            answer.pk = None
            answer.question = self
            answer.save()

        self.depends_on_answer = oldinstance.depends_on_answer

    @staticmethod
    def all_in_tree(page):
        root = page.get_root()
        # Remember that there might be questions on the root page as well!
        tree = root.get_descendants() | Page.objects.filter(id=root.id)
        placeholders = Placeholder.objects.filter(page__in=tree)
        return Question.objects.filter(placeholder__in=placeholders)

    @staticmethod
    def all_in_page(page):
        placeholders = Placeholder.objects.filter(page=page)
        return Question.objects.filter(placeholder__in=placeholders)

    def score(self, answers):
        if self.question_type == 'F':
            return 0
        elif self.question_type == 'S':
            return self.answers.get(slug=answers).score
        elif self.question_type == 'M':
            answers_list = answers.split(',')
            return sum([self.answers.get(slug=a).score for a in answers_list])

    @property
    def max_score(self):
        if not hasattr(self, '_max_score'):
            if self.question_type == "S":
                self._max_score = self.answers.aggregate(
                    Max('score'))['score__max']
            elif self.question_type == "M":
                self._max_score = self.answers.aggregate(
                    Sum('score'))['score__sum']
            else:
                self._max_score = None  # don't score free-text answers
        return self._max_score

    def percent_score_for_user(self, user):
        if self.max_score:
            try:
                score = Submission.objects.get(
                    question=self.slug,
                    user=user,
                ).score
            except Submission.DoesNotExist:
                return 0
            return 100.0 * score / self.max_score
        else:
            return None

    def __unicode__(self):
        return self.slug

...

      

From cms_plugins.py

import itertools
import operator

from django.contrib import admin
from django.utils.translation import ugettext as _

from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool

from cms_saq.models import Question, Answer, GroupedAnswer, Submission, \
        FormNav, ProgressBar, SectionedScoring, ScoreSection, BulkAnswer, \
        QuestionnaireText, SubmissionSetReview


from djangocms_text_ckeditor.cms_plugins import TextPlugin
from djangocms_text_ckeditor.models import Text

from bs4 import BeautifulSoup

...

class QuestionPlugin(CMSPluginBase):
    model = Question
    module = "SAQ"
    inlines = [AnswerAdmin]
    exclude = ('question_type',)

    def render(self, context, instance, placeholder):
        user = context['request'].user

        submission_set = None

        triggered = True
        depends_on = None
        if instance.depends_on_answer:
            depends_on = instance.depends_on_answer.pk
            try:
                Submission.objects.get(
                    user=user,
                    question=instance.depends_on_answer.question.slug,
                    answer=instance.depends_on_answer.slug,
                    submission_set=submission_set,
                )
                triggered = True
            except:
                triggered = False

        extra = {
            'question': instance,
            'answers': instance.answers.all(),
            'triggered': triggered,
            'depends_on': depends_on,
        }

        if user.is_authenticated():
            try:
                extra['submission'] = Submission.objects.get(
                    user=user,
                    question=instance.slug,
                    submission_set=submission_set,
                )
            except Submission.DoesNotExist:
                pass

        context.update(extra)
        return context

    def save_model(self, request, obj, form, change):
        obj.question_type = self.question_type
        super(QuestionPlugin, self).save_model(request, obj, form, change)


...


class MultiChoiceQuestionPlugin(QuestionPlugin):
    name = "Multi Choice Question"
    render_template = "cms_saq/multi_choice_question.html"
    question_type = "M"
    exclude = ('question_type', 'help_text')

...

plugin_pool.register_plugin(SingleChoiceQuestionPlugin)
plugin_pool.register_plugin(MultiChoiceQuestionPlugin)
plugin_pool.register_plugin(DropDownQuestionPlugin)
plugin_pool.register_plugin(GroupedDropDownQuestionPlugin)
plugin_pool.register_plugin(FreeTextQuestionPlugin)
plugin_pool.register_plugin(FreeNumberQuestionPlugin)
plugin_pool.register_plugin(FormNavPlugin)
plugin_pool.register_plugin(SubmissionSetReviewPlugin)
plugin_pool.register_plugin(SectionedScoringPlugin)
plugin_pool.register_plugin(ProgressBarPlugin)
plugin_pool.register_plugin(BulkAnswerPlugin)
plugin_pool.register_plugin(SessionDefinition)
plugin_pool.register_plugin(QuestionnaireTextPlugin)
plugin_pool.register_plugin(TranslatedTextPlugin)

      

From cms_app.py:

from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
from django.utils.translation import ugettext_lazy as _

class CMSSaq(CMSApp):
    name = _("Self Assessment")
    urls = ["cms_saq.urls"]

apphook_pool.register(CMSSaq)

      

Additional Information:

This duplicate observation creates a problem when trying to get the question object through its slug Question.objects.get(slug=question_slug)

. Such a request should only return one question. We get two questions returned.

import re

from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
from django.views.decorators.cache import never_cache
from django.utils import simplejson, datastructures
from django.conf import settings

from cms_saq.models import Question, Answer, Submission, SubmissionSet

ANSWER_RE = re.compile(r'^[\w-]+(,[\w-]+)*$')


@require_POST
def _submit(request):

    post_data = datastructures.MultiValueDict(request.POST)
    submission_set_tag = post_data.pop('submission_set_tag', '')

    for question_slug, answers in post_data.iteritems():

        # validate the question
        try:
            question = Question.objects.get(
                slug=question_slug,
                #placeholder__page__publisher_is_draft=False,
            )
        except Question.DoesNotExist:
            return HttpResponseBadRequest(
                "Invalid question '%s'" % question_slug,
            )

        # check answers is a list of slugs
        if question.question_type != 'F' and not ANSWER_RE.match(answers):
            return HttpResponseBadRequest("Invalid answers: %s" % answers)
        # validate and score the answer
        try:
            score = question.score(answers)
        except Answer.DoesNotExist:
            return HttpResponseBadRequest(
                "Invalid answer '%s:%s'" % (question_slug, answers)
            )

        # save, but don't update submissions belonging to an existing set
        filter_attrs = {
            'user': request.user.id,
            'question': question_slug,
            'submission_set': None,
        }

        attrs = {'answer': answers, 'score': score}

        rows = Submission.objects.filter(**filter_attrs).update(**attrs)

        if not rows:
            attrs.update(filter_attrs)
            Submission.objects.create(**attrs)

    # Create submission set if requested
    if submission_set_tag:
        submission_set_tag = submission_set_tag[0]

        if submission_set_tag:
            _create_submission_set(
                request, submission_set_tag
            )

    return HttpResponse("OK")

      

+3


source to share


1 answer


If you create a page in the CMS and add plugins to it, as soon as you click publish, the CMS will create a copy of each of these plugins as they are at that point in time. This gives the page a live version and allows you to make changes to it, which are kept in draft mode until you hit the post again.

This allows you to have a draft version of the site where changes / changes can be made and completed before they are published.

This way, seeing two copies for each plugin is not a problem.

As a side note, I highly recommend that you join the CMS user group at Google Plus if you are developing CMS sites; https://plus.google.com/communities/107689498573071376044

Update

Ok, so the plugin in the CMS is connected to a placeholder that is on the page and this is a page that has two versions of itself. Since every plugin is attached to a page, you can use this relationship to filter your plugins.

If you register your plugin with admin, or make calls to it, you just look at what's stored in your table, which, as I said, includes a draft and live version of the plugins.



So, when you request your plugin, do this:

questions = Question.objects.filter(placeholder__page__publisher_is_draft=True)

      

This will give you all the questions that come with the draft pages.

The only downside to this is that plugins are guaranteed to be bound to an object Page()

if the plugin is not installed on page_only = True

, but I was only interested in plugins attached to pages, so it works for me. You can read more about this in the docs .

Also, if you ever add CMS page links for any of your plugins, be sure to add a similar argument to the model field to make sure you restrict page objects to draft only;

page_link = models.ForeignKey(
    Page,
    limit_choices_to={'publisher_is_draft': True},
    help_text=_("Link to another page on the site."),
    on_delete=models.SET_NULL,
    related_name='myapp_page_link'
)

      

+2


source







All Articles