Django-CMS 3.0.3 La publicación de una página duplica los datos del complemento django-cms-saq

Django-cms-saq se prueba para 2.4.x. Estoy tratando de actualizar el programa para que funcione con 3.0.X.

Hasta ahora, he actualizado todas las importaciones, pero me encuentro con un error inusual. Cuando agrego una pregunta (un complemento) a una página y presiono publicar, se crean dos copias de la pregunta en la base de datos (que se puede ver a través del sitio de administración). Al eliminar cualquiera de las copias, se eliminan ambas de la página publicada, pero se deja la pregunta en modo de edición.

¿Cómo me gustaría solucionar este problema?

Incluiré algunos de los archivos aquí. Por favor, hágamelo saber si necesita otros archivos.

Tenga en cuenta que estoy tratando de agregar una pregunta de opción múltiple.

Desde 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

...

Desde 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)

Desde 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)

Información Adicional:

Esta observación duplicada crea un problema cuando se intenta obtener el objeto de pregunta a través de su slug Question.objects.get (slug = question_slug). Dicha consulta solo debe devolver una pregunta. Lo que obtenemos aquí son dos preguntas devueltas.

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")
Mejor respuesta
Si crea una página en el CMS y le agrega complementos, una vez que presione publicar, el CMS creará una copia de cada uno de esos complementos tal como están en ese momento. Esto le da a la página una versión en vivo, y le permite hacer cambios en ella que se mantienen en modo borrador, hasta que vuelva a pulsar publicar.

Esto le permite tener una versión de borrador del sitio donde se pueden realizar ediciones / cambios y finalizarlos antes de hacerlos públicos.

Ver dos copias para cada complemento, por lo tanto, no es un problema.

Como nota adicional, le recomiendo que se una al grupo de usuarios de CMS en Google Plus si está desarrollando sitios de CMS; https://plus.google.com/communities/107689498573071376044

actualizar

Ok, entonces un complemento en el CMS se adjunta a un marcador de posición que está en una página, y es la página que tiene dos versiones de sí mismo. Debido a que cada complemento se adjunta a una página, puede usar esa relación para filtrar sus complementos.

Si registra su complemento en admin, o si desea objetar las llamadas, simplemente verá lo que está almacenado en su tabla, que como digo, incluye el borrador y la versión en vivo de los complementos.

Así que cuando estés preguntando por tu plugin haz esto;

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

Esto le dará todas las preguntas que se adjuntan a las páginas de borrador.

El único inconveniente de esto es que se garantiza que los complementos se adjuntarán a un objeto Page () a menos que el complemento esté configurado en page_only = True, pero solo he estado interesado en los complementos adjuntos a las páginas, así que me funciona. Vea el docs para más información sobre esto.

Además, si alguna vez agrega enlaces de páginas CMS a cualquiera de sus complementos, no olvide agregar un argumento similar al campo de su modelo para asegurarse de limitar los objetos de la página al borrador solamente;

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'
)

Por favor indique la dirección original:Django-CMS 3.0.3 La publicación de una página duplica los datos del complemento django-cms-saq - Código de registro